From bacadf4b4acf51365b6df326c72b51d63b1df18d Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 17 Jul 2026 14:54:45 +0800 Subject: [PATCH 001/161] feat(chainbase): add archive state collector Capture canonical block metadata and materialize reverse diffs from completed block snapshots. Classify archive state stores explicitly and project account-asset companion mutations before root merge. --- .../tron/core/db/TronStoreWithRevoking.java | 3 + .../archive/AccountAssetArchiveProjector.java | 124 ++++++ .../core/db2/archive/ArchiveStoreScope.java | 101 +++++ .../core/db2/archive/BlockChangeView.java | 135 ++++++ .../core/db2/archive/BlockReverseDiff.java | 81 ++++ .../db2/archive/BlockReverseDiffSink.java | 13 + .../archive/BoundedBlockReverseDiffQueue.java | 43 ++ .../org/tron/core/db2/archive/OldValue.java | 59 +++ .../core/db2/archive/OldValueCollector.java | 7 + .../archive/SnapshotOldValueCollector.java | 59 +++ .../org/tron/core/db2/core/Chainbase.java | 9 + .../org/tron/core/db2/core/SnapshotImpl.java | 6 + .../tron/core/db2/core/SnapshotManager.java | 130 +++++- .../main/java/org/tron/core/db2/ISession.java | 7 + .../core/db2/archive/BlockSnapshotMeta.java | 99 +++++ .../main/java/org/tron/core/db/Manager.java | 15 +- .../SnapshotOldValueCollectorTest.java | 416 ++++++++++++++++++ 17 files changed, 1303 insertions(+), 4 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiffSink.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/BoundedBlockReverseDiffQueue.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/OldValue.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/OldValueCollector.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java create mode 100644 common/src/main/java/org/tron/core/db2/archive/BlockSnapshotMeta.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java diff --git a/chainbase/src/main/java/org/tron/core/db/TronStoreWithRevoking.java b/chainbase/src/main/java/org/tron/core/db/TronStoreWithRevoking.java index 72e7a1cd82f..4d5b4bbcaf4 100644 --- a/chainbase/src/main/java/org/tron/core/db/TronStoreWithRevoking.java +++ b/chainbase/src/main/java/org/tron/core/db/TronStoreWithRevoking.java @@ -81,6 +81,9 @@ public String getDbName() { @PostConstruct private void init() { + if (revokingDB instanceof Chainbase) { + ((Chainbase) revokingDB).setRegistrationSource(getClass().getName()); + } revokingDatabase.add(revokingDB); dbStatService.register(db); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java new file mode 100644 index 00000000000..70a3b7fb9c0 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java @@ -0,0 +1,124 @@ +package org.tron.core.db2.archive; + +import com.google.common.primitives.Bytes; +import com.google.common.primitives.Longs; +import com.google.protobuf.InvalidProtocolBufferException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BooleanSupplier; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.store.AccountAssetStore; +import org.tron.protos.Protocol.Account; + +/** + * Projects the non-Chainbase {@code account-asset} mutations which SnapshotRoot otherwise creates + * implicitly while merging account snapshots. + */ +public final class AccountAssetArchiveProjector { + + public static final String ACCOUNT_DB = "account"; + public static final String ACCOUNT_ASSET_DB = "account-asset"; + + private final AccountAssetStore assetStore; + private final BooleanSupplier optimizationEnabled; + + public AccountAssetArchiveProjector(AccountAssetStore assetStore, + BooleanSupplier optimizationEnabled) { + this.assetStore = assetStore; + this.optimizationEnabled = optimizationEnabled; + } + + Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue rawPost) { + Account oldAccount = parse(rawOld); + Account postAccount = rawPost.isPresent() ? parse(rawPost.getValue()) : null; + boolean projectPost = postAccount != null + && (postAccount.getAssetOptimized() || optimizationEnabled.getAsBoolean()); + + Map oldAssets = physicalAssets(accountKey, oldAccount, + oldAccount != null && oldAccount.getAssetOptimized()); + Map postAssets = physicalAssets(accountKey, postAccount, projectPost); + + Set assetKeys = new HashSet<>(oldAssets.keySet()); + assetKeys.addAll(postAssets.keySet()); + List reverseAssets = new ArrayList<>(); + for (WrappedByteArray assetKey : assetKeys) { + byte[] oldValue = oldAssets.get(assetKey); + byte[] postValue = postAssets.get(assetKey); + if (!Arrays.equals(oldValue, postValue)) { + reverseAssets.add(new BlockReverseDiff.Entry(assetKey.getBytes(), + OldValue.fromNullable(oldValue))); + } + } + + OldValue canonicalOld = oldAccount == null ? OldValue.absent() + : OldValue.present(canonicalAccount(oldAccount, oldAccount.getAssetOptimized())); + BlockChangeView.PostValue canonicalPost = postAccount == null + ? BlockChangeView.PostValue.absent() + : BlockChangeView.PostValue.present(canonicalAccount(postAccount, projectPost)); + return new Projection(canonicalOld, canonicalPost, reverseAssets); + } + + private Map physicalAssets(byte[] accountKey, Account account, + boolean projected) { + Map result = new HashMap<>(); + if (account == null || !projected) { + return result; + } + if (account.getAssetOptimized()) { + assetStore.prefixQuery(accountKey).forEach((key, value) -> result.put( + WrappedByteArray.copyOf(key.getBytes()), Arrays.copyOf(value, value.length))); + } + account.getAssetV2Map().forEach((token, balance) -> { + WrappedByteArray key = WrappedByteArray.copyOf(Bytes.concat(accountKey, + token.getBytes(StandardCharsets.UTF_8))); + if (balance == 0) { + result.remove(key); + } else { + result.put(key, Longs.toByteArray(balance)); + } + }); + return result; + } + + private byte[] canonicalAccount(Account account, boolean projected) { + if (!projected) { + return account.toByteArray(); + } + return account.toBuilder() + .setAssetOptimized(true) + .clearAsset() + .clearAssetV2() + .build() + .toByteArray(); + } + + private Account parse(byte[] value) { + if (value == null) { + return null; + } + try { + return Account.parseFrom(value); + } catch (InvalidProtocolBufferException e) { + throw new IllegalStateException("Invalid account value while projecting archive state", e); + } + } + + static final class Projection { + final OldValue oldAccount; + final BlockChangeView.PostValue postAccount; + final List reverseAssets; + + private Projection(OldValue oldAccount, BlockChangeView.PostValue postAccount, + List reverseAssets) { + this.oldAccount = oldAccount; + this.postAccount = postAccount; + this.reverseAssets = reverseAssets; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java new file mode 100644 index 00000000000..e8a5a375c27 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java @@ -0,0 +1,101 @@ +package org.tron.core.db2.archive; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; +import org.tron.core.db2.core.Chainbase; + +/** Explicit classification of every database registered with {@code SnapshotManager}. */ +public final class ArchiveStoreScope { + + private static final Set STATE_DATABASES = immutableSet( + "abi", + "accountid-index", + "account-index", + "account", + "account-asset", + "accountTrie", + "asset-issue", + "asset-issue-v2", + "code", + "contract-state", + "contract", + "DelegatedResourceAccountIndex", + "DelegatedResource", + "delegation", + "properties", + "exchange", + "exchange-v2", + "market_account", + "market_order", + "market_pair_price_to_order", + "market_pair_to_price", + "proposal", + "storage-row", + "votes", + "witness_schedule", + "witness", + "nullifier", + "IncrementalMerkleTree"); + + private static final Set NON_STATE_DATABASES = immutableSet( + "account-trace", + "balance-trace", + "block", + "block-index", + "recent-block", + "recent-transaction", + "section-bloom", + "trans", + "trans-cache", + "transactionHistoryStore", + "transactionRetStore", + "tree-block-index"); + + private ArchiveStoreScope() { + } + + public static boolean isStateDatabase(String dbName) { + return STATE_DATABASES.contains(dbName); + } + + public static boolean isClassified(String dbName) { + return STATE_DATABASES.contains(dbName) || NON_STATE_DATABASES.contains(dbName); + } + + public static Set getStateDatabases() { + return STATE_DATABASES; + } + + public static Set getNonStateDatabases() { + return NON_STATE_DATABASES; + } + + public static void validate(Collection databases) { + Set duplicates = databases.stream() + .collect(Collectors.groupingBy(Chainbase::getDbName, Collectors.counting())) + .entrySet().stream() + .filter(entry -> entry.getValue() > 1) + .map(java.util.Map.Entry::getKey) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (!duplicates.isEmpty()) { + throw new IllegalStateException("Duplicate Chainbase dbName(s): " + duplicates); + } + + Set unknown = databases.stream() + .filter(database -> !isClassified(database.getDbName())) + .map(database -> database.getDbName() + " (" + database.getRegistrationSource() + ")") + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (!unknown.isEmpty()) { + throw new IllegalStateException( + "Archive state scope has unclassified Chainbase dbName(s): " + unknown); + } + } + + private static Set immutableSet(String... values) { + return Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(values))); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java b/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java new file mode 100644 index 00000000000..0e7e11ebfbb --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java @@ -0,0 +1,135 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.common.Value; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.Snapshot; +import org.tron.core.db2.core.SnapshotImpl; + +/** + * Immutable block boundary handed to an old-value collector. + * + *

Changed keys and post values are copied. Each database group deliberately retains a strong + * reference to the block layer's previous snapshot until collection finishes. + */ +public final class BlockChangeView { + + private final BlockSnapshotMeta meta; + private final List databases; + + private BlockChangeView(BlockSnapshotMeta meta, List databases) { + this.meta = Objects.requireNonNull(meta, "meta"); + this.databases = Collections.unmodifiableList(new ArrayList<>(databases)); + } + + public static BlockChangeView capture(BlockSnapshotMeta meta, List databases) { + List changes = new ArrayList<>(); + for (Chainbase database : databases) { + if (!ArchiveStoreScope.isStateDatabase(database.getDbName())) { + continue; + } + Snapshot head = database.getHead(); + if (!Snapshot.isImpl(head)) { + throw new IllegalStateException( + "Block snapshot head is not SnapshotImpl for dbName=" + database.getDbName()); + } + SnapshotImpl layer = (SnapshotImpl) head; + List entries = new ArrayList<>(); + layer.getDb().forEach(entry -> entries.add(new Change(entry.getKey().getBytes(), + entry.getValue().getOperator() == Value.Operator.DELETE + ? PostValue.absent() : PostValue.present(entry.getValue().getBytes())))); + entries.sort((left, right) -> BlockReverseDiff.compareUnsigned(left.key, right.key)); + changes.add(new DatabaseChanges(database.getDbName(), layer.getPrevious(), entries)); + } + changes.sort((left, right) -> left.dbName.compareTo(right.dbName)); + return new BlockChangeView(meta, changes); + } + + public BlockSnapshotMeta getMeta() { + return meta; + } + + public List getDatabases() { + return databases; + } + + public static final class DatabaseChanges { + private final String dbName; + private final Snapshot previous; + private final List changes; + + private DatabaseChanges(String dbName, Snapshot previous, List changes) { + this.dbName = dbName; + this.previous = Objects.requireNonNull(previous, "previous"); + this.changes = Collections.unmodifiableList(new ArrayList<>(changes)); + } + + public String getDbName() { + return dbName; + } + + public byte[] getPrevious(byte[] key) { + return previous.get(key); + } + + public List getChanges() { + return changes; + } + } + + public static final class Change { + private final byte[] key; + private final PostValue postValue; + + private Change(byte[] key, PostValue postValue) { + Objects.requireNonNull(key, "key"); + this.key = Arrays.copyOf(key, key.length); + this.postValue = postValue; + } + + public byte[] getKey() { + return Arrays.copyOf(key, key.length); + } + + public PostValue getPostValue() { + return postValue; + } + } + + public static final class PostValue { + private static final PostValue ABSENT = new PostValue(false, null); + + private final boolean present; + private final byte[] value; + + private PostValue(boolean present, byte[] value) { + this.present = present; + this.value = value; + } + + public static PostValue absent() { + return ABSENT; + } + + public static PostValue present(byte[] value) { + Objects.requireNonNull(value, "value"); + return new PostValue(true, Arrays.copyOf(value, value.length)); + } + + public boolean isPresent() { + return present; + } + + public byte[] getValue() { + if (!present) { + throw new IllegalStateException("absent post value has no bytes"); + } + return Arrays.copyOf(value, value.length); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java b/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java new file mode 100644 index 00000000000..30f94e77d67 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java @@ -0,0 +1,81 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +/** Immutable reverse diff for one canonical block. */ +public final class BlockReverseDiff { + + private final BlockSnapshotMeta meta; + private final List groups; + + public BlockReverseDiff(BlockSnapshotMeta meta, List groups) { + this.meta = Objects.requireNonNull(meta, "meta"); + List sorted = new ArrayList<>(groups); + sorted.sort(Comparator.comparing(DbGroup::getDbName)); + this.groups = Collections.unmodifiableList(sorted); + } + + public BlockSnapshotMeta getMeta() { + return meta; + } + + public List getGroups() { + return groups; + } + + public static final class DbGroup { + private final String dbName; + private final List entries; + + public DbGroup(String dbName, List entries) { + this.dbName = Objects.requireNonNull(dbName, "dbName"); + List sorted = new ArrayList<>(entries); + sorted.sort((left, right) -> compareUnsigned(left.key, right.key)); + this.entries = Collections.unmodifiableList(sorted); + } + + public String getDbName() { + return dbName; + } + + public List getEntries() { + return entries; + } + } + + public static final class Entry { + private final byte[] key; + private final OldValue oldValue; + + public Entry(byte[] key, OldValue oldValue) { + Objects.requireNonNull(key, "key"); + this.key = Arrays.copyOf(key, key.length); + this.oldValue = Objects.requireNonNull(oldValue, "oldValue"); + } + + public byte[] getKey() { + return Arrays.copyOf(key, key.length); + } + + public OldValue getOldValue() { + return oldValue; + } + } + + static int compareUnsigned(byte[] left, byte[] right) { + int length = Math.min(left.length, right.length); + for (int i = 0; i < length; i++) { + int comparison = Integer.compare(left[i] & 0xff, right[i] & 0xff); + if (comparison != 0) { + return comparison; + } + } + return Integer.compare(left.length, right.length); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiffSink.java b/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiffSink.java new file mode 100644 index 00000000000..b1bafdb8942 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiffSink.java @@ -0,0 +1,13 @@ +package org.tron.core.db2.archive; + +import org.tron.core.db2.archive.BlockSnapshotMeta; + +/** Downstream boundary for an archive writer or a bounded writer queue. */ +public interface BlockReverseDiffSink { + + void accept(BlockReverseDiff diff); + + default void revert(BlockSnapshotMeta meta) { + // A durable writer will override this and truncate/discard its uncommitted canonical tail. + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/BoundedBlockReverseDiffQueue.java b/chainbase/src/main/java/org/tron/core/db2/archive/BoundedBlockReverseDiffQueue.java new file mode 100644 index 00000000000..df07af4d3e1 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/BoundedBlockReverseDiffQueue.java @@ -0,0 +1,43 @@ +package org.tron.core.db2.archive; + +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +/** Bounded, lossless hand-off queue. A full queue applies block-commit backpressure. */ +public final class BoundedBlockReverseDiffQueue implements BlockReverseDiffSink { + + private final BlockingQueue queue; + + public BoundedBlockReverseDiffQueue(int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("capacity must be positive"); + } + queue = new ArrayBlockingQueue<>(capacity); + } + + @Override + public void accept(BlockReverseDiff diff) { + Objects.requireNonNull(diff, "diff"); + try { + queue.put(diff); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while applying archive queue backpressure", e); + } + } + + @Override + public void revert(BlockSnapshotMeta meta) { + queue.removeIf(diff -> diff.getMeta().equals(meta)); + } + + public BlockReverseDiff take() throws InterruptedException { + return queue.take(); + } + + public int size() { + return queue.size(); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/OldValue.java b/chainbase/src/main/java/org/tron/core/db2/archive/OldValue.java new file mode 100644 index 00000000000..b04cd650c17 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/OldValue.java @@ -0,0 +1,59 @@ +package org.tron.core.db2.archive; + +import java.util.Arrays; +import java.util.Objects; + +/** An old value which preserves the distinction between absent and present-empty. */ +public final class OldValue { + + private static final OldValue ABSENT = new OldValue(false, null); + + private final boolean present; + private final byte[] value; + + private OldValue(boolean present, byte[] value) { + this.present = present; + this.value = value; + } + + public static OldValue absent() { + return ABSENT; + } + + public static OldValue present(byte[] value) { + Objects.requireNonNull(value, "value"); + return new OldValue(true, Arrays.copyOf(value, value.length)); + } + + public static OldValue fromNullable(byte[] value) { + return value == null ? absent() : present(value); + } + + public boolean isPresent() { + return present; + } + + public byte[] getValue() { + if (!present) { + throw new IllegalStateException("absent old value has no bytes"); + } + return Arrays.copyOf(value, value.length); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof OldValue)) { + return false; + } + OldValue that = (OldValue) object; + return present == that.present && Arrays.equals(value, that.value); + } + + @Override + public int hashCode() { + return 31 * Boolean.hashCode(present) + Arrays.hashCode(value); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/OldValueCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/OldValueCollector.java new file mode 100644 index 00000000000..3cd97904535 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/OldValueCollector.java @@ -0,0 +1,7 @@ +package org.tron.core.db2.archive; + +/** Materializes a block reverse diff independently of its persistence format. */ +public interface OldValueCollector { + + BlockReverseDiff collect(BlockChangeView view); +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java new file mode 100644 index 00000000000..826b046178b --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java @@ -0,0 +1,59 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Scheme 2 collector: read old values from the completed block layer's previous view. */ +public final class SnapshotOldValueCollector implements OldValueCollector { + + private final AccountAssetArchiveProjector accountAssetProjector; + + public SnapshotOldValueCollector() { + this(null); + } + + public SnapshotOldValueCollector(AccountAssetArchiveProjector accountAssetProjector) { + this.accountAssetProjector = accountAssetProjector; + } + + @Override + public BlockReverseDiff collect(BlockChangeView view) { + List groups = new ArrayList<>(); + List accountAssetEntries = new ArrayList<>(); + for (BlockChangeView.DatabaseChanges database : view.getDatabases()) { + List entries = new ArrayList<>(); + for (BlockChangeView.Change change : database.getChanges()) { + byte[] key = change.getKey(); + OldValue oldValue = OldValue.fromNullable(database.getPrevious(key)); + BlockChangeView.PostValue postValue = change.getPostValue(); + if (accountAssetProjector != null + && AccountAssetArchiveProjector.ACCOUNT_DB.equals(database.getDbName())) { + AccountAssetArchiveProjector.Projection projection = accountAssetProjector.project( + key, oldValue.isPresent() ? oldValue.getValue() : null, postValue); + oldValue = projection.oldAccount; + postValue = projection.postAccount; + accountAssetEntries.addAll(projection.reverseAssets); + } + if (!sameLogicalValue(oldValue, postValue)) { + entries.add(new BlockReverseDiff.Entry(key, oldValue)); + } + } + if (!entries.isEmpty()) { + groups.add(new BlockReverseDiff.DbGroup(database.getDbName(), entries)); + } + } + if (!accountAssetEntries.isEmpty()) { + groups.add(new BlockReverseDiff.DbGroup( + AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, accountAssetEntries)); + } + return new BlockReverseDiff(view.getMeta(), groups); + } + + private boolean sameLogicalValue(OldValue oldValue, BlockChangeView.PostValue postValue) { + if (oldValue.isPresent() != postValue.isPresent()) { + return false; + } + return !oldValue.isPresent() || Arrays.equals(oldValue.getValue(), postValue.getValue()); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/Chainbase.java b/chainbase/src/main/java/org/tron/core/db2/core/Chainbase.java index 17a047f78ae..a9d0aa73a47 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/Chainbase.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/Chainbase.java @@ -36,6 +36,7 @@ public enum Cursor { private ThreadLocal cursor = new ThreadLocal<>(); private ThreadLocal offset = new ThreadLocal<>(); private Snapshot head; + private String registrationSource = Chainbase.class.getName(); public Chainbase(Snapshot head) { this.head = head; @@ -47,6 +48,14 @@ public String getDbName() { return head.getDbName(); } + public String getRegistrationSource() { + return registrationSource; + } + + public void setRegistrationSource(String registrationSource) { + this.registrationSource = registrationSource; + } + @Override public void setCursor(Cursor cursor) { this.cursor.set(cursor); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java index bc31b406b30..32764dc332c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java @@ -12,6 +12,8 @@ import java.util.Objects; import java.util.Set; import lombok.Getter; +import lombok.Setter; +import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.common.HashDB; import org.tron.core.db2.common.Key; import org.tron.core.db2.common.Value; @@ -23,6 +25,10 @@ public class SnapshotImpl extends AbstractSnapshot { @Getter protected Snapshot root; + @Getter + @Setter + private BlockSnapshotMeta blockSnapshotMeta; + SnapshotImpl(Snapshot snapshot) { root = snapshot.getRoot(); synchronized (this) { diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index e20490d93c0..bb1547f165c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -34,6 +34,12 @@ import org.tron.core.db.RevokingDatabase; import org.tron.core.db.TronDatabase; import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.ArchiveStoreScope; +import org.tron.core.db2.archive.BlockChangeView; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockReverseDiffSink; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.OldValueCollector; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.IRevokingDB; import org.tron.core.db2.common.Key; @@ -84,6 +90,9 @@ public class SnapshotManager implements RevokingDatabase { private int checkpointVersion = 1; // default v1 + private OldValueCollector oldValueCollector; + private BlockReverseDiffSink blockReverseDiffSink; + public SnapshotManager(String checkpointPath) { } @@ -218,6 +227,82 @@ public synchronized void commit() { }); } + /** + * Commits a block session and materializes its reverse diff using the configured collector. + * Plain transaction/pending sessions must continue to use {@link #commit()} or merge/revoke. + */ + public synchronized void commit(BlockSnapshotMeta meta) { + Objects.requireNonNull(meta, "meta"); + if (activeSession <= 0) { + throw new RevokingStoreIllegalStateException(activeSession); + } + + validateBlockMeta(meta); + for (Chainbase db : dbs) { + Snapshot head = db.getHead(); + if (!Snapshot.isImpl(head)) { + throw new IllegalStateException( + "Cannot bind block metadata to non-SnapshotImpl head: " + db.getDbName()); + } + ((SnapshotImpl) head).setBlockSnapshotMeta(meta); + } + + BlockReverseDiff reverseDiff = null; + if (oldValueCollector != null) { + reverseDiff = oldValueCollector.collect(BlockChangeView.capture(meta, dbs)); + } + + dbs.forEach(db -> { + if (db.getHead().isOptimized()) { + db.getHead().reloadToMem(); + } + }); + + if (reverseDiff != null) { + blockReverseDiffSink.accept(reverseDiff); + } + --activeSession; + } + + private void validateBlockMeta(BlockSnapshotMeta meta) { + BlockSnapshotMeta previousMeta = null; + for (Chainbase db : dbs) { + if (!ArchiveStoreScope.isStateDatabase(db.getDbName())) { + continue; + } + Snapshot head = db.getHead(); + if (!Snapshot.isImpl(head)) { + continue; + } + Snapshot previous = head.getPrevious(); + BlockSnapshotMeta candidate = Snapshot.isImpl(previous) + ? ((SnapshotImpl) previous).getBlockSnapshotMeta() : null; + if (candidate != null && previousMeta != null && !previousMeta.equals(candidate)) { + throw new IllegalStateException("Previous block metadata differs across state databases"); + } + if (candidate != null) { + previousMeta = candidate; + } + } + if (previousMeta == null) { + return; + } + if (meta.getEpoch() != previousMeta.getEpoch() + 1 + || meta.getBlockNumber() != previousMeta.getBlockNumber() + 1 + || !Arrays.equals(meta.getParentHash(), previousMeta.getBlockHash())) { + throw new IllegalStateException( + "Non-contiguous block snapshot metadata: previous=" + previousMeta + ", current=" + meta); + } + } + + /** Enables archive collection after all Chainbase stores have registered. */ + public synchronized void installArchiveCollector(OldValueCollector collector, + BlockReverseDiffSink sink) { + ArchiveStoreScope.validate(dbs); + oldValueCollector = Objects.requireNonNull(collector, "collector"); + blockReverseDiffSink = Objects.requireNonNull(sink, "sink"); + } + public synchronized void pop() { if (activeSession != 0) { throw new RevokingStoreIllegalStateException( @@ -229,6 +314,28 @@ public synchronized void pop() { String.format("there is not snapshot to be popped, current: %d", size)); } + if (blockReverseDiffSink != null) { + BlockSnapshotMeta meta = null; + for (Chainbase db : dbs) { + if (!ArchiveStoreScope.isStateDatabase(db.getDbName())) { + continue; + } + Snapshot head = db.getHead(); + if (Snapshot.isImpl(head)) { + BlockSnapshotMeta candidate = ((SnapshotImpl) head).getBlockSnapshotMeta(); + if (candidate != null && meta != null && !meta.equals(candidate)) { + throw new IllegalStateException("Mismatched block metadata while reverting snapshots"); + } + if (candidate != null) { + meta = candidate; + } + } + } + if (meta != null) { + blockReverseDiffSink.revert(meta); + } + } + disabled = true; try { @@ -286,7 +393,22 @@ public boolean shouldBeRefreshed() { private void refresh() { List> futures = new ArrayList<>(dbs.size()); + Chainbase properties = null; + if (oldValueCollector != null) { + properties = dbs.stream() + .filter(db -> "properties".equals(db.getDbName())) + .findFirst() + .orElse(null); + if (properties != null) { + // Account root projection reads the durable optimization flag. Make that dependency + // deterministic when archive mode projects account-asset changes at block boundaries. + refreshOne(properties); + } + } for (Chainbase db : dbs) { + if (db == properties) { + continue; + } futures.add(flushServices.get(db.getDbName()).submit(() -> refreshOne(db))); } Future future = Futures.allAsList(futures); @@ -581,8 +703,14 @@ public Session(SnapshotManager snapshotManager, boolean disableOnExit) { @Override public void commit() { - applySnapshot = false; snapshotManager.commit(); + applySnapshot = false; + } + + @Override + public void commit(BlockSnapshotMeta meta) { + snapshotManager.commit(meta); + applySnapshot = false; } @Override diff --git a/common/src/main/java/org/tron/core/db2/ISession.java b/common/src/main/java/org/tron/core/db2/ISession.java index 21445fb4d7d..c9dfa6f6961 100644 --- a/common/src/main/java/org/tron/core/db2/ISession.java +++ b/common/src/main/java/org/tron/core/db2/ISession.java @@ -1,9 +1,16 @@ package org.tron.core.db2; +import org.tron.core.db2.archive.BlockSnapshotMeta; + public interface ISession extends AutoCloseable { void commit(); + /** Commit a successfully applied block and bind its canonical identity to the snapshot. */ + default void commit(BlockSnapshotMeta meta) { + commit(); + } + void revoke(); void merge(); diff --git a/common/src/main/java/org/tron/core/db2/archive/BlockSnapshotMeta.java b/common/src/main/java/org/tron/core/db2/archive/BlockSnapshotMeta.java new file mode 100644 index 00000000000..86bc61fbffa --- /dev/null +++ b/common/src/main/java/org/tron/core/db2/archive/BlockSnapshotMeta.java @@ -0,0 +1,99 @@ +package org.tron.core.db2.archive; + +import java.util.Arrays; +import java.util.Objects; + +/** + * Immutable identity of a successfully applied canonical block snapshot. + */ +public final class BlockSnapshotMeta { + + private static final int HASH_LENGTH = 32; + + private final long epoch; + private final long blockNumber; + private final byte[] blockHash; + private final byte[] parentHash; + private final long timestamp; + + public BlockSnapshotMeta(long epoch, long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp) { + if (epoch < 0) { + throw new IllegalArgumentException("epoch must not be negative"); + } + if (blockNumber < 0) { + throw new IllegalArgumentException("blockNumber must not be negative"); + } + this.epoch = epoch; + this.blockNumber = blockNumber; + this.blockHash = copyHash(blockHash, "blockHash"); + this.parentHash = copyHash(parentHash, "parentHash"); + this.timestamp = timestamp; + } + + public static BlockSnapshotMeta forBlock(long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp) { + return new BlockSnapshotMeta(blockNumber, blockNumber, blockHash, parentHash, timestamp); + } + + private static byte[] copyHash(byte[] hash, String name) { + Objects.requireNonNull(hash, name); + if (hash.length != HASH_LENGTH) { + throw new IllegalArgumentException(name + " must be exactly " + HASH_LENGTH + " bytes"); + } + return Arrays.copyOf(hash, hash.length); + } + + public long getEpoch() { + return epoch; + } + + public long getBlockNumber() { + return blockNumber; + } + + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + public byte[] getParentHash() { + return Arrays.copyOf(parentHash, parentHash.length); + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof BlockSnapshotMeta)) { + return false; + } + BlockSnapshotMeta that = (BlockSnapshotMeta) object; + return epoch == that.epoch + && blockNumber == that.blockNumber + && timestamp == that.timestamp + && Arrays.equals(blockHash, that.blockHash) + && Arrays.equals(parentHash, that.parentHash); + } + + @Override + public int hashCode() { + int result = Objects.hash(epoch, blockNumber, timestamp); + result = 31 * result + Arrays.hashCode(blockHash); + result = 31 * result + Arrays.hashCode(parentHash); + return result; + } + + @Override + public String toString() { + return "BlockSnapshotMeta{" + + "epoch=" + epoch + + ", blockNumber=" + blockNumber + + ", timestamp=" + timestamp + + '}'; + } +} diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 9d7a7c979b9..f9a4047802d 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -113,6 +113,7 @@ import org.tron.core.db.api.MigrateTurkishKeyHelper; import org.tron.core.db.api.MoveAbiHelper; import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.exception.AccountResourceInsufficientException; @@ -1091,6 +1092,14 @@ private void applyBlock(BlockCapsule block, List txs) } } + private void commitBlockSession(ISession blockSession, BlockCapsule block) { + blockSession.commit(BlockSnapshotMeta.forBlock( + block.getNum(), + block.getBlockId().getBytes(), + block.getParentHash().getBytes(), + block.getTimeStamp())); + } + private void switchFork(BlockCapsule newHead) throws ValidateSignatureException, ContractValidateException, ContractExeException, ValidateScheduleException, AccountResourceInsufficientException, TaposException, @@ -1154,7 +1163,7 @@ private void switchFork(BlockCapsule newHead) tx.setVerified(false); } applyBlock(item.getBlk().setSwitch(true)); - tmpSession.commit(); + commitBlockSession(tmpSession, item.getBlk()); } catch (AccountResourceInsufficientException | ValidateSignatureException | ContractValidateException @@ -1192,7 +1201,7 @@ private void switchFork(BlockCapsule newHead) // todo process the exception carefully later try (ISession tmpSession = revokingStore.buildSession()) { applyBlock(khaosBlock.getBlk().setSwitch(true)); - tmpSession.commit(); + commitBlockSession(tmpSession, khaosBlock.getBlk()); } catch (AccountResourceInsufficientException | ValidateSignatureException | ContractValidateException @@ -1388,7 +1397,7 @@ public void pushBlock(final BlockCapsule block) long oldSolidNum = getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); try (ISession tmpSession = revokingStore.buildSession()) { applyBlock(newBlock, txs); - tmpSession.commit(); + commitBlockSession(tmpSession, newBlock); } catch (Throwable throwable) { logger.error(throwable.getMessage(), throwable); khaosDb.removeBlk(block.getBlockId()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java new file mode 100644 index 00000000000..8b848cbd00a --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -0,0 +1,416 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.primitives.Bytes; +import com.google.common.primitives.Longs; +import com.google.protobuf.ByteString; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import org.junit.Test; +import org.tron.common.BaseMethodTest; +import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.common.DB; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.SnapshotImpl; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.core.SnapshotRoot; +import org.tron.core.store.AccountAssetStore; +import org.tron.protos.Protocol.Account; + +public class SnapshotOldValueCollectorTest extends BaseMethodTest { + + @Test + public void collectsBlockPreStateAfterNestedSessionsFinish() { + MemoryDb memoryDb = new MemoryDb("abi"); + byte[] changed = bytes("changed"); + byte[] deleted = bytes("deleted"); + byte[] created = bytes("created"); + byte[] empty = bytes("empty"); + byte[] createThenDelete = bytes("create-then-delete"); + memoryDb.put(changed, bytes("old")); + memoryDb.put(deleted, bytes("gone")); + memoryDb.put(empty, new byte[0]); + + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + List captured = new ArrayList<>(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), captured::add); + + byte[] hash = new byte[32]; + hash[31] = 1; + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash, new byte[32], 3_000L); + try (ISession block = manager.buildSession()) { + database.put(changed, bytes("intermediate")); + try (ISession transaction = manager.buildSession()) { + database.put(changed, bytes("new")); + database.put(created, bytes("created-value")); + transaction.merge(); + } + try (ISession revertedTransaction = manager.buildSession()) { + database.put(bytes("reverted"), bytes("not-visible")); + } + database.delete(deleted); + database.put(empty, new byte[0]); + database.put(createThenDelete, bytes("temporary")); + database.delete(createThenDelete); + block.commit(meta); + } + + assertEquals(1, captured.size()); + BlockReverseDiff diff = captured.get(0); + assertEquals(meta, diff.getMeta()); + assertEquals(meta, ((SnapshotImpl) database.getHead()).getBlockSnapshotMeta()); + assertEquals(1, diff.getGroups().size()); + DbGroup group = diff.getGroups().get(0); + assertEquals("abi", group.getDbName()); + assertEquals(3, group.getEntries().size()); + + assertArrayEquals(bytes("old"), find(group, changed).getOldValue().getValue()); + assertArrayEquals(bytes("gone"), find(group, deleted).getOldValue().getValue()); + assertFalse(find(group, created).getOldValue().isPresent()); + assertFalse(contains(group, empty)); + assertFalse(contains(group, createThenDelete)); + assertFalse(contains(group, bytes("reverted"))); + manager.shutdown(); + } + + @Test + public void preservesPresentEmptyAndEmitsNoopBlockMetadata() { + MemoryDb memoryDb = new MemoryDb("abi"); + byte[] key = bytes("key"); + memoryDb.put(key, new byte[0]); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + List captured = new ArrayList<>(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), captured::add); + + try (ISession block = manager.buildSession()) { + database.put(key, bytes("value")); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + assertTrue(find(captured.get(0).getGroups().get(0), key).getOldValue().isPresent()); + assertEquals(0, + find(captured.get(0).getGroups().get(0), key).getOldValue().getValue().length); + + try (ISession block = manager.buildSession()) { + block.commit(BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L)); + } + assertEquals(2, captured.size()); + assertTrue(captured.get(1).getGroups().isEmpty()); + manager.shutdown(); + } + + @Test + public void rejectsUnknownOrDuplicateRegisteredDatabaseNames() { + SnapshotManager unknown = new SnapshotManager(""); + unknown.add(new Chainbase(new SnapshotRoot(new MemoryDb("new-store")))); + IllegalStateException unknownError = assertThrows(IllegalStateException.class, + () -> unknown.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { })); + assertTrue(unknownError.getMessage().contains("new-store")); + unknown.shutdown(); + + SnapshotManager duplicate = new SnapshotManager(""); + duplicate.add(new Chainbase(new SnapshotRoot(new MemoryDb("abi")))); + duplicate.add(new Chainbase(new SnapshotRoot(new MemoryDb("abi")))); + IllegalStateException duplicateError = assertThrows(IllegalStateException.class, + () -> duplicate.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { })); + assertTrue(duplicateError.getMessage().contains("Duplicate")); + duplicate.shutdown(); + } + + @Test + public void classifiesEveryChainbaseRegisteredByTheApplication() { + SnapshotManager applicationManager = context.getBean(SnapshotManager.class); + ArchiveStoreScope.validate(applicationManager.getDbs()); + } + + @Test + public void matchesReferenceStateForRandomBlockOperations() { + MemoryDb memoryDb = new MemoryDb("abi"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + List captured = new ArrayList<>(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), captured::add); + + Random random = new Random(0x5a17L); + Map reference = new HashMap<>(); + for (int blockNumber = 1; blockNumber <= 40; blockNumber++) { + Map before = copy(reference); + try (ISession block = manager.buildSession()) { + for (int operation = 0; operation < 25; operation++) { + String key = "key-" + random.nextInt(12); + int action = random.nextInt(4); + boolean nested = random.nextBoolean(); + boolean keepNested = random.nextBoolean(); + if (nested) { + try (ISession transaction = manager.buildSession()) { + byte[] post = applyRandomOperation(database, key, action, blockNumber, operation); + if (keepNested) { + updateReference(reference, key, post); + transaction.merge(); + } + } + } else { + byte[] post = applyRandomOperation(database, key, action, blockNumber, operation); + updateReference(reference, key, post); + } + } + block.commit(BlockSnapshotMeta.forBlock(blockNumber, hash(blockNumber), + hash(blockNumber - 1), blockNumber)); + } + + BlockReverseDiff diff = captured.get(captured.size() - 1); + Map actual = new HashMap<>(); + if (!diff.getGroups().isEmpty()) { + diff.getGroups().get(0).getEntries().forEach(entry -> actual.put( + new String(entry.getKey(), java.nio.charset.StandardCharsets.UTF_8), + entry.getOldValue())); + } + Set keys = new HashSet<>(before.keySet()); + keys.addAll(reference.keySet()); + for (String key : keys) { + byte[] oldValue = before.get(key); + byte[] postValue = reference.get(key); + if (Arrays.equals(oldValue, postValue)) { + assertFalse("no-op key was emitted: " + key, actual.containsKey(key)); + } else { + assertTrue("changed key was not emitted: " + key, actual.containsKey(key)); + OldValue archived = actual.get(key); + assertEquals(oldValue != null, archived.isPresent()); + if (oldValue != null) { + assertArrayEquals(oldValue, archived.getValue()); + } + } + } + assertEquals(keys.stream().filter(key -> !Arrays.equals(before.get(key), reference.get(key))) + .count(), actual.size()); + } + manager.shutdown(); + } + + @Test + public void projectsAccountAssetTransitionBeforeRootMerge() { + byte[] address = bytes("account-address"); + byte[] token = bytes("1000001"); + Account oldAccount = Account.newBuilder() + .setAddress(ByteString.copyFrom(address)) + .putAssetV2("1000001", 100L) + .build(); + Account postAccount = oldAccount.toBuilder().putAssetV2("1000001", 80L).build(); + + MemoryDb memoryDb = new MemoryDb("account"); + memoryDb.put(address, oldAccount.toByteArray()); + AccountAssetStore assetStore = mock(AccountAssetStore.class); + when(assetStore.prefixQuery(any(byte[].class))).thenReturn(new HashMap<>()); + + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + List captured = new ArrayList<>(); + AccountAssetArchiveProjector projector = new AccountAssetArchiveProjector(assetStore, + () -> true); + manager.installArchiveCollector(new SnapshotOldValueCollector(projector), captured::add); + + try (ISession block = manager.buildSession()) { + database.put(address, postAccount.toByteArray()); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + + DbGroup accountGroup = captured.get(0).getGroups().stream() + .filter(group -> "account".equals(group.getDbName())) + .findFirst().orElseThrow(AssertionError::new); + Account archivedAccount; + try { + archivedAccount = Account.parseFrom(find(accountGroup, address).getOldValue().getValue()); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw new AssertionError(e); + } + assertFalse(archivedAccount.getAssetOptimized()); + assertEquals(100L, archivedAccount.getAssetV2Map().get("1000001").longValue()); + + DbGroup assetGroup = captured.get(0).getGroups().stream() + .filter(group -> "account-asset".equals(group.getDbName())) + .findFirst().orElseThrow(AssertionError::new); + Entry assetEntry = find(assetGroup, Bytes.concat(address, token)); + assertFalse(assetEntry.getOldValue().isPresent()); + manager.shutdown(); + } + + @Test + public void projectsOldPhysicalAssetValueForOptimizedAccount() { + byte[] address = bytes("optimized-address"); + byte[] token = bytes("1000002"); + byte[] assetKey = Bytes.concat(address, token); + Account oldAccount = Account.newBuilder() + .setAddress(ByteString.copyFrom(address)) + .setAssetOptimized(true) + .build(); + Account postAccount = oldAccount.toBuilder().putAssetV2("1000002", 80L).build(); + + MemoryDb memoryDb = new MemoryDb("account"); + memoryDb.put(address, oldAccount.toByteArray()); + AccountAssetStore assetStore = mock(AccountAssetStore.class); + Map persisted = new HashMap<>(); + persisted.put(WrappedByteArray.copyOf(assetKey), Longs.toByteArray(100L)); + when(assetStore.prefixQuery(any(byte[].class))).thenReturn(persisted); + + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + List captured = new ArrayList<>(); + manager.installArchiveCollector(new SnapshotOldValueCollector( + new AccountAssetArchiveProjector(assetStore, () -> true)), captured::add); + + try (ISession block = manager.buildSession()) { + database.put(address, postAccount.toByteArray()); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + + assertFalse(captured.get(0).getGroups().stream() + .anyMatch(group -> "account".equals(group.getDbName()))); + DbGroup assetGroup = captured.get(0).getGroups().stream() + .filter(group -> "account-asset".equals(group.getDbName())) + .findFirst().orElseThrow(AssertionError::new); + assertArrayEquals(Longs.toByteArray(100L), + find(assetGroup, assetKey).getOldValue().getValue()); + manager.shutdown(); + } + + private static Entry find(DbGroup group, byte[] key) { + return group.getEntries().stream() + .filter(entry -> Arrays.equals(entry.getKey(), key)) + .findFirst() + .orElseThrow(AssertionError::new); + } + + private static boolean contains(DbGroup group, byte[] key) { + return group.getEntries().stream().anyMatch(entry -> Arrays.equals(entry.getKey(), key)); + } + + private static byte[] bytes(String value) { + return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] applyRandomOperation(Chainbase database, String key, int action, + int blockNumber, int operation) { + byte[] encodedKey = bytes(key); + if (action == 0) { + database.delete(encodedKey); + return null; + } + byte[] value = action == 1 ? new byte[0] + : bytes("value-" + blockNumber + '-' + operation + '-' + action); + database.put(encodedKey, value); + return value; + } + + private static void updateReference(Map reference, String key, byte[] value) { + if (value == null) { + reference.remove(key); + } else { + reference.put(key, Arrays.copyOf(value, value.length)); + } + } + + private static Map copy(Map source) { + Map copy = new HashMap<>(); + source.forEach((key, value) -> copy.put(key, Arrays.copyOf(value, value.length))); + return copy; + } + + private static final class MemoryDb implements DB { + private final String name; + private final Map values = new LinkedHashMap<>(); + + private MemoryDb(String name) { + this.name = name; + } + + @Override + public byte[] get(byte[] key) { + byte[] value = values.get(WrappedByteArray.of(key)); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + @Override + public void put(byte[] key, byte[] value) { + values.put(WrappedByteArray.copyOf(key), Arrays.copyOf(value, value.length)); + } + + @Override + public long size() { + return values.size(); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + @Override + public void remove(byte[] key) { + values.remove(WrappedByteArray.of(key)); + } + + @Override + public Iterator> iterator() { + List> entries = new ArrayList<>(); + values.forEach((key, value) -> entries.add(new AbstractMap.SimpleImmutableEntry<>( + key.getBytes(), Arrays.copyOf(value, value.length)))); + return entries.iterator(); + } + + @Override + public void close() { + values.clear(); + } + + @Override + public String getDbName() { + return name; + } + + @Override + public void stat() { + } + + @Override + public DB newInstance() { + return new MemoryDb(name); + } + } +} From 7cbd7c971fc962279b0449e3e31ae270c9352a91 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 17 Jul 2026 15:22:39 +0800 Subject: [PATCH 002/161] feat(chainbase): persist archive block history Add deterministic history body and index records, ordered durable commit markers, and bounded asynchronous persistence. Gate checkpoint advancement on committed archive history and cover crash, corruption, backpressure, and reorg behavior. --- .../db2/archive/ArchiveHistoryWriter.java | 265 +++++++++++++ .../archive/ArchivePersistenceException.java | 13 + .../db2/archive/AsyncArchiveHistorySink.java | 241 +++++++++++ .../core/db2/archive/BlockHistoryCodec.java | 322 +++++++++++++++ .../archive/DurableBlockReverseDiffSink.java | 9 + .../core/db2/archive/HistoryCommitMarker.java | 58 +++ .../db2/archive/HistoryCommitMarkerCodec.java | 151 +++++++ .../core/db2/archive/HistoryCommitStore.java | 180 +++++++++ .../core/db2/archive/HistoryIndexCodec.java | 243 ++++++++++++ .../db2/archive/HistoryIndexLocation.java | 37 ++ .../core/db2/archive/HistoryIndexRecord.java | 66 ++++ .../core/db2/archive/HistoryIndexStore.java | 238 +++++++++++ .../core/db2/archive/HistoryLocation.java | 51 +++ .../core/db2/archive/HistorySegmentStore.java | 374 ++++++++++++++++++ .../tron/core/db2/core/SnapshotManager.java | 52 +++ .../db2/archive/ArchiveHistoryWriterTest.java | 167 ++++++++ .../archive/AsyncArchiveHistorySinkTest.java | 139 +++++++ .../db2/archive/BlockHistoryCodecTest.java | 93 +++++ .../db2/archive/HistoryIndexStoreTest.java | 117 ++++++ .../db2/archive/HistorySegmentStoreTest.java | 107 +++++ .../SnapshotOldValueCollectorTest.java | 33 ++ 21 files changed, 2956 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchivePersistenceException.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/BlockHistoryCodec.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarker.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarkerCodec.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexCodec.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexLocation.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexRecord.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoryLocation.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/BlockHistoryCodecTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/HistoryIndexStoreTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/HistorySegmentStoreTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java new file mode 100644 index 00000000000..416837be428 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java @@ -0,0 +1,265 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.tron.core.db2.archive.HistoryIndexStore.ScannedIndexRecord; +import org.tron.core.db2.archive.HistorySegmentStore.ScannedRecord; + +/** + * Ordered history body/index/marker writer. A marker is the only reader-visible commit boundary. + */ +public final class ArchiveHistoryWriter implements DurableBlockReverseDiffSink, Closeable { + + private final HistorySegmentStore bodies; + private final HistoryIndexStore index; + private final HistoryCommitStore commits; + private final List participatingDatabases; + private final DurabilityHook hook; + + public ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, + Set participatingDatabases) throws IOException { + this(archiveDirectory, maxSegmentSize, participatingDatabases, (stage, meta) -> { }); + } + + ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, + Set participatingDatabases, DurabilityHook hook) throws IOException { + this.bodies = new HistorySegmentStore(archiveDirectory, new BlockHistoryCodec(), + maxSegmentSize); + this.index = new HistoryIndexStore(archiveDirectory, new HistoryIndexCodec()); + this.commits = new HistoryCommitStore(archiveDirectory, new HistoryCommitMarkerCodec()); + this.participatingDatabases = new ArrayList<>(participatingDatabases); + this.participatingDatabases.sort(String::compareTo); + this.hook = hook; + recoverPreparedSuffix(); + } + + @Override + public synchronized void accept(BlockReverseDiff diff) { + try { + validateNext(diff.getMeta()); + hook.before(Stage.APPEND_BODY, diff.getMeta()); + HistoryLocation bodyLocation = bodies.append(diff); + hook.before(Stage.APPEND_INDEX, diff.getMeta()); + HistoryIndexRecord indexRecord = HistoryIndexRecord.from(diff, bodyLocation); + HistoryIndexLocation indexLocation = index.append(indexRecord); + hook.before(Stage.SYNC_BODY, diff.getMeta()); + bodies.sync(); + hook.before(Stage.SYNC_INDEX, diff.getMeta()); + index.sync(); + hook.before(Stage.COMMIT_MARKER, diff.getMeta()); + HistoryCommitMarker head = commits.head(); + long previousEpoch = head == null ? diff.getMeta().getEpoch() - 1 + : head.getMeta().getEpoch(); + commits.commit(new HistoryCommitMarker(diff.getMeta(), previousEpoch, bodyLocation, + indexLocation, batchId(), participatingDatabases)); + } catch (IOException | RuntimeException e) { + handleWriteFailure(diff.getMeta(), e); + } + } + + @Override + public synchronized void revert(BlockSnapshotMeta meta) { + try { + HistoryCommitMarker head = commits.head(); + if (head != null && head.getMeta().equals(meta)) { + commits.removeHead(meta); + HistoryCommitMarker previous = commits.head(); + index.truncateAfter(previous == null ? null : previous.getIndexLocation()); + bodies.truncateAfter(previous == null ? null : previous.getHistoryLocation()); + return; + } + + ScannedRecord bodyHead = last(bodies.getScanResult().getRecords()); + ScannedIndexRecord indexHead = last(index.getScanResult().getRecords()); + if (bodyHead != null && bodyHead.getDiff().getMeta().equals(meta)) { + HistoryCommitMarker committed = commits.head(); + index.truncateAfter(committed == null ? null : committed.getIndexLocation()); + bodies.truncateAfter(committed == null ? null : committed.getHistoryLocation()); + return; + } + if (indexHead != null && indexHead.getRecord().getMeta().equals(meta)) { + throw new ArchivePersistenceException("Index/body archive heads differ during revert"); + } + throw new ArchivePersistenceException("Archive revert does not target the current head"); + } catch (IOException e) { + throw new ArchivePersistenceException("Failed to revert archive history", e); + } + } + + public synchronized HistoryCommitMarker committedHead() { + return commits.head(); + } + + @Override + public synchronized void awaitCommitted(long epoch) { + HistoryCommitMarker head = commits.head(); + if (head == null || head.getMeta().getEpoch() < epoch) { + throw new ArchivePersistenceException("Archive history has not committed epoch " + epoch); + } + } + + @Override + public void releaseThrough(long epoch) { + // The synchronous writer has no queue bookkeeping to release. + } + + public synchronized BlockReverseDiff readCommitted(long epoch) { + HistoryCommitMarker marker = commits.get(epoch); + if (marker == null) { + throw new IllegalArgumentException("History epoch is not committed: " + epoch); + } + try { + HistoryIndexRecord indexRecord = index.read(marker.getIndexLocation()); + validateMarkerReferences(marker, indexRecord); + BlockReverseDiff diff = bodies.read(marker.getHistoryLocation()); + if (!marker.getMeta().equals(diff.getMeta())) { + throw new ArchivePersistenceException("Marker does not match history body metadata"); + } + return diff; + } catch (IOException e) { + throw new ArchivePersistenceException("Failed to read committed archive history", e); + } + } + + private void validateNext(BlockSnapshotMeta meta) { + HistoryCommitMarker head = commits.head(); + if (head == null) { + return; + } + BlockSnapshotMeta previous = head.getMeta(); + if (meta.getEpoch() != previous.getEpoch() + 1 + || meta.getBlockNumber() != previous.getBlockNumber() + 1 + || !Arrays.equals(meta.getParentHash(), previous.getBlockHash())) { + throw new ArchivePersistenceException("Archive block metadata is not contiguous"); + } + } + + private void handleWriteFailure(BlockSnapshotMeta meta, Exception failure) { + if (commits.mayContain(meta.getEpoch())) { + throw new ArchivePersistenceException( + "History marker may be durable; refusing to roll back committed archive", failure); + } + try { + HistoryCommitMarker committed = commits.head(); + index.truncateAfter(committed == null ? null : committed.getIndexLocation()); + bodies.truncateAfter(committed == null ? null : committed.getHistoryLocation()); + } catch (IOException cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + throw new ArchivePersistenceException("Failed to persist archive history", failure); + } + + private void recoverPreparedSuffix() throws IOException { + HistorySegmentStore.ScanResult bodyScan = bodies.getScanResult(); + HistoryIndexStore.ScanResult indexScan = index.getScanResult(); + int committedCount = commits.getMarkers().size(); + if (bodyScan.getRecords().size() < committedCount + || indexScan.getRecords().size() < committedCount) { + throw new ArchivePersistenceException("Committed marker references missing body/index data"); + } + for (int i = 0; i < committedCount; i++) { + HistoryCommitMarker marker = commits.getMarkers().get(i); + ScannedRecord bodyRecord = bodyScan.getRecords().get(i); + ScannedIndexRecord indexRecord = indexScan.getRecords().get(i); + if (!marker.getMeta().equals(bodyRecord.getDiff().getMeta()) + || !marker.getMeta().equals(indexRecord.getRecord().getMeta())) { + throw new ArchivePersistenceException("Committed history metadata does not align"); + } + validateMarkerReferences(marker, indexRecord.getRecord()); + if (!same(marker.getHistoryLocation(), bodyRecord.getLocation()) + || !same(marker.getIndexLocation(), indexRecord.getLocation())) { + throw new ArchivePersistenceException("Commit marker location/digest mismatch"); + } + } + + if (bodyScan.getInvalidTail() != null) { + if (bodyScan.getRecords().size() < committedCount) { + throw new ArchivePersistenceException("Committed history body is corrupt"); + } + bodies.truncateInvalidTail(); + } + if (indexScan.getInvalidTailOffset() != null) { + if (indexScan.getRecords().size() < committedCount) { + throw new ArchivePersistenceException("Committed history index is corrupt"); + } + index.truncateInvalidTail(); + } + HistoryCommitMarker head = commits.head(); + index.truncateAfter(head == null ? null : head.getIndexLocation()); + bodies.truncateAfter(head == null ? null : head.getHistoryLocation()); + } + + private void validateMarkerReferences(HistoryCommitMarker marker, + HistoryIndexRecord indexRecord) { + if (!marker.getMeta().equals(indexRecord.getMeta()) + || !same(marker.getHistoryLocation(), indexRecord.getHistoryLocation())) { + throw new ArchivePersistenceException("Marker does not match authoritative index delta"); + } + } + + private static boolean same(HistoryLocation left, HistoryLocation right) { + return left.getSegmentId() == right.getSegmentId() + && left.getOffset() == right.getOffset() + && left.getRecordLength() == right.getRecordLength() + && left.getBodyChecksum() == right.getBodyChecksum() + && Arrays.equals(left.getBodyDigest(), right.getBodyDigest()); + } + + private static boolean same(HistoryIndexLocation left, HistoryIndexLocation right) { + return left.getOffset() == right.getOffset() + && left.getRecordLength() == right.getRecordLength() + && Arrays.equals(left.getDigest(), right.getDigest()); + } + + private static byte[] batchId() { + UUID uuid = UUID.randomUUID(); + return ByteBuffer.allocate(16).putLong(uuid.getMostSignificantBits()) + .putLong(uuid.getLeastSignificantBits()).array(); + } + + private static T last(List values) { + return values.isEmpty() ? null : values.get(values.size() - 1); + } + + @Override + public synchronized void close() throws IOException { + IOException failure = null; + try { + index.close(); + } catch (IOException e) { + failure = e; + } + try { + bodies.close(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + commits.close(); + if (failure != null) { + throw failure; + } + } + + enum Stage { + APPEND_BODY, + APPEND_INDEX, + SYNC_BODY, + SYNC_INDEX, + COMMIT_MARKER + } + + interface DurabilityHook { + void before(Stage stage, BlockSnapshotMeta meta) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchivePersistenceException.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchivePersistenceException.java new file mode 100644 index 00000000000..59d983d06fe --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchivePersistenceException.java @@ -0,0 +1,13 @@ +package org.tron.core.db2.archive; + +/** Fatal archive persistence or continuity failure. */ +public class ArchivePersistenceException extends RuntimeException { + + public ArchivePersistenceException(String message, Throwable cause) { + super(message, cause); + } + + public ArchivePersistenceException(String message) { + super(message); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java b/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java new file mode 100644 index 00000000000..ab7173800ce --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java @@ -0,0 +1,241 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +/** Bounded single-writer queue with explicit durable-epoch waiting and head-only reorg handling. */ +public final class AsyncArchiveHistorySink implements DurableBlockReverseDiffSink, Closeable { + + private final ArchiveHistoryWriter writer; + private final BlockingQueue queue; + private final Map submitted = new LinkedHashMap<>(); + private final Thread worker; + private volatile Throwable fatalFailure; + private volatile boolean closed; + private BlockSnapshotMeta acceptedHead; + + public AsyncArchiveHistorySink(ArchiveHistoryWriter writer, int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("capacity must be positive"); + } + this.writer = writer; + this.queue = new ArrayBlockingQueue<>(capacity); + HistoryCommitMarker committedHead = writer.committedHead(); + this.acceptedHead = committedHead == null ? null : committedHead.getMeta(); + this.worker = new Thread(this::run, "archive-history-writer"); + this.worker.setDaemon(true); + this.worker.start(); + } + + @Override + public void accept(BlockReverseDiff diff) { + WorkItem item = new WorkItem(diff); + synchronized (submitted) { + ensureOperational(); + if (acceptedHead != null) { + validateContinuity(acceptedHead, diff.getMeta()); + } + if (submitted.put(diff.getMeta().getEpoch(), item) != null) { + throw new ArchivePersistenceException("Duplicate submitted archive epoch"); + } + acceptedHead = diff.getMeta(); + } + try { + queue.put(item); + } catch (InterruptedException e) { + synchronized (submitted) { + submitted.remove(diff.getMeta().getEpoch()); + WorkItem previous = lastSubmitted(); + acceptedHead = previous == null ? committedMeta() : previous.diff.getMeta(); + } + Thread.currentThread().interrupt(); + throw new ArchivePersistenceException("Interrupted by archive queue backpressure", e); + } + } + + @Override + public void revert(BlockSnapshotMeta meta) { + WorkItem item; + synchronized (submitted) { + ensureOperational(); + item = lastSubmitted(); + if (item == null || !item.diff.getMeta().equals(meta)) { + throw new ArchivePersistenceException("Archive reorg must remove the submitted head"); + } + if (queue.remove(item)) { + submitted.remove(meta.getEpoch()); + WorkItem previous = lastSubmitted(); + acceptedHead = previous == null ? committedMeta() : previous.diff.getMeta(); + item.completion.cancel(false); + return; + } + } + await(item); + writer.revert(meta); + synchronized (submitted) { + submitted.remove(meta.getEpoch()); + WorkItem previous = lastSubmitted(); + acceptedHead = previous == null ? committedMeta() : previous.diff.getMeta(); + } + } + + @Override + public void awaitCommitted(long epoch) { + List required = new ArrayList<>(); + synchronized (submitted) { + ensureOperational(); + submitted.forEach((candidate, item) -> { + if (candidate <= epoch) { + required.add(item); + } + }); + } + required.forEach(this::await); + ensureOperational(); + } + + /** Releases completed queue bookkeeping after the corresponding disk epoch is durable. */ + @Override + public void releaseThrough(long epoch) { + synchronized (submitted) { + submitted.entrySet().removeIf(entry -> entry.getKey() <= epoch + && entry.getValue().completion.isDone() + && !entry.getValue().completion.isCompletedExceptionally()); + } + } + + public int queueSize() { + return queue.size(); + } + + private void run() { + while (true) { + WorkItem item; + try { + item = queue.take(); + } catch (InterruptedException e) { + if (closed || fatalFailure != null) { + return; + } + continue; + } + if (item.poison) { + return; + } + try { + writer.accept(item.diff); + item.completion.complete(null); + } catch (Throwable failure) { + fatalFailure = failure; + item.completion.completeExceptionally(failure); + failQueued(failure); + return; + } + } + } + + private void failQueued(Throwable failure) { + WorkItem item; + while ((item = queue.poll()) != null) { + if (!item.poison) { + item.completion.completeExceptionally(failure); + } + } + } + + private void await(WorkItem item) { + try { + item.completion.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ArchivePersistenceException("Interrupted while waiting for durable history", e); + } catch (ExecutionException e) { + throw new ArchivePersistenceException("Archive writer failed", e.getCause()); + } catch (java.util.concurrent.CancellationException e) { + throw new ArchivePersistenceException("Archive history item was reverted", e); + } + } + + private void validateContinuity(BlockSnapshotMeta previous, BlockSnapshotMeta current) { + if (current.getEpoch() != previous.getEpoch() + 1 + || current.getBlockNumber() != previous.getBlockNumber() + 1 + || !Arrays.equals(current.getParentHash(), previous.getBlockHash())) { + throw new ArchivePersistenceException("Submitted archive metadata is not contiguous"); + } + } + + private WorkItem lastSubmitted() { + WorkItem last = null; + for (WorkItem item : submitted.values()) { + last = item; + } + return last; + } + + private BlockSnapshotMeta committedMeta() { + HistoryCommitMarker marker = writer.committedHead(); + return marker == null ? null : marker.getMeta(); + } + + private void ensureOperational() { + if (fatalFailure != null) { + throw new ArchivePersistenceException("Archive writer is in a failed state", fatalFailure); + } + if (closed) { + throw new ArchivePersistenceException("Archive writer queue is closed"); + } + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + if (fatalFailure == null) { + try { + while (worker.isAlive() + && !queue.offer(WorkItem.poison(), 100, java.util.concurrent.TimeUnit.MILLISECONDS)) { + // Let the writer drain queued blocks before adding the terminal item. + } + worker.join(); + } catch (InterruptedException e) { + worker.interrupt(); + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while closing archive writer queue", e); + } + } else { + worker.interrupt(); + } + writer.close(); + } + + private static final class WorkItem { + private final BlockReverseDiff diff; + private final boolean poison; + private final CompletableFuture completion = new CompletableFuture<>(); + + private WorkItem(BlockReverseDiff diff) { + this.diff = diff; + this.poison = false; + } + + private WorkItem() { + this.diff = null; + this.poison = true; + } + + private static WorkItem poison() { + return new WorkItem(); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/BlockHistoryCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/BlockHistoryCodec.java new file mode 100644 index 00000000000..5dda90fc41c --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/BlockHistoryCodec.java @@ -0,0 +1,322 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +/** Deterministic, checksummed codec for one block history record. */ +public final class BlockHistoryCodec { + + public static final int MAGIC = 0x54415248; // TARH + public static final short VERSION = 1; + public static final int HEADER_LENGTH = 128; + public static final int CHECKSUM_LENGTH = Integer.BYTES; + public static final int DEFAULT_MAX_RECORD_LENGTH = 64 * 1024 * 1024; + + private static final short FLAG_DEFLATE = 1; + private static final int HASH_LENGTH = 32; + + private final int maxRecordLength; + + public BlockHistoryCodec() { + this(DEFAULT_MAX_RECORD_LENGTH); + } + + public BlockHistoryCodec(int maxRecordLength) { + if (maxRecordLength <= HEADER_LENGTH + CHECKSUM_LENGTH) { + throw new IllegalArgumentException("maxRecordLength is too small"); + } + this.maxRecordLength = maxRecordLength; + } + + public byte[] encode(BlockReverseDiff diff) { + try { + byte[] rawPayload = encodePayload(diff); + byte[] payload = deflate(rawPayload); + long recordLength = HEADER_LENGTH + (long) payload.length + CHECKSUM_LENGTH; + checkRecordLength(recordLength); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream((int) recordLength); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(FLAG_DEFLATE); + output.writeInt(HEADER_LENGTH); + output.writeLong(payload.length); + output.writeLong(diff.getMeta().getEpoch()); + output.writeLong(diff.getMeta().getBlockNumber()); + output.write(diff.getMeta().getBlockHash()); + output.write(diff.getMeta().getParentHash()); + output.writeLong(diff.getMeta().getTimestamp()); + output.writeInt(diff.getGroups().size()); + output.writeLong(entryCount(diff)); + output.writeLong(rawPayload.length); + output.write(payload); + output.flush(); + + byte[] withoutChecksum = bytes.toByteArray(); + output.writeInt(crc32c(withoutChecksum)); + output.flush(); + return bytes.toByteArray(); + } catch (IOException e) { + throw new IllegalStateException("Unexpected in-memory history encoding failure", e); + } + } + + public BlockReverseDiff decode(byte[] record) { + if (record == null || record.length < HEADER_LENGTH + CHECKSUM_LENGTH) { + throw new IllegalArgumentException("History record is truncated"); + } + checkRecordLength(record.length); + int expectedChecksum = ByteBuffer.wrap(record, record.length - CHECKSUM_LENGTH, + CHECKSUM_LENGTH).getInt(); + int actualChecksum = crc32c(Arrays.copyOf(record, record.length - CHECKSUM_LENGTH)); + if (expectedChecksum != actualChecksum) { + throw new IllegalArgumentException("History record checksum mismatch"); + } + + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(record)); + if (input.readInt() != MAGIC) { + throw new IllegalArgumentException("Invalid history record magic"); + } + short version = input.readShort(); + if (version != VERSION) { + throw new IllegalArgumentException("Unsupported history record version: " + version); + } + short flags = input.readShort(); + if (flags != FLAG_DEFLATE) { + throw new IllegalArgumentException("Unsupported history record flags: " + flags); + } + int headerLength = input.readInt(); + if (headerLength != HEADER_LENGTH) { + throw new IllegalArgumentException("Invalid history header length: " + headerLength); + } + long payloadLength = input.readLong(); + long expectedLength = HEADER_LENGTH + payloadLength + CHECKSUM_LENGTH; + if (payloadLength < 0 || expectedLength != record.length) { + throw new IllegalArgumentException("Invalid history payload length"); + } + long epoch = input.readLong(); + long blockNumber = input.readLong(); + byte[] blockHash = readExact(input, HASH_LENGTH); + byte[] parentHash = readExact(input, HASH_LENGTH); + long timestamp = input.readLong(); + int groupCount = input.readInt(); + long entryCount = input.readLong(); + long rawPayloadLength = input.readLong(); + if (groupCount < 0 || entryCount < 0 || rawPayloadLength < 0 + || rawPayloadLength > maxRecordLength) { + throw new IllegalArgumentException("Invalid history header counts"); + } + byte[] payload = readExact(input, (int) payloadLength); + byte[] rawPayload = inflate(payload, (int) rawPayloadLength); + List groups = decodePayload(rawPayload, groupCount, entryCount); + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, blockNumber, blockHash, parentHash, + timestamp); + return new BlockReverseDiff(meta, groups); + } catch (EOFException e) { + throw new IllegalArgumentException("History record is truncated", e); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid history record", e); + } + } + + /** Returns the complete record length described by a fixed header. */ + public int recordLength(byte[] header) { + if (header.length < HEADER_LENGTH) { + throw new IllegalArgumentException("History header is truncated"); + } + ByteBuffer buffer = ByteBuffer.wrap(header); + if (buffer.getInt() != MAGIC) { + throw new IllegalArgumentException("Invalid history record magic"); + } + if (buffer.getShort() != VERSION) { + throw new IllegalArgumentException("Unsupported history record version"); + } + if (buffer.getShort() != FLAG_DEFLATE) { + throw new IllegalArgumentException("Unsupported history record flags"); + } + if (buffer.getInt() != HEADER_LENGTH) { + throw new IllegalArgumentException("Invalid history header length"); + } + long payloadLength = buffer.getLong(); + long length = HEADER_LENGTH + payloadLength + CHECKSUM_LENGTH; + checkRecordLength(length); + return (int) length; + } + + private byte[] encodePayload(BlockReverseDiff diff) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + String previousDb = null; + for (DbGroup group : diff.getGroups()) { + if (previousDb != null && previousDb.compareTo(group.getDbName()) >= 0) { + throw new IllegalArgumentException("History database groups are not strictly sorted"); + } + previousDb = group.getDbName(); + byte[] dbName = group.getDbName().getBytes(StandardCharsets.UTF_8); + writeUnsignedVarInt(output, dbName.length); + output.write(dbName); + writeUnsignedVarInt(output, group.getEntries().size()); + byte[] previousKey = null; + for (Entry entry : group.getEntries()) { + byte[] key = entry.getKey(); + if (previousKey != null && BlockReverseDiff.compareUnsigned(previousKey, key) >= 0) { + throw new IllegalArgumentException("History entry keys are not strictly sorted"); + } + previousKey = key; + writeUnsignedVarInt(output, key.length); + output.write(key); + if (entry.getOldValue().isPresent()) { + byte[] value = entry.getOldValue().getValue(); + writeUnsignedVarInt(output, value.length + 1); + output.write(value); + } else { + writeUnsignedVarInt(output, 0); + } + } + } + output.flush(); + return bytes.toByteArray(); + } + + private List decodePayload(byte[] payload, int groupCount, long expectedEntryCount) + throws IOException { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload)); + List groups = new ArrayList<>(groupCount); + long actualEntryCount = 0; + String previousDb = null; + for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) { + String dbName = new String(readLengthPrefixed(input), StandardCharsets.UTF_8); + if (previousDb != null && previousDb.compareTo(dbName) >= 0) { + throw new IllegalArgumentException("Decoded database groups are not strictly sorted"); + } + previousDb = dbName; + int count = readUnsignedVarInt(input); + List entries = new ArrayList<>(count); + byte[] previousKey = null; + for (int entryIndex = 0; entryIndex < count; entryIndex++) { + byte[] key = readLengthPrefixed(input); + if (previousKey != null && BlockReverseDiff.compareUnsigned(previousKey, key) >= 0) { + throw new IllegalArgumentException("Decoded history keys are not strictly sorted"); + } + previousKey = key; + int encodedOldLength = readUnsignedVarInt(input); + OldValue oldValue = encodedOldLength == 0 ? OldValue.absent() + : OldValue.present(readExact(input, encodedOldLength - 1)); + entries.add(new Entry(key, oldValue)); + } + actualEntryCount += count; + groups.add(new DbGroup(dbName, entries)); + } + if (input.available() != 0 || actualEntryCount != expectedEntryCount) { + throw new IllegalArgumentException("History payload count or length mismatch"); + } + return groups; + } + + private byte[] readLengthPrefixed(DataInputStream input) throws IOException { + int length = readUnsignedVarInt(input); + if (length > maxRecordLength) { + throw new IllegalArgumentException("History field exceeds maximum record length"); + } + return readExact(input, length); + } + + private byte[] deflate(byte[] input) { + Deflater deflater = new Deflater(Deflater.BEST_SPEED, true); + deflater.setInput(input); + deflater.finish(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); + output.write(buffer, 0, count); + } + deflater.end(); + return output.toByteArray(); + } + + private byte[] inflate(byte[] input, int expectedLength) { + Inflater inflater = new Inflater(true); + inflater.setInput(input); + byte[] output = new byte[expectedLength]; + try { + int count = inflater.inflate(output); + if (count != expectedLength || !inflater.finished() || inflater.getRemaining() != 0) { + throw new IllegalArgumentException("History compressed payload length mismatch"); + } + return output; + } catch (DataFormatException e) { + throw new IllegalArgumentException("Invalid compressed history payload", e); + } finally { + inflater.end(); + } + } + + private static long entryCount(BlockReverseDiff diff) { + return diff.getGroups().stream().mapToLong(group -> group.getEntries().size()).sum(); + } + + private static byte[] readExact(DataInputStream input, int length) throws IOException { + if (length < 0) { + throw new IllegalArgumentException("Negative history field length"); + } + byte[] bytes = new byte[length]; + input.readFully(bytes); + return bytes; + } + + private static void writeUnsignedVarInt(DataOutputStream output, int value) throws IOException { + if (value < 0) { + throw new IllegalArgumentException("Negative unsigned varint"); + } + int remaining = value; + while ((remaining & 0xffffff80) != 0) { + output.writeByte((remaining & 0x7f) | 0x80); + remaining >>>= 7; + } + output.writeByte(remaining); + } + + private static int readUnsignedVarInt(DataInputStream input) throws IOException { + int value = 0; + for (int shift = 0; shift < 35; shift += 7) { + int next = input.readUnsignedByte(); + if (shift == 28 && (next & 0xf0) != 0) { + throw new IllegalArgumentException("Unsigned varint overflows int"); + } + value |= (next & 0x7f) << shift; + if ((next & 0x80) == 0) { + return value; + } + } + throw new IllegalArgumentException("Unsigned varint is too long"); + } + + private static int crc32c(byte[] bytes) { + return Hashing.crc32c().hashBytes(bytes).asInt(); + } + + private void checkRecordLength(long length) { + if (length < HEADER_LENGTH + CHECKSUM_LENGTH || length > maxRecordLength + || length > Integer.MAX_VALUE) { + throw new IllegalArgumentException("History record exceeds configured maximum: " + length); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java b/chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java new file mode 100644 index 00000000000..b6e2d4dc1b1 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java @@ -0,0 +1,9 @@ +package org.tron.core.db2.archive; + +/** Archive sink whose committed history can gate checkpoint and disk-layer advancement. */ +public interface DurableBlockReverseDiffSink extends BlockReverseDiffSink { + + void awaitCommitted(long epoch); + + void releaseThrough(long epoch); +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarker.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarker.java new file mode 100644 index 00000000000..768635d5b0f --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarker.java @@ -0,0 +1,58 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** Reader visibility boundary proving that one body and index delta are durable. */ +public final class HistoryCommitMarker { + + private final BlockSnapshotMeta meta; + private final long previousEpoch; + private final HistoryLocation historyLocation; + private final HistoryIndexLocation indexLocation; + private final byte[] batchId; + private final List databases; + + public HistoryCommitMarker(BlockSnapshotMeta meta, long previousEpoch, + HistoryLocation historyLocation, HistoryIndexLocation indexLocation, byte[] batchId, + List databases) { + this.meta = Objects.requireNonNull(meta, "meta"); + this.previousEpoch = previousEpoch; + this.historyLocation = Objects.requireNonNull(historyLocation, "historyLocation"); + this.indexLocation = Objects.requireNonNull(indexLocation, "indexLocation"); + if (batchId == null || batchId.length != 16) { + throw new IllegalArgumentException("batchId must be exactly 16 bytes"); + } + this.batchId = Arrays.copyOf(Objects.requireNonNull(batchId, "batchId"), batchId.length); + List sorted = new ArrayList<>(databases); + Collections.sort(sorted); + this.databases = Collections.unmodifiableList(sorted); + } + + public BlockSnapshotMeta getMeta() { + return meta; + } + + public long getPreviousEpoch() { + return previousEpoch; + } + + public HistoryLocation getHistoryLocation() { + return historyLocation; + } + + public HistoryIndexLocation getIndexLocation() { + return indexLocation; + } + + public byte[] getBatchId() { + return Arrays.copyOf(batchId, batchId.length); + } + + public List getDatabases() { + return databases; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarkerCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarkerCodec.java new file mode 100644 index 00000000000..b33652f5b8c --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarkerCodec.java @@ -0,0 +1,151 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Checksummed commit-marker codec. */ +public final class HistoryCommitMarkerCodec { + + private static final int MAGIC = 0x54415243; // TARC + private static final short VERSION = 1; + private static final int MAX_MARKER_LENGTH = 1024 * 1024; + + public byte[] encode(HistoryCommitMarker marker) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(0); // patched after payload is complete + output.writeLong(marker.getMeta().getEpoch()); + output.writeLong(marker.getPreviousEpoch()); + output.writeLong(marker.getMeta().getBlockNumber()); + output.write(marker.getMeta().getBlockHash()); + output.write(marker.getMeta().getParentHash()); + output.writeLong(marker.getMeta().getTimestamp()); + HistoryLocation body = marker.getHistoryLocation(); + output.writeInt(body.getSegmentId()); + output.writeLong(body.getOffset()); + output.writeInt(body.getRecordLength()); + output.writeInt(body.getBodyChecksum()); + output.write(body.getBodyDigest()); + HistoryIndexLocation index = marker.getIndexLocation(); + output.writeLong(index.getOffset()); + output.writeInt(index.getRecordLength()); + output.write(index.getDigest()); + writeBytes(output, marker.getBatchId()); + output.writeInt(marker.getDatabases().size()); + String previousDb = null; + for (String database : marker.getDatabases()) { + if (previousDb != null && previousDb.compareTo(database) >= 0) { + throw new IllegalArgumentException("Marker databases are not strictly sorted"); + } + previousDb = database; + writeBytes(output, database.getBytes(StandardCharsets.UTF_8)); + } + output.flush(); + byte[] withoutChecksum = bytes.toByteArray(); + int finalLength = withoutChecksum.length + Integer.BYTES; + if (finalLength > MAX_MARKER_LENGTH) { + throw new IllegalArgumentException("History commit marker is too large"); + } + ByteBuffer.wrap(withoutChecksum).putInt(8, finalLength); + output = new DataOutputStream(bytes); + bytes.reset(); + output.write(withoutChecksum); + output.writeInt(crc32c(withoutChecksum)); + output.flush(); + return bytes.toByteArray(); + } catch (IOException e) { + throw new IllegalStateException("Unexpected in-memory marker encoding failure", e); + } + } + + public HistoryCommitMarker decode(byte[] encoded) { + if (encoded == null || encoded.length < 12 + Integer.BYTES + || encoded.length > MAX_MARKER_LENGTH) { + throw new IllegalArgumentException("History commit marker length is invalid"); + } + int expectedChecksum = ByteBuffer.wrap(encoded, encoded.length - Integer.BYTES, + Integer.BYTES).getInt(); + if (expectedChecksum != crc32c(Arrays.copyOf(encoded, encoded.length - Integer.BYTES))) { + throw new IllegalArgumentException("History commit marker checksum mismatch"); + } + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != encoded.length) { + throw new IllegalArgumentException("Unsupported history commit marker header"); + } + long epoch = input.readLong(); + long previousEpoch = input.readLong(); + long blockNumber = input.readLong(); + byte[] blockHash = readExact(input, 32); + byte[] parentHash = readExact(input, 32); + long timestamp = input.readLong(); + HistoryLocation body = new HistoryLocation(input.readInt(), input.readLong(), + input.readInt(), input.readInt(), readExact(input, 32)); + HistoryIndexLocation index = new HistoryIndexLocation(input.readLong(), input.readInt(), + readExact(input, 32)); + byte[] batchId = readBytes(input); + int dbCount = input.readInt(); + if (dbCount < 0) { + throw new IllegalArgumentException("Negative marker database count"); + } + List databases = new ArrayList<>(dbCount); + String previousDb = null; + for (int i = 0; i < dbCount; i++) { + String dbName = new String(readBytes(input), StandardCharsets.UTF_8); + if (previousDb != null && previousDb.compareTo(dbName) >= 0) { + throw new IllegalArgumentException("Marker databases are not strictly sorted"); + } + previousDb = dbName; + databases.add(dbName); + } + if (input.available() != Integer.BYTES) { + throw new IllegalArgumentException("History commit marker payload mismatch"); + } + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, blockNumber, blockHash, parentHash, + timestamp); + return new HistoryCommitMarker(meta, previousEpoch, body, index, batchId, databases); + } catch (EOFException e) { + throw new IllegalArgumentException("History commit marker is truncated", e); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid history commit marker", e); + } + } + + private static void writeBytes(DataOutputStream output, byte[] value) throws IOException { + output.writeInt(value.length); + output.write(value); + } + + private static byte[] readBytes(DataInputStream input) throws IOException { + int length = input.readInt(); + if (length < 0 || length > MAX_MARKER_LENGTH) { + throw new IllegalArgumentException("Invalid marker field length"); + } + return readExact(input, length); + } + + private static byte[] readExact(DataInputStream input, int length) throws IOException { + byte[] value = new byte[length]; + input.readFully(value); + return value; + } + + private static int crc32c(byte[] value) { + return Hashing.crc32c().hashBytes(value).asInt(); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java new file mode 100644 index 00000000000..3092e43ae11 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java @@ -0,0 +1,180 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** Atomic, directory-synced history visibility markers. */ +public final class HistoryCommitStore implements Closeable { + + private static final String SUFFIX = ".commit"; + + private final Path directory; + private final HistoryCommitMarkerCodec codec; + private final DirectorySync directorySync; + private final List markers; + private final Map markersByEpoch = new HashMap<>(); + private HistoryCommitMarker uncertainMarker; + + public HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec) + throws IOException { + this(archiveDirectory, codec, HistorySegmentStore::syncDirectory); + } + + HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec, + DirectorySync directorySync) throws IOException { + this.directory = archiveDirectory.resolve("commits"); + this.codec = codec; + this.directorySync = directorySync; + Files.createDirectories(directory); + markers = scan(); + markers.forEach(marker -> markersByEpoch.put(marker.getMeta().getEpoch(), marker)); + } + + public synchronized void commit(HistoryCommitMarker marker) throws IOException { + if (uncertainMarker != null) { + throw new IllegalStateException("A previous commit marker has uncertain durability"); + } + validateNext(head(), marker); + byte[] encoded = codec.encode(marker); + Path target = markerPath(marker.getMeta().getEpoch()); + if (Files.exists(target)) { + byte[] existing = Files.readAllBytes(target); + if (Arrays.equals(existing, encoded)) { + return; + } + throw new IllegalStateException("Conflicting history commit marker for epoch " + + marker.getMeta().getEpoch()); + } + + Path temporary = directory.resolve(".tmp-" + marker.getMeta().getEpoch() + '-' + + UUID.randomUUID()); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + writeFully(channel, ByteBuffer.wrap(encoded)); + channel.force(true); + } + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.deleteIfExists(temporary); + throw new IOException("Atomic commit-marker move is not supported", e); + } + uncertainMarker = marker; + directorySync.sync(directory); + markers.add(marker); + markersByEpoch.put(marker.getMeta().getEpoch(), marker); + uncertainMarker = null; + } + + public synchronized void removeHead(BlockSnapshotMeta expected) throws IOException { + if (uncertainMarker != null) { + throw new IllegalStateException("Cannot revert a commit marker with uncertain durability"); + } + HistoryCommitMarker head = head(); + if (head == null || !head.getMeta().equals(expected)) { + throw new IllegalStateException("Only the committed history head can be reverted"); + } + Files.delete(markerPath(expected.getEpoch())); + directorySync.sync(directory); + markers.remove(markers.size() - 1); + markersByEpoch.remove(expected.getEpoch()); + } + + public synchronized HistoryCommitMarker head() { + return markers.isEmpty() ? null : markers.get(markers.size() - 1); + } + + public synchronized List getMarkers() { + return new ArrayList<>(markers); + } + + public synchronized HistoryCommitMarker get(long epoch) { + return markersByEpoch.get(epoch); + } + + public synchronized boolean mayContain(long epoch) { + return markersByEpoch.containsKey(epoch) + || (uncertainMarker != null && uncertainMarker.getMeta().getEpoch() == epoch); + } + + private List scan() throws IOException { + List paths = new ArrayList<>(); + try (DirectoryStream stream = Files.newDirectoryStream(directory, "*" + SUFFIX)) { + for (Path path : stream) { + paths.add(path); + } + } + paths.sort(Comparator.comparingLong(HistoryCommitStore::parseEpoch)); + List decoded = new ArrayList<>(); + HistoryCommitMarker previous = null; + for (Path path : paths) { + HistoryCommitMarker marker = codec.decode(Files.readAllBytes(path)); + if (parseEpoch(path) != marker.getMeta().getEpoch()) { + throw new IllegalStateException("Commit marker filename/epoch mismatch: " + path); + } + validateNext(previous, marker); + decoded.add(marker); + previous = marker; + } + return decoded; + } + + private void validateNext(HistoryCommitMarker previous, HistoryCommitMarker current) { + if (previous == null) { + if (current.getPreviousEpoch() >= current.getMeta().getEpoch()) { + throw new IllegalArgumentException("Invalid base commit marker previous epoch"); + } + return; + } + if (current.getMeta().getEpoch() != previous.getMeta().getEpoch() + 1 + || current.getPreviousEpoch() != previous.getMeta().getEpoch() + || current.getMeta().getBlockNumber() != previous.getMeta().getBlockNumber() + 1 + || !Arrays.equals(current.getMeta().getParentHash(), + previous.getMeta().getBlockHash())) { + throw new IllegalArgumentException("Non-contiguous history commit marker"); + } + } + + private Path markerPath(long epoch) { + return directory.resolve(String.format("%020d%s", epoch, SUFFIX)); + } + + private static long parseEpoch(Path path) { + String name = path.getFileName().toString(); + try { + return Long.parseLong(name.substring(0, name.length() - SUFFIX.length())); + } catch (RuntimeException e) { + throw new IllegalArgumentException("Invalid history commit marker name: " + name, e); + } + } + + private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } + + @Override + public void close() { + // Marker files do not keep open resources. + } + + interface DirectorySync { + void sync(Path directory) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexCodec.java new file mode 100644 index 00000000000..19ba9025660 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexCodec.java @@ -0,0 +1,243 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.tron.core.db2.archive.HistoryIndexRecord.KeyGroup; + +/** Deterministic codec for one authoritative state_history.idx delta. */ +public final class HistoryIndexCodec { + + public static final int MAGIC = 0x54415249; // TARI + public static final short VERSION = 1; + public static final int FIXED_HEADER_LENGTH = 160; + public static final int CHECKSUM_LENGTH = Integer.BYTES; + + private final int maxRecordLength; + + public HistoryIndexCodec() { + this(BlockHistoryCodec.DEFAULT_MAX_RECORD_LENGTH); + } + + public HistoryIndexCodec(int maxRecordLength) { + this.maxRecordLength = maxRecordLength; + } + + public byte[] encode(HistoryIndexRecord record) { + try { + byte[] payload = encodePayload(record); + int length = FIXED_HEADER_LENGTH + payload.length + CHECKSUM_LENGTH; + checkLength(length); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(length); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(length); + output.writeLong(record.getMeta().getEpoch()); + output.writeLong(record.getMeta().getBlockNumber()); + output.write(record.getMeta().getBlockHash()); + output.write(record.getMeta().getParentHash()); + output.writeLong(record.getMeta().getTimestamp()); + output.writeInt(record.getHistoryLocation().getSegmentId()); + output.writeLong(record.getHistoryLocation().getOffset()); + output.writeInt(record.getHistoryLocation().getRecordLength()); + output.writeInt(record.getHistoryLocation().getBodyChecksum()); + output.write(record.getHistoryLocation().getBodyDigest()); + output.writeInt(record.getGroups().size()); + output.writeInt(entryCount(record)); + output.write(payload); + output.flush(); + byte[] withoutChecksum = bytes.toByteArray(); + output.writeInt(crc32c(withoutChecksum)); + output.flush(); + return bytes.toByteArray(); + } catch (IOException e) { + throw new IllegalStateException("Unexpected in-memory index encoding failure", e); + } + } + + public HistoryIndexRecord decode(byte[] encoded) { + if (encoded == null || encoded.length < FIXED_HEADER_LENGTH + CHECKSUM_LENGTH) { + throw new IllegalArgumentException("History index record is truncated"); + } + checkLength(encoded.length); + int expectedChecksum = ByteBuffer.wrap(encoded, encoded.length - CHECKSUM_LENGTH, + CHECKSUM_LENGTH).getInt(); + if (expectedChecksum != crc32c(Arrays.copyOf(encoded, + encoded.length - CHECKSUM_LENGTH))) { + throw new IllegalArgumentException("History index checksum mismatch"); + } + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0) { + throw new IllegalArgumentException("Unsupported history index header"); + } + int recordLength = input.readInt(); + if (recordLength != encoded.length) { + throw new IllegalArgumentException("History index length mismatch"); + } + long epoch = input.readLong(); + long blockNumber = input.readLong(); + byte[] blockHash = readExact(input, 32); + byte[] parentHash = readExact(input, 32); + long timestamp = input.readLong(); + int segmentId = input.readInt(); + long offset = input.readLong(); + int bodyLength = input.readInt(); + int bodyChecksum = input.readInt(); + byte[] bodyDigest = readExact(input, 32); + int groupCount = input.readInt(); + int entryCount = input.readInt(); + if (groupCount < 0 || entryCount < 0) { + throw new IllegalArgumentException("Invalid history index counts"); + } + List groups = decodePayload(input, groupCount, entryCount); + if (input.available() != CHECKSUM_LENGTH) { + throw new IllegalArgumentException("History index payload length mismatch"); + } + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, blockNumber, blockHash, parentHash, + timestamp); + HistoryLocation history = new HistoryLocation(segmentId, offset, bodyLength, bodyChecksum, + bodyDigest); + return new HistoryIndexRecord(meta, history, groups); + } catch (EOFException e) { + throw new IllegalArgumentException("History index record is truncated", e); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid history index record", e); + } + } + + public int recordLength(byte[] prefix) { + if (prefix.length < 12) { + throw new IllegalArgumentException("History index prefix is truncated"); + } + ByteBuffer buffer = ByteBuffer.wrap(prefix); + if (buffer.getInt() != MAGIC || buffer.getShort() != VERSION || buffer.getShort() != 0) { + throw new IllegalArgumentException("Unsupported history index header"); + } + int length = buffer.getInt(); + checkLength(length); + return length; + } + + private byte[] encodePayload(HistoryIndexRecord record) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + String previousDb = null; + for (KeyGroup group : record.getGroups()) { + if (previousDb != null && previousDb.compareTo(group.getDbName()) >= 0) { + throw new IllegalArgumentException("Index database groups are not strictly sorted"); + } + previousDb = group.getDbName(); + byte[] dbName = group.getDbName().getBytes(StandardCharsets.UTF_8); + writeUnsignedVarInt(output, dbName.length); + output.write(dbName); + List keys = group.getKeys(); + writeUnsignedVarInt(output, keys.size()); + byte[] previousKey = null; + for (byte[] key : keys) { + if (previousKey != null && BlockReverseDiff.compareUnsigned(previousKey, key) >= 0) { + throw new IllegalArgumentException("Index keys are not strictly sorted"); + } + previousKey = key; + writeUnsignedVarInt(output, key.length); + output.write(key); + } + } + output.flush(); + return bytes.toByteArray(); + } + + private List decodePayload(DataInputStream input, int groupCount, int expectedEntries) + throws IOException { + List groups = new ArrayList<>(groupCount); + int actualEntries = 0; + String previousDb = null; + for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) { + String dbName = new String(readLengthPrefixed(input), StandardCharsets.UTF_8); + if (previousDb != null && previousDb.compareTo(dbName) >= 0) { + throw new IllegalArgumentException("Decoded index groups are not strictly sorted"); + } + previousDb = dbName; + int count = readUnsignedVarInt(input); + List keys = new ArrayList<>(count); + byte[] previousKey = null; + for (int keyIndex = 0; keyIndex < count; keyIndex++) { + byte[] key = readLengthPrefixed(input); + if (previousKey != null && BlockReverseDiff.compareUnsigned(previousKey, key) >= 0) { + throw new IllegalArgumentException("Decoded index keys are not strictly sorted"); + } + previousKey = key; + keys.add(key); + } + actualEntries += count; + groups.add(new KeyGroup(dbName, keys)); + } + if (actualEntries != expectedEntries) { + throw new IllegalArgumentException("History index entry count mismatch"); + } + return groups; + } + + private byte[] readLengthPrefixed(DataInputStream input) throws IOException { + int length = readUnsignedVarInt(input); + if (length > maxRecordLength) { + throw new IllegalArgumentException("History index field is too large"); + } + return readExact(input, length); + } + + private static byte[] readExact(DataInputStream input, int length) throws IOException { + byte[] bytes = new byte[length]; + input.readFully(bytes); + return bytes; + } + + private static int entryCount(HistoryIndexRecord record) { + return record.getGroups().stream().mapToInt(group -> group.getKeys().size()).sum(); + } + + private static void writeUnsignedVarInt(DataOutputStream output, int value) throws IOException { + int remaining = value; + while ((remaining & 0xffffff80) != 0) { + output.writeByte((remaining & 0x7f) | 0x80); + remaining >>>= 7; + } + output.writeByte(remaining); + } + + private static int readUnsignedVarInt(DataInputStream input) throws IOException { + int value = 0; + for (int shift = 0; shift < 35; shift += 7) { + int next = input.readUnsignedByte(); + if (shift == 28 && (next & 0xf0) != 0) { + throw new IllegalArgumentException("Unsigned varint overflows int"); + } + value |= (next & 0x7f) << shift; + if ((next & 0x80) == 0) { + return value; + } + } + throw new IllegalArgumentException("Unsigned varint is too long"); + } + + private static int crc32c(byte[] bytes) { + return Hashing.crc32c().hashBytes(bytes).asInt(); + } + + private void checkLength(int length) { + if (length < FIXED_HEADER_LENGTH + CHECKSUM_LENGTH || length > maxRecordLength) { + throw new IllegalArgumentException("History index record length is invalid: " + length); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexLocation.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexLocation.java new file mode 100644 index 00000000000..1205934368b --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexLocation.java @@ -0,0 +1,37 @@ +package org.tron.core.db2.archive; + +import java.util.Arrays; +import java.util.Objects; + +/** Byte range and digest of an authoritative index delta. */ +public final class HistoryIndexLocation { + + private final long offset; + private final int recordLength; + private final byte[] digest; + + public HistoryIndexLocation(long offset, int recordLength, byte[] digest) { + if (offset < 0 || recordLength <= 0) { + throw new IllegalArgumentException("Invalid history index location"); + } + this.offset = offset; + this.recordLength = recordLength; + this.digest = Arrays.copyOf(Objects.requireNonNull(digest, "digest"), digest.length); + } + + public long getOffset() { + return offset; + } + + public int getRecordLength() { + return recordLength; + } + + public byte[] getDigest() { + return Arrays.copyOf(digest, digest.length); + } + + public long endOffset() { + return offset + recordLength; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexRecord.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexRecord.java new file mode 100644 index 00000000000..173c2d6e4ff --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexRecord.java @@ -0,0 +1,66 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** Authoritative block-to-body location and per-key changed delta. */ +public final class HistoryIndexRecord { + + private final BlockSnapshotMeta meta; + private final HistoryLocation historyLocation; + private final List groups; + + public HistoryIndexRecord(BlockSnapshotMeta meta, HistoryLocation historyLocation, + List groups) { + this.meta = Objects.requireNonNull(meta, "meta"); + this.historyLocation = Objects.requireNonNull(historyLocation, "historyLocation"); + this.groups = Collections.unmodifiableList(new ArrayList<>(groups)); + } + + public static HistoryIndexRecord from(BlockReverseDiff diff, HistoryLocation location) { + List groups = new ArrayList<>(); + diff.getGroups().forEach(group -> { + List keys = new ArrayList<>(); + group.getEntries().forEach(entry -> keys.add(entry.getKey())); + groups.add(new KeyGroup(group.getDbName(), keys)); + }); + return new HistoryIndexRecord(diff.getMeta(), location, groups); + } + + public BlockSnapshotMeta getMeta() { + return meta; + } + + public HistoryLocation getHistoryLocation() { + return historyLocation; + } + + public List getGroups() { + return groups; + } + + public static final class KeyGroup { + private final String dbName; + private final List keys; + + public KeyGroup(String dbName, List keys) { + this.dbName = Objects.requireNonNull(dbName, "dbName"); + List copied = new ArrayList<>(); + keys.forEach(key -> copied.add(Arrays.copyOf(key, key.length))); + this.keys = Collections.unmodifiableList(copied); + } + + public String getDbName() { + return dbName; + } + + public List getKeys() { + List copied = new ArrayList<>(); + keys.forEach(key -> copied.add(Arrays.copyOf(key, key.length))); + return copied; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java new file mode 100644 index 00000000000..36e809b312e --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java @@ -0,0 +1,238 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** Append-only authoritative {@code state_history.idx}. */ +public final class HistoryIndexStore implements Closeable { + + private final Path archiveDirectory; + private final Path indexPath; + private final HistoryIndexCodec codec; + private final FileChannel channel; + private ScanResult scanResult; + + public HistoryIndexStore(Path archiveDirectory, HistoryIndexCodec codec) throws IOException { + this.archiveDirectory = archiveDirectory; + this.indexPath = archiveDirectory.resolve("state_history.idx"); + this.codec = codec; + Files.createDirectories(archiveDirectory); + boolean created = !Files.exists(indexPath); + channel = FileChannel.open(indexPath, StandardOpenOption.CREATE, StandardOpenOption.READ, + StandardOpenOption.WRITE); + if (created) { + HistorySegmentStore.syncDirectory(archiveDirectory); + } + scanResult = scan(); + channel.position(channel.size()); + } + + public synchronized HistoryIndexLocation append(HistoryIndexRecord record) throws IOException { + if (scanResult.getInvalidTailOffset() != null) { + throw new IllegalStateException("History index has an invalid tail"); + } + if (!scanResult.getRecords().isEmpty()) { + validateContinuity(scanResult.getRecords().get(scanResult.getRecords().size() - 1) + .getRecord().getMeta(), record.getMeta()); + } + byte[] encoded = codec.encode(record); + long offset = channel.size(); + channel.position(offset); + writeFully(channel, ByteBuffer.wrap(encoded)); + HistoryIndexLocation location = new HistoryIndexLocation(offset, encoded.length, + sha256(encoded)); + List records = new ArrayList<>(scanResult.getRecords()); + records.add(new ScannedIndexRecord(record, location)); + scanResult = new ScanResult(records, null, null); + return location; + } + + public synchronized void sync() throws IOException { + channel.force(true); + HistorySegmentStore.syncDirectory(archiveDirectory); + } + + public synchronized HistoryIndexRecord read(HistoryIndexLocation location) throws IOException { + if (location.endOffset() > channel.size()) { + throw new IllegalArgumentException("History index location is outside file bounds"); + } + ByteBuffer buffer = ByteBuffer.allocate(location.getRecordLength()); + channel.position(location.getOffset()); + readFully(channel, buffer); + byte[] encoded = buffer.array(); + if (!Arrays.equals(sha256(encoded), location.getDigest())) { + throw new IllegalArgumentException("History index location digest mismatch"); + } + return codec.decode(encoded); + } + + public synchronized ScanResult getScanResult() { + return scanResult; + } + + public synchronized ScanResult rescan() throws IOException { + scanResult = scan(); + return scanResult; + } + + public synchronized void truncateInvalidTail() throws IOException { + if (scanResult.getInvalidTailOffset() == null) { + return; + } + channel.truncate(scanResult.getInvalidTailOffset()); + channel.force(true); + scanResult = scan(); + channel.position(channel.size()); + } + + public synchronized void truncateAfter(HistoryIndexLocation last) throws IOException { + long length = last == null ? 0 : last.endOffset(); + channel.truncate(length); + channel.force(true); + scanResult = scan(); + channel.position(channel.size()); + } + + private ScanResult scan() throws IOException { + List records = new ArrayList<>(); + Long invalidOffset = null; + String invalidReason = null; + long offset = 0; + BlockSnapshotMeta previous = null; + while (offset < channel.size()) { + long remaining = channel.size() - offset; + if (remaining < 12) { + invalidOffset = offset; + invalidReason = "truncated index header"; + break; + } + ByteBuffer prefix = ByteBuffer.allocate(12); + channel.position(offset); + readFully(channel, prefix); + int recordLength; + try { + recordLength = codec.recordLength(prefix.array()); + } catch (IllegalArgumentException e) { + invalidOffset = offset; + invalidReason = e.getMessage(); + break; + } + if (recordLength > remaining) { + invalidOffset = offset; + invalidReason = "truncated index record"; + break; + } + ByteBuffer recordBuffer = ByteBuffer.allocate(recordLength); + channel.position(offset); + readFully(channel, recordBuffer); + byte[] encoded = recordBuffer.array(); + HistoryIndexRecord record; + try { + record = codec.decode(encoded); + validateContinuity(previous, record.getMeta()); + } catch (IllegalArgumentException e) { + invalidOffset = offset; + invalidReason = e.getMessage(); + break; + } + HistoryIndexLocation location = new HistoryIndexLocation(offset, recordLength, + sha256(encoded)); + records.add(new ScannedIndexRecord(record, location)); + previous = record.getMeta(); + offset += recordLength; + } + return new ScanResult(records, invalidOffset, invalidReason); + } + + private void validateContinuity(BlockSnapshotMeta previous, BlockSnapshotMeta current) { + if (previous == null) { + return; + } + if (current.getEpoch() != previous.getEpoch() + 1 + || current.getBlockNumber() != previous.getBlockNumber() + 1 + || !Arrays.equals(current.getParentHash(), previous.getBlockHash())) { + throw new IllegalArgumentException("non-contiguous history index metadata"); + } + } + + private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } + + private static void readFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + if (channel.read(buffer) < 0) { + throw new IOException("Unexpected end of history index"); + } + } + } + + private static byte[] sha256(byte[] bytes) { + try { + return MessageDigest.getInstance("SHA-256").digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is unavailable", e); + } + } + + @Override + public synchronized void close() throws IOException { + channel.close(); + } + + public static final class ScannedIndexRecord { + private final HistoryIndexRecord record; + private final HistoryIndexLocation location; + + private ScannedIndexRecord(HistoryIndexRecord record, HistoryIndexLocation location) { + this.record = record; + this.location = location; + } + + public HistoryIndexRecord getRecord() { + return record; + } + + public HistoryIndexLocation getLocation() { + return location; + } + } + + public static final class ScanResult { + private final List records; + private final Long invalidTailOffset; + private final String invalidReason; + + private ScanResult(List records, Long invalidTailOffset, + String invalidReason) { + this.records = Collections.unmodifiableList(new ArrayList<>(records)); + this.invalidTailOffset = invalidTailOffset; + this.invalidReason = invalidReason; + } + + public List getRecords() { + return records; + } + + public Long getInvalidTailOffset() { + return invalidTailOffset; + } + + public String getInvalidReason() { + return invalidReason; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryLocation.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryLocation.java new file mode 100644 index 00000000000..0904f6434e5 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryLocation.java @@ -0,0 +1,51 @@ +package org.tron.core.db2.archive; + +import java.util.Arrays; +import java.util.Objects; + +/** Durable location and identity of one encoded block history body. */ +public final class HistoryLocation { + + private final int segmentId; + private final long offset; + private final int recordLength; + private final int bodyChecksum; + private final byte[] bodyDigest; + + public HistoryLocation(int segmentId, long offset, int recordLength, int bodyChecksum, + byte[] bodyDigest) { + if (segmentId < 0 || offset < 0 || recordLength <= 0) { + throw new IllegalArgumentException("Invalid history location"); + } + this.segmentId = segmentId; + this.offset = offset; + this.recordLength = recordLength; + this.bodyChecksum = bodyChecksum; + this.bodyDigest = Arrays.copyOf(Objects.requireNonNull(bodyDigest, "bodyDigest"), + bodyDigest.length); + } + + public int getSegmentId() { + return segmentId; + } + + public long getOffset() { + return offset; + } + + public int getRecordLength() { + return recordLength; + } + + public int getBodyChecksum() { + return bodyChecksum; + } + + public byte[] getBodyDigest() { + return Arrays.copyOf(bodyDigest, bodyDigest.length); + } + + public long endOffset() { + return offset + recordLength; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java new file mode 100644 index 00000000000..131e05b2a76 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java @@ -0,0 +1,374 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** Append-only, rotating history body segments with strict tail scanning. */ +public final class HistorySegmentStore implements Closeable { + + private static final String PREFIX = "history."; + private static final String SUFFIX = ".dat"; + + private final Path directory; + private final BlockHistoryCodec codec; + private final long maxSegmentSize; + + private FileChannel appendChannel; + private int appendSegmentId; + private ScanResult scanResult; + + public HistorySegmentStore(Path archiveDirectory, BlockHistoryCodec codec, long maxSegmentSize) + throws IOException { + if (maxSegmentSize <= 0) { + throw new IllegalArgumentException("maxSegmentSize must be positive"); + } + this.directory = archiveDirectory.resolve("history"); + this.codec = codec; + this.maxSegmentSize = maxSegmentSize; + Files.createDirectories(directory); + scanResult = scan(); + openAppendChannel(); + } + + public synchronized HistoryLocation append(BlockReverseDiff diff) throws IOException { + if (scanResult.getInvalidTail() != null) { + throw new IllegalStateException("History has an invalid tail which must be truncated first"); + } + if (!scanResult.getRecords().isEmpty()) { + validateContinuity(scanResult.getRecords().get(scanResult.getRecords().size() - 1) + .getDiff().getMeta(), diff.getMeta()); + } + byte[] record = codec.encode(diff); + long offset = appendChannel.size(); + if (offset > 0 && offset + record.length > maxSegmentSize) { + rotate(); + offset = 0; + } + appendChannel.position(offset); + writeFully(appendChannel, ByteBuffer.wrap(record)); + HistoryLocation location = location(appendSegmentId, offset, record); + List records = new ArrayList<>(scanResult.getRecords()); + records.add(new ScannedRecord(diff, location)); + scanResult = new ScanResult(records, null); + return location; + } + + public synchronized void sync() throws IOException { + appendChannel.force(true); + syncDirectory(directory); + } + + public synchronized BlockReverseDiff read(HistoryLocation location) throws IOException { + return codec.decode(readRecord(location)); + } + + public synchronized byte[] readRecord(HistoryLocation location) throws IOException { + Path path = segmentPath(location.getSegmentId()); + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { + if (location.endOffset() > channel.size()) { + throw new IllegalArgumentException("History location is outside segment bounds"); + } + ByteBuffer record = ByteBuffer.allocate(location.getRecordLength()); + channel.position(location.getOffset()); + readFully(channel, record); + byte[] bytes = record.array(); + if (!Arrays.equals(sha256(bytes), location.getBodyDigest())) { + throw new IllegalArgumentException("History location digest mismatch"); + } + return bytes; + } + } + + public synchronized ScanResult getScanResult() { + return scanResult; + } + + public synchronized ScanResult rescan() throws IOException { + scanResult = scan(); + return scanResult; + } + + /** Removes only the suffix starting at the invalid record found by {@link #scan()}. */ + public synchronized void truncateInvalidTail() throws IOException { + InvalidTail tail = scanResult.getInvalidTail(); + if (tail == null) { + return; + } + closeAppendChannel(); + truncateFrom(tail.getSegmentId(), tail.getOffset()); + scanResult = scan(); + openAppendChannel(); + } + + /** Truncates all records after {@code last}; null means remove every body record. */ + public synchronized void truncateAfter(HistoryLocation last) throws IOException { + closeAppendChannel(); + if (last == null) { + for (Path segment : listSegments()) { + Files.deleteIfExists(segment); + } + } else { + truncateFrom(last.getSegmentId(), last.endOffset()); + } + syncDirectory(directory); + scanResult = scan(); + openAppendChannel(); + } + + private ScanResult scan() throws IOException { + List records = new ArrayList<>(); + InvalidTail invalidTail = null; + BlockSnapshotMeta previous = null; + List segments = listSegments(); + int expectedSegmentId = segments.isEmpty() ? 0 : parseSegmentId(segments.get(0)); + for (Path segment : segments) { + int segmentId = parseSegmentId(segment); + if (segmentId != expectedSegmentId) { + return new ScanResult(records, new InvalidTail(segmentId, 0, + "non-contiguous segment id")); + } + expectedSegmentId++; + try (FileChannel channel = FileChannel.open(segment, StandardOpenOption.READ)) { + long offset = 0; + while (offset < channel.size()) { + long remaining = channel.size() - offset; + if (remaining < BlockHistoryCodec.HEADER_LENGTH) { + invalidTail = new InvalidTail(segmentId, offset, "truncated record header"); + break; + } + ByteBuffer header = ByteBuffer.allocate(BlockHistoryCodec.HEADER_LENGTH); + channel.position(offset); + readFully(channel, header); + int recordLength; + try { + recordLength = codec.recordLength(header.array()); + } catch (IllegalArgumentException e) { + invalidTail = new InvalidTail(segmentId, offset, e.getMessage()); + break; + } + if (recordLength > remaining) { + invalidTail = new InvalidTail(segmentId, offset, "truncated record body"); + break; + } + ByteBuffer recordBuffer = ByteBuffer.allocate(recordLength); + channel.position(offset); + readFully(channel, recordBuffer); + byte[] record = recordBuffer.array(); + BlockReverseDiff diff; + try { + diff = codec.decode(record); + validateContinuity(previous, diff.getMeta()); + } catch (IllegalArgumentException e) { + invalidTail = new InvalidTail(segmentId, offset, e.getMessage()); + break; + } + HistoryLocation location = location(segmentId, offset, record); + records.add(new ScannedRecord(diff, location)); + previous = diff.getMeta(); + offset += recordLength; + } + } + if (invalidTail != null) { + break; + } + } + return new ScanResult(records, invalidTail); + } + + private void validateContinuity(BlockSnapshotMeta previous, BlockSnapshotMeta current) { + if (previous == null) { + return; + } + if (current.getEpoch() != previous.getEpoch() + 1 + || current.getBlockNumber() != previous.getBlockNumber() + 1 + || !Arrays.equals(current.getParentHash(), previous.getBlockHash())) { + throw new IllegalArgumentException("non-contiguous history block metadata"); + } + } + + private void openAppendChannel() throws IOException { + List segments = listSegments(); + appendSegmentId = segments.isEmpty() ? 0 : parseSegmentId(segments.get(segments.size() - 1)); + Path path = segmentPath(appendSegmentId); + boolean created = !Files.exists(path); + appendChannel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, + StandardOpenOption.WRITE); + appendChannel.position(appendChannel.size()); + if (created) { + syncDirectory(directory); + } + } + + private void rotate() throws IOException { + appendChannel.force(true); + closeAppendChannel(); + appendSegmentId++; + Path next = segmentPath(appendSegmentId); + appendChannel = FileChannel.open(next, StandardOpenOption.CREATE_NEW, StandardOpenOption.READ, + StandardOpenOption.WRITE); + syncDirectory(directory); + } + + private void truncateFrom(int segmentId, long offset) throws IOException { + for (Path segment : listSegments()) { + int candidate = parseSegmentId(segment); + if (candidate > segmentId) { + Files.deleteIfExists(segment); + } else if (candidate == segmentId) { + try (FileChannel channel = FileChannel.open(segment, StandardOpenOption.WRITE)) { + channel.truncate(offset); + channel.force(true); + } + } + } + syncDirectory(directory); + } + + private List listSegments() throws IOException { + List segments = new ArrayList<>(); + try (DirectoryStream stream = Files.newDirectoryStream(directory, + PREFIX + "*" + SUFFIX)) { + for (Path path : stream) { + parseSegmentId(path); + segments.add(path); + } + } + segments.sort(Comparator.comparingInt(HistorySegmentStore::parseSegmentId)); + return segments; + } + + private Path segmentPath(int segmentId) { + return directory.resolve(String.format("%s%06d%s", PREFIX, segmentId, SUFFIX)); + } + + private static int parseSegmentId(Path path) { + String name = path.getFileName().toString(); + if (!name.startsWith(PREFIX) || !name.endsWith(SUFFIX)) { + throw new IllegalArgumentException("Invalid history segment name: " + name); + } + String id = name.substring(PREFIX.length(), name.length() - SUFFIX.length()); + try { + return Integer.parseInt(id); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid history segment name: " + name, e); + } + } + + private static HistoryLocation location(int segmentId, long offset, byte[] record) { + int checksum = ByteBuffer.wrap(record, record.length - Integer.BYTES, Integer.BYTES).getInt(); + return new HistoryLocation(segmentId, offset, record.length, checksum, sha256(record)); + } + + private static byte[] sha256(byte[] bytes) { + try { + return MessageDigest.getInstance("SHA-256").digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is unavailable", e); + } + } + + private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } + + private static void readFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + if (channel.read(buffer) < 0) { + throw new IOException("Unexpected end of history segment"); + } + } + } + + static void syncDirectory(Path directory) throws IOException { + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + private void closeAppendChannel() throws IOException { + if (appendChannel != null) { + appendChannel.close(); + appendChannel = null; + } + } + + @Override + public synchronized void close() throws IOException { + closeAppendChannel(); + } + + public static final class ScannedRecord { + private final BlockReverseDiff diff; + private final HistoryLocation location; + + private ScannedRecord(BlockReverseDiff diff, HistoryLocation location) { + this.diff = diff; + this.location = location; + } + + public BlockReverseDiff getDiff() { + return diff; + } + + public HistoryLocation getLocation() { + return location; + } + } + + public static final class InvalidTail { + private final int segmentId; + private final long offset; + private final String reason; + + private InvalidTail(int segmentId, long offset, String reason) { + this.segmentId = segmentId; + this.offset = offset; + this.reason = reason; + } + + public int getSegmentId() { + return segmentId; + } + + public long getOffset() { + return offset; + } + + public String getReason() { + return reason; + } + } + + public static final class ScanResult { + private final List records; + private final InvalidTail invalidTail; + + private ScanResult(List records, InvalidTail invalidTail) { + this.records = Collections.unmodifiableList(new ArrayList<>(records)); + this.invalidTail = invalidTail; + } + + public List getRecords() { + return records; + } + + public InvalidTail getInvalidTail() { + return invalidTail; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index bb1547f165c..4ac6c3e7a17 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -7,7 +7,9 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import java.io.Closeable; import java.io.File; +import java.io.IOException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; @@ -39,6 +41,7 @@ import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockReverseDiffSink; import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.DurableBlockReverseDiffSink; import org.tron.core.db2.archive.OldValueCollector; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.IRevokingDB; @@ -377,6 +380,13 @@ public void shutdown() { ExecutorServiceManager.shutdownAndAwaitTermination(pruneCheckpointThread, pruneName); flushServices.forEach((key, value) -> ExecutorServiceManager.shutdownAndAwaitTermination(value, "flush-service-" + key)); + if (blockReverseDiffSink instanceof Closeable) { + try { + ((Closeable) blockReverseDiffSink).close(); + } catch (IOException e) { + logger.error("Failed to close archive history sink.", e); + } + } } public void updateSolidity(int hops) { @@ -455,6 +465,7 @@ public void flush() { if (shouldBeRefreshed()) { try { long start = System.currentTimeMillis(); + Long archiveEpoch = awaitArchiveHistoryForFlush(); if (!isV2Open()) { deleteCheckpoint(); } @@ -462,6 +473,9 @@ public void flush() { long checkPointEnd = System.currentTimeMillis(); refresh(); + if (archiveEpoch != null) { + ((DurableBlockReverseDiffSink) blockReverseDiffSink).releaseThrough(archiveEpoch); + } flushCount = 0; logger.info("Flush cost: {} ms, create checkpoint cost: {} ms, refresh cost: {} ms.", System.currentTimeMillis() - start, @@ -476,6 +490,44 @@ public void flush() { } } + private Long awaitArchiveHistoryForFlush() { + if (oldValueCollector == null) { + return null; + } + if (!(blockReverseDiffSink instanceof DurableBlockReverseDiffSink)) { + throw new TronDBException("Archive sink cannot prove durable history before checkpoint"); + } + Chainbase stateDatabase = dbs.stream() + .filter(db -> ArchiveStoreScope.isStateDatabase(db.getDbName())) + .findFirst() + .orElseThrow(() -> new TronDBException("Archive mode has no registered state database")); + Snapshot next = stateDatabase.getHead().getRoot(); + BlockSnapshotMeta last = null; + for (int i = 0; i < flushCount; i++) { + next = next.getNext(); + if (!Snapshot.isImpl(next)) { + throw new TronDBException("Archive flush range is missing a snapshot layer"); + } + BlockSnapshotMeta meta = ((SnapshotImpl) next).getBlockSnapshotMeta(); + if (meta == null) { + throw new TronDBException("Archive flush range contains a layer without block metadata"); + } + if (last != null && meta.getEpoch() != last.getEpoch() + 1) { + throw new TronDBException("Archive flush range is not epoch-contiguous"); + } + last = meta; + } + if (last == null) { + return null; + } + try { + ((DurableBlockReverseDiffSink) blockReverseDiffSink).awaitCommitted(last.getEpoch()); + } catch (RuntimeException e) { + throw new TronDBException("Archive history durability gate failed", e); + } + return last.getEpoch(); + } + public void createCheckpoint() { TronDatabase checkPointStore = null; try { diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java new file mode 100644 index 00000000000..6aad551db81 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java @@ -0,0 +1,167 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveHistoryWriter.Stage; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +public class ArchiveHistoryWriterTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void makesHistoryVisibleOnlyAfterOrderedDurabilityStages() throws Exception { + Path archive = temporaryFolder.newFolder("writer").toPath(); + List stages = new ArrayList<>(); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, + databases(), (stage, meta) -> stages.add(stage))) { + writer.accept(diff(1)); + assertEquals(Arrays.asList(Stage.APPEND_BODY, Stage.APPEND_INDEX, Stage.SYNC_BODY, + Stage.SYNC_INDEX, Stage.COMMIT_MARKER), stages); + assertEquals(1, writer.committedHead().getMeta().getEpoch()); + assertEquals(0, writer.committedHead().getPreviousEpoch()); + assertEquals(Arrays.asList("account", "properties"), + writer.committedHead().getDatabases()); + assertEquals(16, writer.committedHead().getBatchId().length); + assertEquals(diff(1).getMeta(), writer.readCommitted(1).getMeta()); + + writer.accept(diff(2)); + assertEquals(2, writer.committedHead().getMeta().getEpoch()); + writer.revert(diff(2).getMeta()); + assertEquals(1, writer.committedHead().getMeta().getEpoch()); + assertThrows(IllegalArgumentException.class, () -> writer.readCommitted(2)); + } + + try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter( + archive, 4096, databases())) { + assertEquals(1, reopened.committedHead().getMeta().getEpoch()); + assertEquals(diff(1).getMeta(), reopened.readCommitted(1).getMeta()); + } + } + + @Test + public void rollsBackPreparedSuffixAtEveryPreCommitFailure() throws Exception { + for (Stage failedStage : Stage.values()) { + Path archive = temporaryFolder.newFolder("failure-" + failedStage).toPath(); + ArchiveHistoryWriter.DurabilityHook hook = (stage, meta) -> { + if (stage == failedStage) { + throw new java.io.IOException("injected " + stage); + } + }; + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, + databases(), hook)) { + assertThrows(ArchivePersistenceException.class, () -> writer.accept(diff(1))); + assertNull(writer.committedHead()); + } + try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter( + archive, 4096, databases())) { + assertNull(reopened.committedHead()); + assertThrows(IllegalArgumentException.class, () -> reopened.readCommitted(1)); + } + } + } + + @Test + public void truncatesCrashLeftPreparedBodyAndIndexOnOpen() throws Exception { + Path archive = temporaryFolder.newFolder("prepared").toPath(); + BlockReverseDiff diff = diff(1); + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096); + HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec())) { + HistoryLocation body = bodies.append(diff); + index.append(HistoryIndexRecord.from(diff, body)); + bodies.sync(); + index.sync(); + } + + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + assertNull(writer.committedHead()); + writer.accept(diff); + assertEquals(1, writer.committedHead().getMeta().getEpoch()); + } + } + + @Test + public void rejectsNonContiguousCanonicalInput() throws Exception { + Path archive = temporaryFolder.newFolder("continuity").toPath(); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + writer.accept(diff(1)); + BlockReverseDiff gap = new BlockReverseDiff(new BlockSnapshotMeta( + 3, 3, hash(3), hash(2), 3L), Collections.emptyList()); + assertThrows(ArchivePersistenceException.class, () -> writer.accept(gap)); + assertEquals(1, writer.committedHead().getMeta().getEpoch()); + } + } + + @Test + public void ignoresUncommittedTemporaryMarkerFiles() throws Exception { + Path archive = temporaryFolder.newFolder("temporary-marker").toPath(); + Files.createDirectories(archive.resolve("commits")); + Files.write(archive.resolve("commits").resolve(".tmp-interrupted"), new byte[]{1, 2, 3}); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + assertNull(writer.committedHead()); + assertThrows(IllegalArgumentException.class, () -> writer.readCommitted(1)); + } + } + + @Test + public void markerDirectorySyncFailurePreservesBodyAndIndexAsUncertain() throws Exception { + Path archive = temporaryFolder.newFolder("uncertain-marker").toPath(); + HistoryCommitMarker marker = new HistoryCommitMarker(diff(1).getMeta(), 0, + new HistoryLocation(0, 0, 100, 17, new byte[32]), + new HistoryIndexLocation(0, 100, new byte[32]), new byte[16], + new ArrayList<>(databases())); + HistoryCommitStore.DirectorySync directorySync = directory -> { + throw new java.io.IOException("injected directory sync failure"); + }; + try (HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec(), directorySync)) { + assertThrows(java.io.IOException.class, () -> commits.commit(marker)); + assertNull(commits.head()); + assertTrue(commits.mayContain(1)); + try (java.util.stream.Stream paths = Files.list(archive.resolve("commits"))) { + assertEquals(1, paths + .filter(path -> path.getFileName().toString().endsWith(".commit")) + .count()); + } + assertThrows(IllegalStateException.class, + () -> commits.removeHead(marker.getMeta())); + } + } + + private static Set databases() { + return new java.util.LinkedHashSet<>(Arrays.asList("account", "properties")); + } + + private static BlockReverseDiff diff(int number) { + return new BlockReverseDiff(new BlockSnapshotMeta(number, number, hash(number), + hash(number - 1), number * 3_000L), Collections.singletonList( + new DbGroup("account", Collections.singletonList( + new Entry(bytes("key-" + number), OldValue.present(bytes("value-" + number))))))); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java b/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java new file mode 100644 index 00000000000..9b7a274a1cf --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java @@ -0,0 +1,139 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveHistoryWriter.Stage; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +public class AsyncArchiveHistorySinkTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void waitsForDurabilityAndRevertsCommittedHead() throws Exception { + Path archive = temporaryFolder.newFolder("async").toPath(); + ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases()); + try (AsyncArchiveHistorySink sink = new AsyncArchiveHistorySink(writer, 2)) { + sink.accept(diff(1)); + sink.accept(diff(2)); + sink.awaitCommitted(2); + assertEquals(2, writer.committedHead().getMeta().getEpoch()); + sink.revert(diff(2).getMeta()); + assertEquals(1, writer.committedHead().getMeta().getEpoch()); + sink.releaseThrough(1); + } + } + + @Test + public void removesQueuedForkHeadWithoutPublishingIt() throws Exception { + Path archive = temporaryFolder.newFolder("queued-reorg").toPath(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases(), + (stage, meta) -> { + if (stage == Stage.APPEND_BODY && meta.getEpoch() == 1) { + entered.countDown(); + await(release); + } + }); + try (AsyncArchiveHistorySink sink = new AsyncArchiveHistorySink(writer, 1)) { + sink.accept(diff(1)); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + sink.accept(diff(2)); + sink.revert(diff(2).getMeta()); + release.countDown(); + sink.awaitCommitted(1); + assertEquals(1, writer.committedHead().getMeta().getEpoch()); + } + } + + @Test + public void fullQueueAppliesBackpressureWithoutDroppingBlocks() throws Exception { + Path archive = temporaryFolder.newFolder("backpressure").toPath(); + CountDownLatch release = new CountDownLatch(1); + CountDownLatch entered = new CountDownLatch(1); + ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases(), + (stage, meta) -> { + if (stage == Stage.APPEND_BODY && meta.getEpoch() == 1) { + entered.countDown(); + await(release); + } + }); + try (AsyncArchiveHistorySink sink = new AsyncArchiveHistorySink(writer, 1)) { + sink.accept(diff(1)); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + sink.accept(diff(2)); + CountDownLatch thirdAccepted = new CountDownLatch(1); + Thread producer = new Thread(() -> { + sink.accept(diff(3)); + thirdAccepted.countDown(); + }); + producer.start(); + assertFalse(thirdAccepted.await(200, TimeUnit.MILLISECONDS)); + release.countDown(); + assertTrue(thirdAccepted.await(5, TimeUnit.SECONDS)); + sink.awaitCommitted(3); + producer.join(); + assertEquals(3, writer.committedHead().getMeta().getEpoch()); + } + } + + @Test + public void writerFailureBecomesFatalForQueue() throws Exception { + Path archive = temporaryFolder.newFolder("failure").toPath(); + ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases(), + (stage, meta) -> { + if (stage == Stage.APPEND_INDEX) { + throw new java.io.IOException("injected"); + } + }); + try (AsyncArchiveHistorySink sink = new AsyncArchiveHistorySink(writer, 1)) { + sink.accept(diff(1)); + assertThrows(ArchivePersistenceException.class, () -> sink.awaitCommitted(1)); + assertThrows(ArchivePersistenceException.class, () -> sink.accept(diff(2))); + } + } + + private static Set databases() { + return new java.util.LinkedHashSet<>(Arrays.asList("account", "properties")); + } + + private static BlockReverseDiff diff(int number) { + return new BlockReverseDiff(new BlockSnapshotMeta(number, number, hash(number), + hash(number - 1), number), Collections.singletonList(new DbGroup("account", + Collections.singletonList(new Entry(bytes("key-" + number), OldValue.absent()))))); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + private static void await(CountDownLatch latch) throws java.io.IOException { + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new java.io.IOException("interrupted", e); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/BlockHistoryCodecTest.java b/framework/src/test/java/org/tron/core/db2/archive/BlockHistoryCodecTest.java new file mode 100644 index 00000000000..79576384984 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/BlockHistoryCodecTest.java @@ -0,0 +1,93 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +public class BlockHistoryCodecTest { + + private final BlockHistoryCodec codec = new BlockHistoryCodec(); + + @Test + public void encodesDeterministicallyAndRoundTripsValueStates() { + BlockReverseDiff first = diff(Arrays.asList( + new DbGroup("votes", Arrays.asList( + new Entry(bytes("z"), OldValue.present(bytes("value"))), + new Entry(bytes("a"), OldValue.absent()))), + new DbGroup("account", Collections.singletonList( + new Entry(bytes("empty"), OldValue.present(new byte[0])))))); + BlockReverseDiff reordered = diff(Arrays.asList( + new DbGroup("account", Collections.singletonList( + new Entry(bytes("empty"), OldValue.present(new byte[0])))), + new DbGroup("votes", Arrays.asList( + new Entry(bytes("a"), OldValue.absent()), + new Entry(bytes("z"), OldValue.present(bytes("value"))))))); + + byte[] encoded = codec.encode(first); + assertArrayEquals(encoded, codec.encode(reordered)); + assertEquals(encoded.length, + codec.recordLength(Arrays.copyOf(encoded, BlockHistoryCodec.HEADER_LENGTH))); + + BlockReverseDiff decoded = codec.decode(encoded); + assertEquals(first.getMeta(), decoded.getMeta()); + assertEquals(2, decoded.getGroups().size()); + Entry empty = decoded.getGroups().get(0).getEntries().get(0); + assertTrue(empty.getOldValue().isPresent()); + assertEquals(0, empty.getOldValue().getValue().length); + Entry absent = decoded.getGroups().get(1).getEntries().get(0); + assertFalse(absent.getOldValue().isPresent()); + assertArrayEquals(bytes("value"), + decoded.getGroups().get(1).getEntries().get(1).getOldValue().getValue()); + } + + @Test + public void rejectsCorruptionTruncationAndOversizedRecords() { + byte[] encoded = codec.encode(diff(Collections.singletonList( + new DbGroup("account", Collections.singletonList( + new Entry(bytes("key"), OldValue.present(bytes("value")))))))); + byte[] corrupted = Arrays.copyOf(encoded, encoded.length); + corrupted[BlockHistoryCodec.HEADER_LENGTH] ^= 1; + assertThrows(IllegalArgumentException.class, () -> codec.decode(corrupted)); + assertThrows(IllegalArgumentException.class, + () -> codec.decode(Arrays.copyOf(encoded, encoded.length - 1))); + + BlockHistoryCodec smallCodec = new BlockHistoryCodec(180); + byte[] incompressible = new byte[512]; + new java.util.Random(17L).nextBytes(incompressible); + BlockReverseDiff oversized = diff(Collections.singletonList( + new DbGroup("account", Collections.singletonList( + new Entry(bytes("key"), OldValue.present(incompressible)))))); + assertThrows(IllegalArgumentException.class, () -> smallCodec.encode(oversized)); + } + + @Test + public void preservesNoopBlockMetadata() { + BlockReverseDiff empty = diff(Collections.emptyList()); + BlockReverseDiff decoded = codec.decode(codec.encode(empty)); + assertEquals(empty.getMeta(), decoded.getMeta()); + assertTrue(decoded.getGroups().isEmpty()); + } + + private static BlockReverseDiff diff(java.util.List groups) { + return new BlockReverseDiff(new BlockSnapshotMeta( + 12, 12, hash(12), hash(11), 36_000L), groups); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/HistoryIndexStoreTest.java b/framework/src/test/java/org/tron/core/db2/archive/HistoryIndexStoreTest.java new file mode 100644 index 00000000000..2d0c3ecb7ad --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/HistoryIndexStoreTest.java @@ -0,0 +1,117 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +public class HistoryIndexStoreTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void locatesAndBackReferencesHistoryBody() throws Exception { + Path archive = temporaryFolder.newFolder("index").toPath(); + BlockReverseDiff diff = diff(1); + HistoryLocation bodyLocation; + HistoryIndexLocation indexLocation; + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096); + HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec())) { + bodyLocation = bodies.append(diff); + indexLocation = index.append(HistoryIndexRecord.from(diff, bodyLocation)); + bodies.sync(); + index.sync(); + + HistoryIndexRecord decodedIndex = index.read(indexLocation); + assertEquals(diff.getMeta(), decodedIndex.getMeta()); + assertEquals(bodyLocation.getSegmentId(), + decodedIndex.getHistoryLocation().getSegmentId()); + assertArrayEquals(bodyLocation.getBodyDigest(), + decodedIndex.getHistoryLocation().getBodyDigest()); + assertEquals(diff.getMeta(), bodies.read(decodedIndex.getHistoryLocation()).getMeta()); + assertArrayEquals(bytes("key-1"), decodedIndex.getGroups().get(0).getKeys().get(0)); + } + + try (HistoryIndexStore reopened = new HistoryIndexStore(archive, new HistoryIndexCodec())) { + assertNull(reopened.getScanResult().getInvalidTailOffset()); + assertEquals(1, reopened.getScanResult().getRecords().size()); + assertArrayEquals(indexLocation.getDigest(), reopened.getScanResult().getRecords().get(0) + .getLocation().getDigest()); + } + } + + @Test + public void detectsAndTruncatesCorruptIndexTail() throws Exception { + Path archive = temporaryFolder.newFolder("corrupt-index").toPath(); + BlockReverseDiff diff = diff(1); + HistoryIndexLocation location; + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096); + HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec())) { + HistoryLocation body = bodies.append(diff); + location = index.append(HistoryIndexRecord.from(diff, body)); + bodies.sync(); + index.sync(); + } + + Path indexPath = archive.resolve("state_history.idx"); + try (FileChannel channel = FileChannel.open(indexPath, StandardOpenOption.WRITE)) { + channel.position(location.getOffset() + HistoryIndexCodec.FIXED_HEADER_LENGTH); + channel.write(ByteBuffer.wrap(new byte[]{0x7f})); + channel.force(true); + } + try (HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec())) { + assertNotNull(index.getScanResult().getInvalidTailOffset()); + assertEquals(Long.valueOf(0), index.getScanResult().getInvalidTailOffset()); + assertThrows(IllegalStateException.class, + () -> index.append(HistoryIndexRecord.from(diff, + new HistoryLocation(0, 0, 1, 0, new byte[32])))); + index.truncateInvalidTail(); + assertNull(index.getScanResult().getInvalidTailOffset()); + assertEquals(0, index.getScanResult().getRecords().size()); + } + } + + @Test + public void indexEncodingIsDeterministic() { + BlockReverseDiff diff = diff(1); + HistoryLocation location = new HistoryLocation(2, 7, 99, 17, new byte[32]); + HistoryIndexCodec codec = new HistoryIndexCodec(); + byte[] first = codec.encode(HistoryIndexRecord.from(diff, location)); + byte[] second = codec.encode(HistoryIndexRecord.from(diff, location)); + assertArrayEquals(first, second); + assertEquals(first.length, codec.recordLength(Arrays.copyOf(first, 12))); + } + + private static BlockReverseDiff diff(int number) { + return new BlockReverseDiff(new BlockSnapshotMeta(number, number, hash(number), + hash(number - 1), number * 3_000L), Collections.singletonList( + new DbGroup("account", Collections.singletonList( + new Entry(bytes("key-" + number), OldValue.present(bytes("value-" + number))))))); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/HistorySegmentStoreTest.java b/framework/src/test/java/org/tron/core/db2/archive/HistorySegmentStoreTest.java new file mode 100644 index 00000000000..3d893b40689 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/HistorySegmentStoreTest.java @@ -0,0 +1,107 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Collections; +import java.util.Random; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +public class HistorySegmentStoreTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void appendsRotatesScansAndReadsRecords() throws Exception { + Path archive = temporaryFolder.newFolder("archive").toPath(); + BlockHistoryCodec codec = new BlockHistoryCodec(); + HistoryLocation first; + HistoryLocation second; + try (HistorySegmentStore store = new HistorySegmentStore(archive, codec, 250)) { + first = store.append(diff(1, randomBytes(100, 1))); + second = store.append(diff(2, randomBytes(100, 2))); + store.sync(); + assertEquals(0, first.getSegmentId()); + assertEquals(1, second.getSegmentId()); + assertEquals(2, store.getScanResult().getRecords().size()); + assertEquals(2, store.read(second).getMeta().getBlockNumber()); + } + + try (HistorySegmentStore reopened = new HistorySegmentStore(archive, codec, 250)) { + assertNull(reopened.getScanResult().getInvalidTail()); + assertEquals(2, reopened.getScanResult().getRecords().size()); + assertArrayEquals(second.getBodyDigest(), reopened.getScanResult().getRecords().get(1) + .getLocation().getBodyDigest()); + } + } + + @Test + public void findsAndTruncatesPartialTailBeforeAppending() throws Exception { + Path archive = temporaryFolder.newFolder("partial").toPath(); + BlockHistoryCodec codec = new BlockHistoryCodec(); + HistoryLocation first; + try (HistorySegmentStore store = new HistorySegmentStore(archive, codec, 4096)) { + first = store.append(diff(1, bytes("one"))); + store.sync(); + } + + Path segment = archive.resolve("history").resolve("history.000000.dat"); + Files.write(segment, new byte[]{0x54, 0x41, 0x52}, StandardOpenOption.APPEND); + try (HistorySegmentStore store = new HistorySegmentStore(archive, codec, 4096)) { + assertNotNull(store.getScanResult().getInvalidTail()); + assertEquals(first.endOffset(), store.getScanResult().getInvalidTail().getOffset()); + assertThrows(IllegalStateException.class, () -> store.append(diff(2, bytes("two")))); + store.truncateInvalidTail(); + assertNull(store.getScanResult().getInvalidTail()); + store.append(diff(2, bytes("two"))); + store.sync(); + assertEquals(2, store.getScanResult().getRecords().size()); + } + } + + @Test + public void rejectsHashOrEpochGaps() throws Exception { + Path archive = temporaryFolder.newFolder("gap").toPath(); + try (HistorySegmentStore store = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096)) { + store.append(diff(1, bytes("one"))); + BlockReverseDiff wrongParent = new BlockReverseDiff( + new BlockSnapshotMeta(2, 2, hash(2), hash(99), 2L), Collections.emptyList()); + assertThrows(IllegalArgumentException.class, () -> store.append(wrongParent)); + } + } + + private static BlockReverseDiff diff(int number, byte[] value) { + return new BlockReverseDiff(new BlockSnapshotMeta( + number, number, hash(number), hash(number - 1), number), + Collections.singletonList(new DbGroup("account", Collections.singletonList( + new Entry(bytes("key-" + number), OldValue.present(value)))))); + } + + private static byte[] randomBytes(int length, int seed) { + byte[] bytes = new byte[length]; + new Random(seed).nextBytes(bytes); + return bytes; + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index 8b848cbd00a..26a6b549705 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -6,7 +6,10 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.primitives.Bytes; @@ -34,7 +37,9 @@ import org.tron.core.db2.core.SnapshotImpl; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.db2.core.SnapshotRoot; +import org.tron.core.exception.TronError; import org.tron.core.store.AccountAssetStore; +import org.tron.core.store.CheckTmpStore; import org.tron.protos.Protocol.Account; public class SnapshotOldValueCollectorTest extends BaseMethodTest { @@ -304,6 +309,34 @@ public void projectsOldPhysicalAssetValueForOptimizedAccount() { manager.shutdown(); } + @Test + public void archiveDurabilityFailurePreventsCheckpointAndRefresh() throws Exception { + MemoryDb memoryDb = new MemoryDb("abi"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.setUnChecked(false); + CheckTmpStore checkpoint = mock(CheckTmpStore.class); + manager.setCheckTmpStore(checkpoint); + DurableBlockReverseDiffSink sink = mock(DurableBlockReverseDiffSink.class); + manager.installArchiveCollector(new SnapshotOldValueCollector(), sink); + try (ISession block = manager.buildSession()) { + database.put(bytes("key"), bytes("value")); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + java.lang.reflect.Field flushCount = SnapshotManager.class.getDeclaredField("flushCount"); + flushCount.setAccessible(true); + flushCount.setInt(manager, 1); + doThrow(new ArchivePersistenceException("injected")) + .when(sink).awaitCommitted(1L); + + assertThrows(TronError.class, manager::flush); + verify(sink).awaitCommitted(1L); + verify(checkpoint, never()).updateByBatch(any(Map.class)); + manager.shutdown(); + } + private static Entry find(DbGroup group, byte[] key) { return group.getEntries().stream() .filter(entry -> Arrays.equals(entry.getKey(), key)) From 5a83f787eb2cf85557ddcea1bb1b4e37405460e0 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 18 Aug 2026 18:04:31 +0800 Subject: [PATCH 003/161] feat(chainbase): add scalable state archive Persist reverse state history with bounded append-only body, index, and commit structures. Wire an opt-in archive writer into snapshot flush and add restart-safe account change indexing. Expose an experimental historical account balance endpoint for trace comparison. --- .../org/tron/core/vm/program/Storage.java | 30 +- .../core/db2/archive/AccountChangeIndex.java | 172 +++++ .../core/db2/archive/ArchiveBaseManifest.java | 159 +++++ .../db2/archive/ArchiveHistoryWriter.java | 200 ++++-- .../ArchiveQueryLimitExceededException.java | 9 + .../core/db2/archive/ArchiveReadContext.java | 191 ++++++ .../core/db2/archive/ArchiveReadSnapshot.java | 194 ++++++ .../core/db2/archive/ArchiveStoreScope.java | 2 +- .../db2/archive/AsyncArchiveHistorySink.java | 61 +- .../db2/archive/BlockReverseDiffSink.java | 5 + .../db2/archive/CommittedHistoryReader.java | 217 +++++++ .../HistoricalAccountBalanceReader.java | 89 +++ .../db2/archive/HistoricalRangeOverlay.java | 179 ++++++ .../db2/archive/HistoryCommitMarkerCodec.java | 17 + .../core/db2/archive/HistoryCommitStore.java | 287 ++++++--- .../core/db2/archive/HistoryIndexStore.java | 35 +- .../core/db2/archive/HistorySegmentStore.java | 37 +- .../db2/archive/ServingKeyIndexCatalog.java | 33 + .../archive/ServingKeyIndexGeneration.java | 599 ++++++++++++++++++ .../org/tron/core/db2/core/SnapshotImpl.java | 17 +- .../tron/core/db2/core/SnapshotManager.java | 57 +- .../tron/core/store/StorageRowKeyCodec.java | 55 ++ .../org/tron/core/config/args/Storage.java | 16 + .../tron/core/config/args/StorageConfig.java | 30 + common/src/main/resources/reference.conf | 6 + .../core/config/args/StorageConfigTest.java | 22 + .../src/main/java/org/tron/core/Wallet.java | 24 + .../java/org/tron/core/config/args/Args.java | 5 +- .../main/java/org/tron/core/db/Manager.java | 76 +++ .../services/http/FullNodeHttpApiService.java | 4 + .../GetAccountBalanceFromArchiveServlet.java | 32 + framework/src/main/resources/config.conf | 6 + .../db2/archive/ArchiveHistoryWriterTest.java | 93 ++- .../db2/archive/ArchiveReadSnapshotTest.java | 434 +++++++++++++ .../archive/AsyncArchiveHistorySinkTest.java | 19 + .../HistoricalAccountBalanceReaderTest.java | 150 +++++ .../archive/HistoricalRangeOverlayTest.java | 151 +++++ .../db2/archive/HistoryIndexStoreTest.java | 6 +- .../db2/archive/HistorySegmentStoreTest.java | 8 +- .../ServingKeyIndexGenerationTest.java | 229 +++++++ .../SnapshotOldValueCollectorTest.java | 201 +++++- .../db2/archive/StorageRowKeyCodecTest.java | 74 +++ 42 files changed, 3943 insertions(+), 288 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveQueryLimitExceededException.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/CommittedHistoryReader.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountBalanceReader.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexCatalog.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java create mode 100644 chainbase/src/main/java/org/tron/core/store/StorageRowKeyCodec.java create mode 100644 framework/src/main/java/org/tron/core/services/http/GetAccountBalanceFromArchiveServlet.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountBalanceReaderTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/HistoricalRangeOverlayTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ServingKeyIndexGenerationTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StorageRowKeyCodecTest.java diff --git a/actuator/src/main/java/org/tron/core/vm/program/Storage.java b/actuator/src/main/java/org/tron/core/vm/program/Storage.java index 572af048081..666b4611d98 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/Storage.java +++ b/actuator/src/main/java/org/tron/core/vm/program/Storage.java @@ -1,20 +1,16 @@ package org.tron.core.vm.program; -import static java.lang.System.arraycopy; - import java.util.HashMap; import java.util.Map; import lombok.Getter; import lombok.Setter; -import org.tron.common.crypto.Hash; import org.tron.common.runtime.vm.DataWord; -import org.tron.common.utils.ByteUtil; import org.tron.core.capsule.StorageRowCapsule; +import org.tron.core.store.StorageRowKeyCodec; import org.tron.core.store.StorageRowStore; public class Storage { - private static final int PREFIX_BYTES = 16; @Getter private final Map rowCache = new HashMap<>(); @Getter @@ -27,7 +23,7 @@ public class Storage { private int contractVersion; public Storage(byte[] address, StorageRowStore store) { - addrHash = addrHash(address); + addrHash = StorageRowKeyCodec.addressHash(address, null); this.address = address; this.store = store; } @@ -44,30 +40,12 @@ public Storage(Storage storage) { } private byte[] compose(byte[] key, byte[] addrHash) { - if (contractVersion == 1) { - key = Hash.sha3(key); - } - byte[] result = new byte[key.length]; - arraycopy(addrHash, 0, result, 0, PREFIX_BYTES); - arraycopy(key, PREFIX_BYTES, result, PREFIX_BYTES, PREFIX_BYTES); - return result; - } - - // 32 bytes - private static byte[] addrHash(byte[] address) { - return Hash.sha3(address); - } - - private static byte[] addrHash(byte[] address, byte[] trxHash) { - if (ByteUtil.isNullOrZeroArray(trxHash)) { - return Hash.sha3(address); - } - return Hash.sha3(ByteUtil.merge(address, trxHash)); + return StorageRowKeyCodec.physicalKeyFromAddressHash(addrHash, key, contractVersion); } public void generateAddrHash(byte[] trxId) { // update addreHash for create2 - addrHash = addrHash(address, trxId); + addrHash = StorageRowKeyCodec.addressHash(address, trxId); } public DataWord getValue(DataWord key) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java new file mode 100644 index 00000000000..2b6b70e761c --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java @@ -0,0 +1,172 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.OptionalLong; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +/** Persistent derived exact-key change index for the narrow historical account query. */ +final class AccountChangeIndex implements Closeable { + + static { + RocksDB.loadLibrary(); + } + + private static final byte DATA_PREFIX = 1; + private static final byte[] HEAD_KEY = new byte[]{0, 'h', 'e', 'a', 'd'}; + private static final int ADDRESS_LENGTH = HistoricalAccountBalanceReader.ADDRESS_LENGTH; + private static final int DATA_KEY_LENGTH = 1 + ADDRESS_LENGTH + Long.BYTES; + + private final Options options = new Options().setCreateIfMissing(true); + private final RocksDB database; + private final WriteOptions syncWrites = new WriteOptions().setSync(true); + + AccountChangeIndex(Path directory) throws IOException { + try { + database = RocksDB.open(options, directory.toString()); + } catch (RocksDBException failure) { + throw new IOException("Failed to open account change index", failure); + } + } + + synchronized void apply(List diffs) throws IOException { + if (diffs.isEmpty()) { + return; + } + long current = getIndexedThrough(); + BlockSnapshotMeta previous = null; + try (WriteBatch batch = new WriteBatch()) { + for (BlockReverseDiff diff : diffs) { + BlockSnapshotMeta meta = diff.getMeta(); + if (current >= 0 && previous == null && meta.getEpoch() != current + 1) { + throw new ArchivePersistenceException("Account index catch-up is not contiguous"); + } + if (previous != null && meta.getEpoch() != previous.getEpoch() + 1) { + throw new ArchivePersistenceException("Account index batch is not contiguous"); + } + for (DbGroup group : diff.getGroups()) { + if (!HistoricalAccountBalanceReader.ACCOUNT_DATABASE.equals(group.getDbName())) { + continue; + } + for (Entry entry : group.getEntries()) { + byte[] address = entry.getKey(); + if (address.length != ADDRESS_LENGTH) { + continue; + } + batch.put(dataKey(address, meta.getEpoch()), new byte[]{1}); + } + } + previous = meta; + } + batch.put(HEAD_KEY, encodeHead(previous)); + database.write(syncWrites, batch); + } catch (RocksDBException failure) { + throw new IOException("Failed to update account change index", failure); + } + } + + synchronized void revert(BlockReverseDiff diff, BlockSnapshotMeta newHead) throws IOException { + if (getIndexedThrough() != diff.getMeta().getEpoch()) { + throw new ArchivePersistenceException("Account index revert does not target its head"); + } + try (WriteBatch batch = new WriteBatch()) { + for (DbGroup group : diff.getGroups()) { + if (HistoricalAccountBalanceReader.ACCOUNT_DATABASE.equals(group.getDbName())) { + for (Entry entry : group.getEntries()) { + if (entry.getKey().length == ADDRESS_LENGTH) { + batch.delete(dataKey(entry.getKey(), diff.getMeta().getEpoch())); + } + } + } + } + if (newHead == null) { + batch.delete(HEAD_KEY); + } else { + batch.put(HEAD_KEY, encodeHead(newHead)); + } + database.write(syncWrites, batch); + } catch (RocksDBException failure) { + throw new IOException("Failed to revert account change index", failure); + } + } + + synchronized OptionalLong firstChangeAfter(byte[] address, long target, long upperBound) + throws IOException { + if (address == null || address.length != ADDRESS_LENGTH) { + throw new IllegalArgumentException("TRON account address must be exactly 21 bytes"); + } + if (target > upperBound || upperBound > getIndexedThrough()) { + throw new IllegalArgumentException("Account query is outside index coverage"); + } + if (target == Long.MAX_VALUE) { + return OptionalLong.empty(); + } + byte[] seek = dataKey(address, target + 1); + try (RocksIterator iterator = database.newIterator()) { + iterator.seek(seek); + if (!iterator.isValid()) { + return OptionalLong.empty(); + } + byte[] key = iterator.key(); + if (key.length != DATA_KEY_LENGTH || key[0] != DATA_PREFIX + || !Arrays.equals(address, Arrays.copyOfRange(key, 1, 1 + ADDRESS_LENGTH))) { + return OptionalLong.empty(); + } + long epoch = ByteBuffer.wrap(key, 1 + ADDRESS_LENGTH, Long.BYTES).getLong(); + return epoch <= upperBound ? OptionalLong.of(epoch) : OptionalLong.empty(); + } + } + + synchronized long getIndexedThrough() { + try { + byte[] encoded = database.get(HEAD_KEY); + return encoded == null ? -1 : ByteBuffer.wrap(encoded).getLong(); + } catch (RocksDBException failure) { + throw new ArchivePersistenceException("Failed to read account index head", failure); + } + } + + synchronized boolean headMatches(BlockSnapshotMeta meta) { + try { + byte[] encoded = database.get(HEAD_KEY); + return encoded != null + && encoded.length == Long.BYTES + 32 + && ByteBuffer.wrap(encoded).getLong() == meta.getEpoch() + && Arrays.equals(Arrays.copyOfRange(encoded, Long.BYTES, encoded.length), + meta.getBlockHash()); + } catch (RocksDBException failure) { + throw new ArchivePersistenceException("Failed to validate account index head", failure); + } + } + + private static byte[] dataKey(byte[] address, long epoch) { + if (address.length != ADDRESS_LENGTH || epoch < 0) { + throw new IllegalArgumentException("Invalid account change-index key"); + } + return ByteBuffer.allocate(DATA_KEY_LENGTH).put(DATA_PREFIX).put(address).putLong(epoch) + .array(); + } + + private static byte[] encodeHead(BlockSnapshotMeta meta) { + return ByteBuffer.allocate(Long.BYTES + 32).putLong(meta.getEpoch()).put(meta.getBlockHash()) + .array(); + } + + @Override + public synchronized void close() throws IOException { + syncWrites.close(); + database.close(); + options.close(); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java new file mode 100644 index 00000000000..44b11564de4 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java @@ -0,0 +1,159 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +/** Durable generation base identity for the experimental state archive. */ +final class ArchiveBaseManifest { + + private static final int MAGIC = 0x54414d46; // TAMF + private static final short VERSION = 1; + private static final int MAX_LENGTH = 1024 * 1024; + + private final Path directory; + private final Path path; + private final List participants; + private BaseIdentity base; + + ArchiveBaseManifest(Path directory, List participants) throws IOException { + this.directory = directory; + this.path = directory.resolve("MANIFEST"); + this.participants = new ArrayList<>(participants); + Files.createDirectories(directory); + if (Files.exists(path)) { + base = decode(Files.readAllBytes(path)); + if (!this.participants.equals(base.participants)) { + throw new ArchivePersistenceException("Archive manifest participant set mismatch"); + } + } + } + + synchronized void ensureBase(BlockSnapshotMeta firstArchivedBlock) throws IOException { + long epoch = firstArchivedBlock.getEpoch() - 1; + byte[] hash = firstArchivedBlock.getParentHash(); + if (base != null) { + if (base.epoch != epoch || !Arrays.equals(base.hash, hash)) { + throw new ArchivePersistenceException("Archive input does not extend the manifest base"); + } + return; + } + BaseIdentity identity = new BaseIdentity(epoch, hash, participants); + byte[] encoded = encode(identity); + Path temporary = directory.resolve(".MANIFEST-" + UUID.randomUUID()); + Files.write(temporary, encoded); + try (java.nio.channels.FileChannel channel = java.nio.channels.FileChannel.open(temporary, + java.nio.file.StandardOpenOption.WRITE)) { + channel.force(true); + } + try { + Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException failure) { + Files.deleteIfExists(temporary); + throw new IOException("Atomic archive manifest publication is not supported", failure); + } + HistorySegmentStore.syncDirectory(directory); + base = identity; + } + + private static byte[] encode(BaseIdentity identity) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(0); + output.writeLong(identity.epoch); + output.write(identity.hash); + output.writeInt(identity.participants.size()); + for (String participant : identity.participants) { + byte[] name = participant.getBytes(StandardCharsets.UTF_8); + output.writeInt(name.length); + output.write(name); + } + output.flush(); + byte[] payload = bytes.toByteArray(); + int length = payload.length + Integer.BYTES; + if (length > MAX_LENGTH) { + throw new IllegalArgumentException("Archive manifest is too large"); + } + ByteBuffer.wrap(payload).putInt(8, length); + ByteArrayOutputStream encoded = new ByteArrayOutputStream(length); + encoded.write(payload); + try (DataOutputStream checksum = new DataOutputStream(encoded)) { + checksum.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + checksum.flush(); + } + return encoded.toByteArray(); + } + + private static BaseIdentity decode(byte[] encoded) throws IOException { + if (encoded.length < 64 || encoded.length > MAX_LENGTH) { + throw new ArchivePersistenceException("Archive manifest length is invalid"); + } + int checksum = ByteBuffer.wrap(encoded, encoded.length - Integer.BYTES, + Integer.BYTES).getInt(); + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new ArchivePersistenceException("Archive manifest checksum mismatch"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != encoded.length) { + throw new ArchivePersistenceException("Unsupported archive manifest header"); + } + long epoch = input.readLong(); + byte[] hash = new byte[32]; + input.readFully(hash); + int count = input.readInt(); + if (epoch < -1 || count <= 0 || count > 1024) { + throw new ArchivePersistenceException("Archive manifest identity is invalid"); + } + List participants = new ArrayList<>(count); + String previous = null; + for (int i = 0; i < count; i++) { + int length = input.readInt(); + if (length <= 0 || length > 1024) { + throw new ArchivePersistenceException("Archive manifest participant is invalid"); + } + byte[] name = new byte[length]; + input.readFully(name); + String participant = new String(name, StandardCharsets.UTF_8); + if (previous != null && previous.compareTo(participant) >= 0) { + throw new ArchivePersistenceException("Archive manifest participants are not sorted"); + } + participants.add(participant); + previous = participant; + } + if (input.available() != Integer.BYTES) { + throw new ArchivePersistenceException("Archive manifest payload mismatch"); + } + return new BaseIdentity(epoch, hash, participants); + } + } + + private static final class BaseIdentity { + private final long epoch; + private final byte[] hash; + private final List participants; + + private BaseIdentity(long epoch, byte[] hash, List participants) { + this.epoch = epoch; + this.hash = Arrays.copyOf(hash, hash.length); + this.participants = new ArrayList<>(participants); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java index 416837be428..cda4bb5fc3c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java @@ -9,8 +9,6 @@ import java.util.List; import java.util.Set; import java.util.UUID; -import org.tron.core.db2.archive.HistoryIndexStore.ScannedIndexRecord; -import org.tron.core.db2.archive.HistorySegmentStore.ScannedRecord; /** * Ordered history body/index/marker writer. A marker is the only reader-visible commit boundary. @@ -20,6 +18,8 @@ public final class ArchiveHistoryWriter implements DurableBlockReverseDiffSink, private final HistorySegmentStore bodies; private final HistoryIndexStore index; private final HistoryCommitStore commits; + private final AccountChangeIndex accountIndex; + private final ArchiveBaseManifest manifest; private final List participatingDatabases; private final DurabilityHook hook; @@ -30,37 +30,74 @@ public ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, Set participatingDatabases, DurabilityHook hook) throws IOException { + this.participatingDatabases = new ArrayList<>(participatingDatabases); + this.participatingDatabases.sort(String::compareTo); + this.manifest = new ArchiveBaseManifest(archiveDirectory, this.participatingDatabases); this.bodies = new HistorySegmentStore(archiveDirectory, new BlockHistoryCodec(), maxSegmentSize); this.index = new HistoryIndexStore(archiveDirectory, new HistoryIndexCodec()); this.commits = new HistoryCommitStore(archiveDirectory, new HistoryCommitMarkerCodec()); - this.participatingDatabases = new ArrayList<>(participatingDatabases); - this.participatingDatabases.sort(String::compareTo); this.hook = hook; recoverPreparedSuffix(); + if (commits.head() != null) { + manifest.ensureBase(commits.get(commits.firstEpoch()).getMeta()); + } + this.accountIndex = new AccountChangeIndex(archiveDirectory.resolve("account-change-index")); + catchUpAccountIndex(); } @Override public synchronized void accept(BlockReverseDiff diff) { + acceptAll(java.util.Collections.singletonList(diff)); + } + + @Override + public synchronized void acceptAll(List diffs) { + if (diffs.isEmpty()) { + return; + } + if (commits.head() == null) { + try { + manifest.ensureBase(diffs.get(0).getMeta()); + } catch (IOException failure) { + throw new ArchivePersistenceException("Failed to establish archive base manifest", failure); + } + } + BlockSnapshotMeta previous = commits.head() == null ? null : commits.head().getMeta(); + for (BlockReverseDiff diff : diffs) { + validateNext(previous, diff.getMeta()); + previous = diff.getMeta(); + } try { - validateNext(diff.getMeta()); - hook.before(Stage.APPEND_BODY, diff.getMeta()); - HistoryLocation bodyLocation = bodies.append(diff); - hook.before(Stage.APPEND_INDEX, diff.getMeta()); - HistoryIndexRecord indexRecord = HistoryIndexRecord.from(diff, bodyLocation); - HistoryIndexLocation indexLocation = index.append(indexRecord); - hook.before(Stage.SYNC_BODY, diff.getMeta()); + List bodyLocations = new ArrayList<>(diffs.size()); + List indexLocations = new ArrayList<>(diffs.size()); + for (BlockReverseDiff diff : diffs) { + hook.before(Stage.APPEND_BODY, diff.getMeta()); + HistoryLocation bodyLocation = bodies.append(diff); + bodyLocations.add(bodyLocation); + hook.before(Stage.APPEND_INDEX, diff.getMeta()); + indexLocations.add(index.append(HistoryIndexRecord.from(diff, bodyLocation))); + } + BlockSnapshotMeta lastMeta = diffs.get(diffs.size() - 1).getMeta(); + hook.before(Stage.SYNC_BODY, lastMeta); bodies.sync(); - hook.before(Stage.SYNC_INDEX, diff.getMeta()); + hook.before(Stage.SYNC_INDEX, lastMeta); index.sync(); - hook.before(Stage.COMMIT_MARKER, diff.getMeta()); HistoryCommitMarker head = commits.head(); - long previousEpoch = head == null ? diff.getMeta().getEpoch() - 1 + long previousEpoch = head == null ? diffs.get(0).getMeta().getEpoch() - 1 : head.getMeta().getEpoch(); - commits.commit(new HistoryCommitMarker(diff.getMeta(), previousEpoch, bodyLocation, - indexLocation, batchId(), participatingDatabases)); + List markers = new ArrayList<>(diffs.size()); + for (int i = 0; i < diffs.size(); i++) { + BlockReverseDiff diff = diffs.get(i); + hook.before(Stage.COMMIT_MARKER, diff.getMeta()); + markers.add(new HistoryCommitMarker(diff.getMeta(), previousEpoch, + bodyLocations.get(i), indexLocations.get(i), batchId(), participatingDatabases)); + previousEpoch = diff.getMeta().getEpoch(); + } + commits.commitAll(markers); + accountIndex.apply(diffs); } catch (IOException | RuntimeException e) { - handleWriteFailure(diff.getMeta(), e); + handleWriteFailure(diffs.get(diffs.size() - 1).getMeta(), e); } } @@ -69,15 +106,18 @@ public synchronized void revert(BlockSnapshotMeta meta) { try { HistoryCommitMarker head = commits.head(); if (head != null && head.getMeta().equals(meta)) { + BlockReverseDiff reverted = readCommitted(meta.getEpoch()); + HistoryCommitMarker previous = commits.get(meta.getEpoch() - 1); + accountIndex.revert(reverted, previous == null ? null : previous.getMeta()); commits.removeHead(meta); - HistoryCommitMarker previous = commits.head(); + previous = commits.head(); index.truncateAfter(previous == null ? null : previous.getIndexLocation()); bodies.truncateAfter(previous == null ? null : previous.getHistoryLocation()); return; } - ScannedRecord bodyHead = last(bodies.getScanResult().getRecords()); - ScannedIndexRecord indexHead = last(index.getScanResult().getRecords()); + HistorySegmentStore.ScannedRecord bodyHead = bodies.getScanResult().getHead(); + HistoryIndexStore.ScannedIndexRecord indexHead = index.getScanResult().getHead(); if (bodyHead != null && bodyHead.getDiff().getMeta().equals(meta)) { HistoryCommitMarker committed = commits.head(); index.truncateAfter(committed == null ? null : committed.getIndexLocation()); @@ -128,12 +168,38 @@ public synchronized BlockReverseDiff readCommitted(long epoch) { } } - private void validateNext(BlockSnapshotMeta meta) { + public synchronized OldValue readAccountAt(long targetBlock, byte[] address, + byte[] accountAtCommittedHead) { HistoryCommitMarker head = commits.head(); if (head == null) { + throw new IllegalStateException("State archive has no committed history"); + } + long base = commits.firstEpoch() - 1; + if (targetBlock < base || targetBlock > head.getMeta().getEpoch()) { + throw new IllegalArgumentException("Account query is outside archive coverage"); + } + try { + java.util.OptionalLong changed = accountIndex.firstChangeAfter(address, targetBlock, + head.getMeta().getEpoch()); + if (!changed.isPresent()) { + return OldValue.fromNullable(accountAtCommittedHead); + } + BlockReverseDiff diff = readCommitted(changed.getAsLong()); + return findOldValue(diff, HistoricalAccountBalanceReader.ACCOUNT_DATABASE, address); + } catch (IOException failure) { + throw new ArchivePersistenceException("Failed to query historical account", failure); + } + } + + public synchronized BlockSnapshotMeta committedHeadMeta() { + HistoryCommitMarker marker = commits.head(); + return marker == null ? null : marker.getMeta(); + } + + private void validateNext(BlockSnapshotMeta previous, BlockSnapshotMeta meta) { + if (previous == null) { return; } - BlockSnapshotMeta previous = head.getMeta(); if (meta.getEpoch() != previous.getEpoch() + 1 || meta.getBlockNumber() != previous.getBlockNumber() + 1 || !Arrays.equals(meta.getParentHash(), previous.getBlockHash())) { @@ -159,43 +225,82 @@ private void handleWriteFailure(BlockSnapshotMeta meta, Exception failure) { private void recoverPreparedSuffix() throws IOException { HistorySegmentStore.ScanResult bodyScan = bodies.getScanResult(); HistoryIndexStore.ScanResult indexScan = index.getScanResult(); - int committedCount = commits.getMarkers().size(); - if (bodyScan.getRecords().size() < committedCount - || indexScan.getRecords().size() < committedCount) { + long committedCount = commits.size(); + if (bodyScan.getRecordCount() < committedCount + || indexScan.getRecordCount() < committedCount) { throw new ArchivePersistenceException("Committed marker references missing body/index data"); } - for (int i = 0; i < committedCount; i++) { - HistoryCommitMarker marker = commits.getMarkers().get(i); - ScannedRecord bodyRecord = bodyScan.getRecords().get(i); - ScannedIndexRecord indexRecord = indexScan.getRecords().get(i); - if (!marker.getMeta().equals(bodyRecord.getDiff().getMeta()) - || !marker.getMeta().equals(indexRecord.getRecord().getMeta())) { - throw new ArchivePersistenceException("Committed history metadata does not align"); - } - validateMarkerReferences(marker, indexRecord.getRecord()); - if (!same(marker.getHistoryLocation(), bodyRecord.getLocation()) - || !same(marker.getIndexLocation(), indexRecord.getLocation())) { - throw new ArchivePersistenceException("Commit marker location/digest mismatch"); - } - } if (bodyScan.getInvalidTail() != null) { - if (bodyScan.getRecords().size() < committedCount) { + if (bodyScan.getRecordCount() < committedCount) { throw new ArchivePersistenceException("Committed history body is corrupt"); } bodies.truncateInvalidTail(); } if (indexScan.getInvalidTailOffset() != null) { - if (indexScan.getRecords().size() < committedCount) { + if (indexScan.getRecordCount() < committedCount) { throw new ArchivePersistenceException("Committed history index is corrupt"); } index.truncateInvalidTail(); } HistoryCommitMarker head = commits.head(); + if (head != null) { + HistoryIndexRecord indexRecord = index.read(head.getIndexLocation()); + validateMarkerReferences(head, indexRecord); + BlockReverseDiff body = bodies.read(head.getHistoryLocation()); + if (!head.getMeta().equals(body.getMeta())) { + throw new ArchivePersistenceException("Commit head does not match history body metadata"); + } + } index.truncateAfter(head == null ? null : head.getIndexLocation()); bodies.truncateAfter(head == null ? null : head.getHistoryLocation()); } + private void catchUpAccountIndex() throws IOException { + HistoryCommitMarker head = commits.head(); + if (head == null) { + return; + } + long indexed = accountIndex.getIndexedThrough(); + long first = commits.firstEpoch(); + if (indexed >= 0) { + HistoryCommitMarker indexedMarker = commits.get(indexed); + if (indexedMarker == null || !accountIndex.headMatches(indexedMarker.getMeta())) { + throw new ArchivePersistenceException( + "Account index head differs from committed history"); + } + } + if (indexed >= head.getMeta().getEpoch()) { + if (indexed > head.getMeta().getEpoch()) { + throw new ArchivePersistenceException("Account index is ahead of committed history"); + } + return; + } + long next = indexed < 0 ? first : indexed + 1; + List batch = new ArrayList<>(1024); + for (long epoch = next; epoch <= head.getMeta().getEpoch(); epoch++) { + batch.add(readCommitted(epoch)); + if (batch.size() == 1024 || epoch == head.getMeta().getEpoch()) { + accountIndex.apply(batch); + batch.clear(); + } + } + } + + private static OldValue findOldValue(BlockReverseDiff diff, String dbName, byte[] rawKey) { + for (BlockReverseDiff.DbGroup group : diff.getGroups()) { + if (!dbName.equals(group.getDbName())) { + continue; + } + for (BlockReverseDiff.Entry entry : group.getEntries()) { + if (Arrays.equals(rawKey, entry.getKey())) { + return entry.getOldValue(); + } + } + } + throw new ArchivePersistenceException("Account index references a missing history key"); + } + private void validateMarkerReferences(HistoryCommitMarker marker, HistoryIndexRecord indexRecord) { if (!marker.getMeta().equals(indexRecord.getMeta()) @@ -224,10 +329,6 @@ private static byte[] batchId() { .putLong(uuid.getLeastSignificantBits()).array(); } - private static T last(List values) { - return values.isEmpty() ? null : values.get(values.size() - 1); - } - @Override public synchronized void close() throws IOException { IOException failure = null; @@ -236,6 +337,15 @@ public synchronized void close() throws IOException { } catch (IOException e) { failure = e; } + try { + accountIndex.close(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } try { bodies.close(); } catch (IOException e) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveQueryLimitExceededException.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveQueryLimitExceededException.java new file mode 100644 index 00000000000..ea46c71bf3c --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveQueryLimitExceededException.java @@ -0,0 +1,9 @@ +package org.tron.core.db2.archive; + +/** Raised when a historical query cannot produce a complete result inside its resource budget. */ +public class ArchiveQueryLimitExceededException extends RuntimeException { + + public ArchiveQueryLimitExceededException(String message) { + super(message); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java new file mode 100644 index 00000000000..2265bbcb007 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java @@ -0,0 +1,191 @@ +package org.tron.core.db2.archive; + +import com.google.protobuf.InvalidProtocolBufferException; +import java.io.Closeable; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import org.tron.core.store.StorageRowKeyCodec; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +/** Request-owned bindings from every versioned Store to one pinned archive snapshot. */ +public final class ArchiveReadContext implements Closeable { + + private final ArchiveReadSnapshot snapshot; + private final Map> adapters; + private boolean closed; + + private ArchiveReadContext(ArchiveReadSnapshot snapshot, + Collection> adapters) { + this.snapshot = Objects.requireNonNull(snapshot, "snapshot"); + this.adapters = validateAdapters(adapters); + } + + /** Takes ownership of {@code snapshot}, including when adapter validation fails. */ + public static ArchiveReadContext open(ArchiveReadSnapshot snapshot, + Collection> adapters) throws IOException { + try { + return new ArchiveReadContext(snapshot, adapters); + } catch (RuntimeException failure) { + closeAfterFailedOpen(snapshot, failure); + throw failure; + } + } + + public synchronized HistoricalStore store(StoreAdapter adapter) { + ensureOpen(); + Objects.requireNonNull(adapter, "adapter"); + if (adapters.get(adapter.getDbName()) != adapter) { + throw new IllegalArgumentException( + "Store adapter does not belong to this archive read context: " + adapter.getDbName()); + } + return new HistoricalStore<>(snapshot, adapter); + } + + public Set getAdapterDbNames() { + return Collections.unmodifiableSet(new LinkedHashSet<>(adapters.keySet())); + } + + public long getTargetBlock() { + return snapshot.getTargetBlock(); + } + + public long getPinnedBlock() { + return snapshot.getPinnedBlock(); + } + + /** Resolves one logical contract slot using contract metadata from this same pinned context. */ + public synchronized Optional getStorage(byte[] contractAddress, byte[] logicalSlot) + throws IOException { + ensureOpen(); + Objects.requireNonNull(contractAddress, "contractAddress"); + Objects.requireNonNull(logicalSlot, "logicalSlot"); + OldValue contractValue = snapshot.get("contract", contractAddress); + if (!contractValue.isPresent()) { + throw new ArchivePersistenceException( + "Historical Contract metadata is absent for logical storage lookup"); + } + SmartContract contract; + try { + contract = SmartContract.parseFrom(contractValue.getValue()); + } catch (InvalidProtocolBufferException e) { + throw new ArchivePersistenceException( + "Historical Contract metadata cannot be decoded", e); + } + byte[] physicalKey = StorageRowKeyCodec.physicalKey(contractAddress, logicalSlot, + contract.getVersion(), contract.getTrxHash().toByteArray()); + OldValue storageValue = snapshot.get("storage-row", physicalKey); + return storageValue.isPresent() + ? Optional.of(storageValue.getValue()) : Optional.empty(); + } + + @Override + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + snapshot.close(); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Archive read context is closed"); + } + } + + private static Map> validateAdapters( + Collection> definitions) { + Objects.requireNonNull(definitions, "adapters"); + Map> indexed = new LinkedHashMap<>(); + for (StoreAdapter adapter : definitions) { + Objects.requireNonNull(adapter, "adapter"); + if (indexed.put(adapter.getDbName(), adapter) != null) { + throw new IllegalArgumentException("Duplicate historical Store adapter: " + + adapter.getDbName()); + } + } + Set expected = ArchiveStoreScope.getStateDatabases(); + if (!indexed.keySet().equals(expected)) { + Set missing = new LinkedHashSet<>(expected); + missing.removeAll(indexed.keySet()); + Set unexpected = new LinkedHashSet<>(indexed.keySet()); + unexpected.removeAll(expected); + throw new IllegalArgumentException("Historical Store adapter set mismatch; missing=" + + missing + ", unexpected=" + unexpected); + } + return Collections.unmodifiableMap(indexed); + } + + private static void closeAfterFailedOpen(ArchiveReadSnapshot snapshot, + RuntimeException failure) throws IOException { + if (snapshot == null) { + return; + } + try { + snapshot.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + + /** Immutable adapter definition; one definition must exist for every versioned physical Store. */ + public static final class StoreAdapter { + private final String dbName; + private final ValueDecoder decoder; + + private StoreAdapter(String dbName, ValueDecoder decoder) { + this.dbName = Objects.requireNonNull(dbName, "dbName"); + this.decoder = Objects.requireNonNull(decoder, "decoder"); + if (!ArchiveStoreScope.isStateDatabase(dbName)) { + throw new IllegalArgumentException("Not a versioned archive state database: " + dbName); + } + } + + public static StoreAdapter define(String dbName, ValueDecoder decoder) { + return new StoreAdapter<>(dbName, decoder); + } + + public String getDbName() { + return dbName; + } + } + + /** Read-only point view for one exact physical Store keyspace. */ + public static final class HistoricalStore { + private final ArchiveReadSnapshot snapshot; + private final StoreAdapter adapter; + + private HistoricalStore(ArchiveReadSnapshot snapshot, StoreAdapter adapter) { + this.snapshot = snapshot; + this.adapter = adapter; + } + + public Optional get(byte[] physicalRawKey) throws IOException { + OldValue value = snapshot.get(adapter.dbName, physicalRawKey); + if (!value.isPresent()) { + return Optional.empty(); + } + T decoded = adapter.decoder.decode(value.getValue()); + if (decoded == null) { + throw new IllegalStateException( + "Historical Store adapter returned null: " + adapter.dbName); + } + return Optional.of(decoded); + } + + public boolean has(byte[] physicalRawKey) throws IOException { + return snapshot.get(adapter.dbName, physicalRawKey).isPresent(); + } + } + + @FunctionalInterface + public interface ValueDecoder { + T decode(byte[] value); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java new file mode 100644 index 00000000000..e00af49f52d --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java @@ -0,0 +1,194 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.OptionalLong; +import org.tron.core.db2.archive.HistoricalRangeOverlay.Entry; +import org.tron.core.db2.archive.HistoricalRangeOverlay.KeyRange; +import org.tron.core.db2.archive.HistoricalRangeOverlay.Limits; + +/** One immutable physical-key archive read context pinned at {@code S(P)}. */ +public final class ArchiveReadSnapshot implements Closeable { + + private final long targetBlock; + private final long pinnedBlock; + private final byte[] pinnedHash; + private final ServingKeyIndexGeneration serving; + private final PinnedLatestState latest; + private final PinnedHistory history; + private boolean closed; + + private ArchiveReadSnapshot(long targetBlock, long pinnedBlock, byte[] pinnedHash, + ServingKeyIndexGeneration serving, PinnedLatestState latest, PinnedHistory history) { + if (targetBlock > pinnedBlock) { + throw new IllegalArgumentException("Target block must not exceed pinned block"); + } + this.pinnedHash = copyHash(pinnedHash, "pinnedHash"); + this.serving = Objects.requireNonNull(serving, "serving"); + this.latest = Objects.requireNonNull(latest, "latest"); + this.history = Objects.requireNonNull(history, "history"); + validateIdentity(targetBlock, pinnedBlock); + this.targetBlock = targetBlock; + this.pinnedBlock = pinnedBlock; + } + + /** Takes ownership of already pinned resources, including on identity-validation failure. */ + public static ArchiveReadSnapshot pin(long targetBlock, long pinnedBlock, byte[] pinnedHash, + ServingKeyIndexGeneration serving, PinnedLatestState latest, PinnedHistory history) + throws IOException { + try { + return new ArchiveReadSnapshot(targetBlock, pinnedBlock, pinnedHash, serving, latest, + history); + } catch (RuntimeException failure) { + closeAfterFailedPin(history, latest, failure); + throw failure; + } + } + + public synchronized OldValue get(String dbName, byte[] physicalRawKey) throws IOException { + ensureOpen(); + Objects.requireNonNull(dbName, "dbName"); + Objects.requireNonNull(physicalRawKey, "physicalRawKey"); + OptionalLong first = serving.firstChangeAfter( + dbName, physicalRawKey, targetBlock, pinnedBlock); + OldValue value = first.isPresent() + ? history.read(dbName, physicalRawKey, first.getAsLong()) + : latest.get(dbName, physicalRawKey); + if (value == null) { + throw new IllegalStateException("Pinned latest state returned null"); + } + return value; + } + + public synchronized List range(String dbName, KeyRange range, Limits limits) + throws IOException { + ensureOpen(); + List pinnedLatest = latest.range(dbName, range.getLowerInclusive(), + range.getUpperExclusive()); + if (pinnedLatest == null) { + throw new IllegalStateException("Pinned latest range returned null"); + } + return HistoricalRangeOverlay.materialize(dbName, targetBlock, pinnedBlock, range, + pinnedLatest, serving, history::read, limits); + } + + public long getTargetBlock() { + return targetBlock; + } + + public long getPinnedBlock() { + return pinnedBlock; + } + + public byte[] getPinnedHash() { + return Arrays.copyOf(pinnedHash, pinnedHash.length); + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + IOException failure = null; + try { + history.close(); + } catch (IOException e) { + failure = e; + } + try { + latest.close(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + if (failure != null) { + throw failure; + } + } + + private void validateIdentity(long target, long pinned) { + if (target < serving.getIndexedFrom() || pinned != serving.getIndexedThrough()) { + throw new IllegalArgumentException( + "Prototype read snapshot requires complete serving coverage through P"); + } + if (latest.getBlockNumber() != pinned || history.getIndexedFrom() != serving.getIndexedFrom() + || history.getIndexedThrough() != pinned + || !Arrays.equals(pinnedHash, serving.getHeadHash()) + || !Arrays.equals(pinnedHash, latest.getBlockHash()) + || !Arrays.equals(pinnedHash, history.getHeadHash()) + || !Arrays.equals(serving.getAuthoritativePrefixDigest(), + history.getAuthoritativePrefixDigest())) { + throw new IllegalArgumentException("Archive read snapshot identity mismatch"); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Archive read snapshot is closed"); + } + } + + private static byte[] copyHash(byte[] hash, String name) { + if (hash == null || hash.length != 32) { + throw new IllegalArgumentException(name + " must be exactly 32 bytes"); + } + return Arrays.copyOf(hash, hash.length); + } + + private static void closeAfterFailedPin(PinnedHistory history, PinnedLatestState latest, + RuntimeException failure) throws IOException { + IOException closeFailure = null; + if (history != null) { + try { + history.close(); + } catch (IOException e) { + closeFailure = e; + } + } + if (latest != null) { + try { + latest.close(); + } catch (IOException e) { + if (closeFailure == null) { + closeFailure = e; + } else { + closeFailure.addSuppressed(e); + } + } + } + if (closeFailure != null) { + failure.addSuppressed(closeFailure); + } + } + + public interface PinnedLatestState extends Closeable { + long getBlockNumber(); + + byte[] getBlockHash(); + + OldValue get(String dbName, byte[] physicalRawKey) throws IOException; + + List range(String dbName, byte[] lowerInclusive, byte[] upperExclusive) + throws IOException; + } + + public interface PinnedHistory extends Closeable { + long getIndexedFrom(); + + long getIndexedThrough(); + + byte[] getHeadHash(); + + byte[] getAuthoritativePrefixDigest(); + + OldValue read(String dbName, byte[] physicalRawKey, long firstChangeBlock) + throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java index e8a5a375c27..76d6ad8e4f9 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java @@ -17,7 +17,6 @@ public final class ArchiveStoreScope { "account-index", "account", "account-asset", - "accountTrie", "asset-issue", "asset-issue-v2", "code", @@ -43,6 +42,7 @@ public final class ArchiveStoreScope { private static final Set NON_STATE_DATABASES = immutableSet( "account-trace", + "accountTrie", "balance-trace", "block", "block-index", diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java b/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java index ab7173800ce..9e39ef72d64 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java @@ -38,24 +38,37 @@ public AsyncArchiveHistorySink(ArchiveHistoryWriter writer, int capacity) { @Override public void accept(BlockReverseDiff diff) { - WorkItem item = new WorkItem(diff); + acceptAll(java.util.Collections.singletonList(diff)); + } + + @Override + public void acceptAll(List diffs) { + if (diffs.isEmpty()) { + return; + } + WorkItem item = new WorkItem(diffs); synchronized (submitted) { ensureOperational(); - if (acceptedHead != null) { - validateContinuity(acceptedHead, diff.getMeta()); - } - if (submitted.put(diff.getMeta().getEpoch(), item) != null) { - throw new ArchivePersistenceException("Duplicate submitted archive epoch"); + BlockSnapshotMeta previous = acceptedHead; + for (BlockReverseDiff diff : item.diffs) { + if (previous != null) { + validateContinuity(previous, diff.getMeta()); + } + if (submitted.containsKey(diff.getMeta().getEpoch())) { + throw new ArchivePersistenceException("Duplicate submitted archive epoch"); + } + previous = diff.getMeta(); } - acceptedHead = diff.getMeta(); + item.diffs.forEach(diff -> submitted.put(diff.getMeta().getEpoch(), item)); + acceptedHead = item.lastMeta(); } try { queue.put(item); } catch (InterruptedException e) { synchronized (submitted) { - submitted.remove(diff.getMeta().getEpoch()); + removeSubmitted(item); WorkItem previous = lastSubmitted(); - acceptedHead = previous == null ? committedMeta() : previous.diff.getMeta(); + acceptedHead = previous == null ? committedMeta() : previous.lastMeta(); } Thread.currentThread().interrupt(); throw new ArchivePersistenceException("Interrupted by archive queue backpressure", e); @@ -68,13 +81,13 @@ public void revert(BlockSnapshotMeta meta) { synchronized (submitted) { ensureOperational(); item = lastSubmitted(); - if (item == null || !item.diff.getMeta().equals(meta)) { + if (item == null || item.diffs.size() != 1 || !item.lastMeta().equals(meta)) { throw new ArchivePersistenceException("Archive reorg must remove the submitted head"); } if (queue.remove(item)) { - submitted.remove(meta.getEpoch()); + removeSubmitted(item); WorkItem previous = lastSubmitted(); - acceptedHead = previous == null ? committedMeta() : previous.diff.getMeta(); + acceptedHead = previous == null ? committedMeta() : previous.lastMeta(); item.completion.cancel(false); return; } @@ -82,9 +95,9 @@ public void revert(BlockSnapshotMeta meta) { await(item); writer.revert(meta); synchronized (submitted) { - submitted.remove(meta.getEpoch()); + removeSubmitted(item); WorkItem previous = lastSubmitted(); - acceptedHead = previous == null ? committedMeta() : previous.diff.getMeta(); + acceptedHead = previous == null ? committedMeta() : previous.lastMeta(); } } @@ -94,7 +107,7 @@ public void awaitCommitted(long epoch) { synchronized (submitted) { ensureOperational(); submitted.forEach((candidate, item) -> { - if (candidate <= epoch) { + if (candidate <= epoch && !required.contains(item)) { required.add(item); } }); @@ -132,7 +145,7 @@ private void run() { return; } try { - writer.accept(item.diff); + writer.acceptAll(item.diffs); item.completion.complete(null); } catch (Throwable failure) { fatalFailure = failure; @@ -181,6 +194,10 @@ private WorkItem lastSubmitted() { return last; } + private void removeSubmitted(WorkItem item) { + item.diffs.forEach(diff -> submitted.remove(diff.getMeta().getEpoch())); + } + private BlockSnapshotMeta committedMeta() { HistoryCommitMarker marker = writer.committedHead(); return marker == null ? null : marker.getMeta(); @@ -220,22 +237,26 @@ public synchronized void close() throws IOException { } private static final class WorkItem { - private final BlockReverseDiff diff; + private final List diffs; private final boolean poison; private final CompletableFuture completion = new CompletableFuture<>(); - private WorkItem(BlockReverseDiff diff) { - this.diff = diff; + private WorkItem(List diffs) { + this.diffs = java.util.Collections.unmodifiableList(new ArrayList<>(diffs)); this.poison = false; } private WorkItem() { - this.diff = null; + this.diffs = java.util.Collections.emptyList(); this.poison = true; } private static WorkItem poison() { return new WorkItem(); } + + private BlockSnapshotMeta lastMeta() { + return diffs.get(diffs.size() - 1).getMeta(); + } } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiffSink.java b/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiffSink.java index b1bafdb8942..b050d825ee4 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiffSink.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiffSink.java @@ -1,5 +1,6 @@ package org.tron.core.db2.archive; +import java.util.List; import org.tron.core.db2.archive.BlockSnapshotMeta; /** Downstream boundary for an archive writer or a bounded writer queue. */ @@ -7,6 +8,10 @@ public interface BlockReverseDiffSink { void accept(BlockReverseDiff diff); + default void acceptAll(List diffs) { + diffs.forEach(this::accept); + } + default void revert(BlockSnapshotMeta meta) { // A durable writer will override this and truncate/discard its uncommitted canonical tail. } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/CommittedHistoryReader.java b/chainbase/src/main/java/org/tron/core/db2/archive/CommittedHistoryReader.java new file mode 100644 index 00000000000..7a2a8c97e3b --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/CommittedHistoryReader.java @@ -0,0 +1,217 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.HistoryIndexRecord.KeyGroup; + +/** + * Fail-closed authoritative old-value reader for one immutable committed prefix. + * + *

The current prototype validates its source identity by rebuilding a throwaway serving + * generation. A production reader will pin commit/index/segment generations without rebuilding. + */ +public final class CommittedHistoryReader implements ArchiveReadSnapshot.PinnedHistory { + + private final long indexedFrom; + private final long indexedThrough; + private final byte[] headHash; + private final byte[] authoritativePrefixDigest; + private final Map markers; + private final ServingKeyIndexGeneration.AuthoritativeIndexReader indexReader; + private final BodyReader bodyReader; + private final Closeable release; + private boolean closed; + + public CommittedHistoryReader(long baseEpoch, byte[] baseHash, + List committed, + ServingKeyIndexGeneration.AuthoritativeIndexReader indexReader, BodyReader bodyReader) + throws IOException { + this(baseEpoch, baseHash, committed, indexReader, bodyReader, null, () -> { }); + } + + public CommittedHistoryReader(long baseEpoch, byte[] baseHash, + List committed, + ServingKeyIndexGeneration.AuthoritativeIndexReader indexReader, BodyReader bodyReader, + List participatingDatabases) throws IOException { + this(baseEpoch, baseHash, committed, indexReader, bodyReader, participatingDatabases, + () -> { }); + } + + public CommittedHistoryReader(long baseEpoch, byte[] baseHash, + List committed, + ServingKeyIndexGeneration.AuthoritativeIndexReader indexReader, BodyReader bodyReader, + Closeable release) throws IOException { + this(baseEpoch, baseHash, committed, indexReader, bodyReader, null, release); + } + + private CommittedHistoryReader(long baseEpoch, byte[] baseHash, + List committed, + ServingKeyIndexGeneration.AuthoritativeIndexReader indexReader, BodyReader bodyReader, + List participatingDatabases, Closeable release) throws IOException { + Objects.requireNonNull(committed, "committed"); + this.indexReader = Objects.requireNonNull(indexReader, "indexReader"); + this.bodyReader = Objects.requireNonNull(bodyReader, "bodyReader"); + this.release = Objects.requireNonNull(release, "release"); + ServingKeyIndexGeneration verified; + try { + verified = participatingDatabases == null + ? ServingKeyIndexGeneration.rebuild( + "authoritative-history-reader", baseEpoch, baseHash, committed, indexReader) + : ServingKeyIndexGeneration.rebuild( + "authoritative-history-reader", baseEpoch, baseHash, committed, indexReader, + participatingDatabases, + ServingKeyIndexGeneration.IndexLayout.prototypeDefaults()); + } catch (IOException | RuntimeException failure) { + closeAfterFailedConstruction(release, failure); + throw failure; + } + this.indexedFrom = verified.getIndexedFrom(); + this.indexedThrough = verified.getIndexedThrough(); + this.headHash = verified.getHeadHash(); + this.authoritativePrefixDigest = verified.getAuthoritativePrefixDigest(); + this.markers = new HashMap<>(); + for (HistoryCommitMarker marker : committed) { + if (markers.put(marker.getMeta().getEpoch(), marker) != null) { + throw new IllegalArgumentException("Duplicate committed history epoch"); + } + } + } + + @Override + public synchronized OldValue read(String dbName, byte[] rawKey, long firstChangeBlock) + throws IOException { + ensureOpen(); + Objects.requireNonNull(dbName, "dbName"); + Objects.requireNonNull(rawKey, "rawKey"); + HistoryCommitMarker marker = markers.get(firstChangeBlock); + if (marker == null) { + throw new ArchivePersistenceException( + "Serving index references an uncommitted history epoch: " + firstChangeBlock); + } + HistoryIndexRecord index = indexReader.read(marker.getIndexLocation()); + validateMarker(marker, index); + BlockReverseDiff body = bodyReader.read(marker.getHistoryLocation()); + if (!marker.getMeta().equals(body.getMeta()) || !sameKeys(index, body)) { + throw new ArchivePersistenceException("Authoritative history index/body key mismatch"); + } + if (!contains(index, dbName, rawKey)) { + throw new ArchivePersistenceException( + "Serving index key is absent from authoritative history index"); + } + for (DbGroup group : body.getGroups()) { + if (!dbName.equals(group.getDbName())) { + continue; + } + for (Entry entry : group.getEntries()) { + if (Arrays.equals(rawKey, entry.getKey())) { + return entry.getOldValue(); + } + } + } + throw new ArchivePersistenceException( + "Authoritative history body is missing the indexed key"); + } + + @Override + public long getIndexedFrom() { + return indexedFrom; + } + + @Override + public long getIndexedThrough() { + return indexedThrough; + } + + @Override + public byte[] getHeadHash() { + return Arrays.copyOf(headHash, headHash.length); + } + + @Override + public byte[] getAuthoritativePrefixDigest() { + return Arrays.copyOf(authoritativePrefixDigest, authoritativePrefixDigest.length); + } + + @Override + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + release.close(); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Committed history reader is closed"); + } + } + + private static void validateMarker(HistoryCommitMarker marker, HistoryIndexRecord index) { + if (!marker.getMeta().equals(index.getMeta()) + || !same(marker.getHistoryLocation(), index.getHistoryLocation())) { + throw new ArchivePersistenceException( + "Commit marker does not match authoritative history index"); + } + } + + private static boolean contains(HistoryIndexRecord index, String dbName, byte[] rawKey) { + for (KeyGroup group : index.getGroups()) { + if (dbName.equals(group.getDbName())) { + return group.getKeys().stream().anyMatch(key -> Arrays.equals(key, rawKey)); + } + } + return false; + } + + private static boolean sameKeys(HistoryIndexRecord index, BlockReverseDiff body) { + List indexed = index.getGroups(); + List stored = body.getGroups(); + if (indexed.size() != stored.size()) { + return false; + } + for (int groupIndex = 0; groupIndex < indexed.size(); groupIndex++) { + KeyGroup indexedGroup = indexed.get(groupIndex); + DbGroup storedGroup = stored.get(groupIndex); + List indexedKeys = indexedGroup.getKeys(); + List storedEntries = storedGroup.getEntries(); + if (!indexedGroup.getDbName().equals(storedGroup.getDbName()) + || indexedKeys.size() != storedEntries.size()) { + return false; + } + for (int keyIndex = 0; keyIndex < indexedKeys.size(); keyIndex++) { + if (!Arrays.equals(indexedKeys.get(keyIndex), storedEntries.get(keyIndex).getKey())) { + return false; + } + } + } + return true; + } + + private static boolean same(HistoryLocation left, HistoryLocation right) { + return left.getSegmentId() == right.getSegmentId() + && left.getOffset() == right.getOffset() + && left.getRecordLength() == right.getRecordLength() + && left.getBodyChecksum() == right.getBodyChecksum() + && Arrays.equals(left.getBodyDigest(), right.getBodyDigest()); + } + + private static void closeAfterFailedConstruction(Closeable release, Exception failure) { + try { + release.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + + @FunctionalInterface + public interface BodyReader { + BlockReverseDiff read(HistoryLocation location) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountBalanceReader.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountBalanceReader.java new file mode 100644 index 00000000000..512634bf68e --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountBalanceReader.java @@ -0,0 +1,89 @@ +package org.tron.core.db2.archive; + +import com.google.protobuf.InvalidProtocolBufferException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; +import org.tron.protos.Protocol.Account; + +/** Narrow historical TRX account-balance reader over one pinned archive snapshot. */ +public final class HistoricalAccountBalanceReader { + + public static final String ACCOUNT_DATABASE = "account"; + public static final int ADDRESS_LENGTH = 21; + + private HistoricalAccountBalanceReader() { + } + + public static Result read(ArchiveReadSnapshot snapshot, byte[] address) throws IOException { + Objects.requireNonNull(snapshot, "snapshot"); + if (address == null || address.length != ADDRESS_LENGTH) { + throw new IllegalArgumentException("TRON account address must be exactly 21 bytes"); + } + OldValue historical = snapshot.get(ACCOUNT_DATABASE, address); + return decode(snapshot.getTargetBlock(), address, historical); + } + + public static Result decode(long blockNumber, byte[] address, OldValue historical) { + Objects.requireNonNull(historical, "historical"); + if (address == null || address.length != ADDRESS_LENGTH) { + throw new IllegalArgumentException("TRON account address must be exactly 21 bytes"); + } + if (!historical.isPresent()) { + return Result.absent(blockNumber, address); + } + Account account; + try { + account = Account.parseFrom(historical.getValue()); + } catch (InvalidProtocolBufferException failure) { + throw new ArchivePersistenceException("Historical account value is not valid protobuf", + failure); + } + if (!Arrays.equals(address, account.getAddress().toByteArray())) { + throw new ArchivePersistenceException( + "Historical account value address does not match the physical key"); + } + return Result.present(blockNumber, address, account.getBalance()); + } + + public static final class Result { + private final long blockNumber; + private final byte[] address; + private final boolean present; + private final long balance; + + private Result(long blockNumber, byte[] address, boolean present, long balance) { + this.blockNumber = blockNumber; + this.address = Arrays.copyOf(address, address.length); + this.present = present; + this.balance = balance; + } + + private static Result absent(long blockNumber, byte[] address) { + return new Result(blockNumber, address, false, 0); + } + + private static Result present(long blockNumber, byte[] address, long balance) { + return new Result(blockNumber, address, true, balance); + } + + public long getBlockNumber() { + return blockNumber; + } + + public byte[] getAddress() { + return Arrays.copyOf(address, address.length); + } + + public boolean isPresent() { + return present; + } + + public long getBalance() { + if (!present) { + throw new IllegalStateException("Historical account is absent"); + } + return balance; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java new file mode 100644 index 00000000000..eb880d3ad46 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java @@ -0,0 +1,179 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.ServingKeyIndexGeneration.ChangedKey; + +/** + * Backend-neutral prototype for one-store historical range materialization. + * + *

The caller must supply latest entries from the same pinned {@code S(P)} generation as the + * serving index. This class deliberately provides no cross-database merge API and no persistent + * cursor encoding. + */ +public final class HistoricalRangeOverlay { + + private HistoricalRangeOverlay() { + } + + public static List materialize(String dbName, long targetBlock, long upperBound, + KeyRange range, List pinnedLatest, ServingKeyIndexGeneration index, + HistoricalValueReader history, Limits limits) throws IOException { + Objects.requireNonNull(dbName, "dbName"); + Objects.requireNonNull(range, "range"); + Objects.requireNonNull(pinnedLatest, "pinnedLatest"); + Objects.requireNonNull(index, "index"); + Objects.requireNonNull(history, "history"); + Objects.requireNonNull(limits, "limits"); + validateLatest(pinnedLatest, range); + + List changed = index.changesInRange(dbName, range.lowerInclusive, + range.upperExclusive, targetBlock, upperBound, limits.maxChangedKeys); + List result = new ArrayList<>(); + int latestIndex = 0; + int changedIndex = 0; + int candidates = 0; + + while (latestIndex < pinnedLatest.size() || changedIndex < changed.size()) { + if (++candidates > limits.maxCandidateKeys) { + throw new ArchiveQueryLimitExceededException("candidate-key budget exceeded"); + } + Entry latest = latestIndex < pinnedLatest.size() ? pinnedLatest.get(latestIndex) : null; + ChangedKey delta = changedIndex < changed.size() ? changed.get(changedIndex) : null; + int comparison = latest == null ? 1 : delta == null ? -1 + : BlockReverseDiff.compareUnsigned(latest.key, delta.getKey()); + + if (comparison < 0) { + addResult(result, latest, limits.maxResults); + latestIndex++; + } else { + byte[] key = delta.getKey(); + OldValue value = history.read(dbName, key, delta.getFirstChangeBlock()); + if (value == null) { + throw new IllegalStateException("historical value reader returned null"); + } + if (value.isPresent()) { + addResult(result, new Entry(key, value.getValue()), limits.maxResults); + } + changedIndex++; + if (comparison == 0) { + latestIndex++; + } + } + } + return Collections.unmodifiableList(result); + } + + private static void validateLatest(List latest, KeyRange range) { + byte[] previous = null; + for (Entry entry : latest) { + Objects.requireNonNull(entry, "latest entry"); + if (!range.contains(entry.key)) { + throw new IllegalArgumentException("latest entry is outside requested range"); + } + if (previous != null && BlockReverseDiff.compareUnsigned(previous, entry.key) >= 0) { + throw new IllegalArgumentException("latest entries must be strictly sorted"); + } + previous = entry.key; + } + } + + private static void addResult(List result, Entry entry, int maxResults) { + if (result.size() == maxResults) { + throw new ArchiveQueryLimitExceededException("result budget exceeded"); + } + result.add(entry); + } + + @FunctionalInterface + public interface HistoricalValueReader { + OldValue read(String dbName, byte[] rawKey, long firstChangeBlock) throws IOException; + } + + public static final class Limits { + private final int maxChangedKeys; + private final int maxCandidateKeys; + private final int maxResults; + + public Limits(int maxChangedKeys, int maxCandidateKeys, int maxResults) { + if (maxChangedKeys <= 0 || maxCandidateKeys <= 0 || maxResults <= 0) { + throw new IllegalArgumentException("historical range limits must be positive"); + } + this.maxChangedKeys = maxChangedKeys; + this.maxCandidateKeys = maxCandidateKeys; + this.maxResults = maxResults; + } + } + + public static final class KeyRange { + private final byte[] lowerInclusive; + private final byte[] upperExclusive; + + private KeyRange(byte[] lowerInclusive, byte[] upperExclusive) { + this.lowerInclusive = Arrays.copyOf(lowerInclusive, lowerInclusive.length); + this.upperExclusive = upperExclusive == null ? null + : Arrays.copyOf(upperExclusive, upperExclusive.length); + if (this.upperExclusive != null + && BlockReverseDiff.compareUnsigned(this.lowerInclusive, this.upperExclusive) > 0) { + throw new IllegalArgumentException("lowerInclusive must not exceed upperExclusive"); + } + } + + public static KeyRange range(byte[] lowerInclusive, byte[] upperExclusive) { + Objects.requireNonNull(lowerInclusive, "lowerInclusive"); + return new KeyRange(lowerInclusive, upperExclusive); + } + + public static KeyRange prefix(byte[] prefix) { + Objects.requireNonNull(prefix, "prefix"); + byte[] upper = Arrays.copyOf(prefix, prefix.length); + for (int i = upper.length - 1; i >= 0; i--) { + if ((upper[i] & 0xff) != 0xff) { + upper[i]++; + upper = Arrays.copyOf(upper, i + 1); + return new KeyRange(prefix, upper); + } + } + return new KeyRange(prefix, null); + } + + public byte[] getLowerInclusive() { + return Arrays.copyOf(lowerInclusive, lowerInclusive.length); + } + + public byte[] getUpperExclusive() { + return upperExclusive == null ? null + : Arrays.copyOf(upperExclusive, upperExclusive.length); + } + + private boolean contains(byte[] key) { + return BlockReverseDiff.compareUnsigned(key, lowerInclusive) >= 0 + && (upperExclusive == null + || BlockReverseDiff.compareUnsigned(key, upperExclusive) < 0); + } + } + + public static final class Entry { + private final byte[] key; + private final byte[] value; + + public Entry(byte[] key, byte[] value) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(value, "value"); + this.key = Arrays.copyOf(key, key.length); + this.value = Arrays.copyOf(value, value.length); + } + + public byte[] getKey() { + return Arrays.copyOf(key, key.length); + } + + public byte[] getValue() { + return Arrays.copyOf(value, value.length); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarkerCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarkerCodec.java index b33652f5b8c..4c738be9ee6 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarkerCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitMarkerCodec.java @@ -19,6 +19,7 @@ public final class HistoryCommitMarkerCodec { private static final int MAGIC = 0x54415243; // TARC private static final short VERSION = 1; private static final int MAX_MARKER_LENGTH = 1024 * 1024; + static final int HEADER_LENGTH = 12; public byte[] encode(HistoryCommitMarker marker) { try { @@ -126,6 +127,22 @@ public HistoryCommitMarker decode(byte[] encoded) { } } + /** Returns the complete marker length described by its fixed prefix. */ + public int recordLength(byte[] prefix) { + if (prefix == null || prefix.length < HEADER_LENGTH) { + throw new IllegalArgumentException("History commit marker prefix is truncated"); + } + ByteBuffer buffer = ByteBuffer.wrap(prefix); + if (buffer.getInt() != MAGIC || buffer.getShort() != VERSION || buffer.getShort() != 0) { + throw new IllegalArgumentException("Unsupported history commit marker header"); + } + int length = buffer.getInt(); + if (length < HEADER_LENGTH + Integer.BYTES || length > MAX_MARKER_LENGTH) { + throw new IllegalArgumentException("History commit marker length is invalid"); + } + return length; + } + private static void writeBytes(DataOutputStream output, byte[] value) throws IOException { output.writeInt(value.length); output.write(value); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java index 3092e43ae11..8c8ae56a02c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java @@ -4,137 +4,248 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; -import java.nio.file.AtomicMoveNotSupportedException; -import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.UUID; -/** Atomic, directory-synced history visibility markers. */ +/** + * Append-only, checksummed commit log with constant resident state. + * + *

All records in one generation have the same descriptor participant set and therefore the + * same encoded length. Contiguous epochs can be addressed directly without retaining one object + * or creating one directory entry per block. + */ public final class HistoryCommitStore implements Closeable { - private static final String SUFFIX = ".commit"; + private static final String FILE_NAME = "commit.log"; private final Path directory; + private final Path logPath; private final HistoryCommitMarkerCodec codec; - private final DirectorySync directorySync; - private final List markers; - private final Map markersByEpoch = new HashMap<>(); - private HistoryCommitMarker uncertainMarker; + private final FileChannel channel; + private final DirectorySync postForceHook; + private HistoryCommitMarker head; + private long uncertainFrom = -1; + private long uncertainThrough = -1; + private long firstEpoch = -1; + private long recordCount; + private int recordLength; public HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec) throws IOException { - this(archiveDirectory, codec, HistorySegmentStore::syncDirectory); + this(archiveDirectory, codec, ignored -> { }); } HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec, - DirectorySync directorySync) throws IOException { + DirectorySync postForceHook) throws IOException { this.directory = archiveDirectory.resolve("commits"); + this.logPath = directory.resolve(FILE_NAME); this.codec = codec; - this.directorySync = directorySync; + this.postForceHook = postForceHook; Files.createDirectories(directory); - markers = scan(); - markers.forEach(marker -> markersByEpoch.put(marker.getMeta().getEpoch(), marker)); + boolean created = !Files.exists(logPath); + this.channel = FileChannel.open(logPath, StandardOpenOption.CREATE, StandardOpenOption.READ, + StandardOpenOption.WRITE); + if (created) { + HistorySegmentStore.syncDirectory(directory); + } + scanAndRepairTruncatedTail(); + channel.position(channel.size()); } public synchronized void commit(HistoryCommitMarker marker) throws IOException { - if (uncertainMarker != null) { - throw new IllegalStateException("A previous commit marker has uncertain durability"); - } - validateNext(head(), marker); - byte[] encoded = codec.encode(marker); - Path target = markerPath(marker.getMeta().getEpoch()); - if (Files.exists(target)) { - byte[] existing = Files.readAllBytes(target); - if (Arrays.equals(existing, encoded)) { - return; + commitAll(java.util.Collections.singletonList(marker)); + } + + public synchronized void commitAll(List batch) throws IOException { + if (batch.isEmpty()) { + return; + } + if (uncertainFrom >= 0) { + throw new IllegalStateException("A previous commit record has uncertain durability"); + } + HistoryCommitMarker previous = head; + List encodedBatch = new ArrayList<>(batch.size()); + int expectedLength = recordLength; + for (HistoryCommitMarker marker : batch) { + validateNext(previous, marker); + byte[] encoded = codec.encode(marker); + if (expectedLength != 0 && encoded.length != expectedLength) { + throw new IllegalArgumentException( + "Commit record length changed inside one archive generation"); } - throw new IllegalStateException("Conflicting history commit marker for epoch " - + marker.getMeta().getEpoch()); + expectedLength = encoded.length; + encodedBatch.add(encoded); + previous = marker; } - - Path temporary = directory.resolve(".tmp-" + marker.getMeta().getEpoch() + '-' - + UUID.randomUUID()); - try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE_NEW, - StandardOpenOption.WRITE)) { + long offset = channel.size(); + channel.position(offset); + for (byte[] encoded : encodedBatch) { writeFully(channel, ByteBuffer.wrap(encoded)); - channel.force(true); } + uncertainFrom = batch.get(0).getMeta().getEpoch(); + uncertainThrough = batch.get(batch.size() - 1).getMeta().getEpoch(); try { - Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE); - } catch (AtomicMoveNotSupportedException e) { - Files.deleteIfExists(temporary); - throw new IOException("Atomic commit-marker move is not supported", e); + channel.force(true); + postForceHook.sync(directory); + } catch (IOException failure) { + throw failure; + } + if (recordCount == 0) { + firstEpoch = batch.get(0).getMeta().getEpoch(); + recordLength = expectedLength; } - uncertainMarker = marker; - directorySync.sync(directory); - markers.add(marker); - markersByEpoch.put(marker.getMeta().getEpoch(), marker); - uncertainMarker = null; + recordCount += batch.size(); + head = batch.get(batch.size() - 1); + uncertainFrom = -1; + uncertainThrough = -1; } public synchronized void removeHead(BlockSnapshotMeta expected) throws IOException { - if (uncertainMarker != null) { - throw new IllegalStateException("Cannot revert a commit marker with uncertain durability"); + if (uncertainFrom >= 0) { + throw new IllegalStateException("Cannot revert a commit record with uncertain durability"); } - HistoryCommitMarker head = head(); if (head == null || !head.getMeta().equals(expected)) { throw new IllegalStateException("Only the committed history head can be reverted"); } - Files.delete(markerPath(expected.getEpoch())); - directorySync.sync(directory); - markers.remove(markers.size() - 1); - markersByEpoch.remove(expected.getEpoch()); + long newCount = recordCount - 1; + channel.truncate(newCount * (long) recordLength); + channel.force(true); + recordCount = newCount; + if (newCount == 0) { + head = null; + firstEpoch = -1; + recordLength = 0; + } else { + head = readOrdinal(newCount - 1); + } + channel.position(channel.size()); } public synchronized HistoryCommitMarker head() { - return markers.isEmpty() ? null : markers.get(markers.size() - 1); + return head; + } + + public synchronized long size() { + return recordCount; } + public synchronized long firstEpoch() { + return firstEpoch; + } + + /** Materializes the committed prefix. Do not use this method in the scale ingestion path. */ public synchronized List getMarkers() { - return new ArrayList<>(markers); + if (recordCount > Integer.MAX_VALUE) { + throw new IllegalStateException("Commit prefix is too large to materialize"); + } + List markers = new ArrayList<>((int) recordCount); + try { + for (long ordinal = 0; ordinal < recordCount; ordinal++) { + markers.add(readOrdinal(ordinal)); + } + return markers; + } catch (IOException failure) { + throw new ArchivePersistenceException("Failed to materialize commit prefix", failure); + } } public synchronized HistoryCommitMarker get(long epoch) { - return markersByEpoch.get(epoch); + if (recordCount == 0 || epoch < firstEpoch || epoch - firstEpoch >= recordCount) { + return null; + } + try { + return readOrdinal(epoch - firstEpoch); + } catch (IOException failure) { + throw new ArchivePersistenceException("Failed to read history commit epoch " + epoch, + failure); + } } public synchronized boolean mayContain(long epoch) { - return markersByEpoch.containsKey(epoch) - || (uncertainMarker != null && uncertainMarker.getMeta().getEpoch() == epoch); + return get(epoch) != null + || (uncertainFrom >= 0 && epoch >= uncertainFrom && epoch <= uncertainThrough); } - private List scan() throws IOException { - List paths = new ArrayList<>(); - try (DirectoryStream stream = Files.newDirectoryStream(directory, "*" + SUFFIX)) { - for (Path path : stream) { - paths.add(path); - } - } - paths.sort(Comparator.comparingLong(HistoryCommitStore::parseEpoch)); - List decoded = new ArrayList<>(); + Path getLogPath() { + return logPath; + } + + private void scanAndRepairTruncatedTail() throws IOException { + long offset = 0; HistoryCommitMarker previous = null; - for (Path path : paths) { - HistoryCommitMarker marker = codec.decode(Files.readAllBytes(path)); - if (parseEpoch(path) != marker.getMeta().getEpoch()) { - throw new IllegalStateException("Commit marker filename/epoch mismatch: " + path); + int expectedLength = 0; + long count = 0; + long size = channel.size(); + while (offset < size) { + long remaining = size - offset; + if (remaining < HistoryCommitMarkerCodec.HEADER_LENGTH) { + truncateTail(offset); + size = offset; + break; + } + byte[] prefix = read(offset, HistoryCommitMarkerCodec.HEADER_LENGTH); + int length; + try { + length = codec.recordLength(prefix); + } catch (IllegalArgumentException invalidHeader) { + throw new ArchivePersistenceException( + "Committed history log contains an invalid record header at " + offset, + invalidHeader); + } + if (length > remaining) { + truncateTail(offset); + size = offset; + break; + } + if (expectedLength != 0 && length != expectedLength) { + throw new ArchivePersistenceException( + "Commit record length changes inside one archive generation"); + } + HistoryCommitMarker marker; + try { + marker = codec.decode(read(offset, length)); + validateNext(previous, marker); + } catch (IllegalArgumentException invalidRecord) { + throw new ArchivePersistenceException( + "Committed history log contains an invalid record at " + offset, invalidRecord); + } + if (count == 0) { + firstEpoch = marker.getMeta().getEpoch(); + expectedLength = length; } - validateNext(previous, marker); - decoded.add(marker); previous = marker; + count++; + offset += length; } - return decoded; + recordLength = expectedLength; + recordCount = count; + head = previous; } - private void validateNext(HistoryCommitMarker previous, HistoryCommitMarker current) { + private void truncateTail(long offset) throws IOException { + channel.truncate(offset); + channel.force(true); + } + + private HistoryCommitMarker readOrdinal(long ordinal) throws IOException { + if (ordinal < 0 || ordinal >= recordCount) { + throw new IllegalArgumentException("Commit ordinal is outside the committed prefix"); + } + return codec.decode(read(ordinal * (long) recordLength, recordLength)); + } + + private byte[] read(long offset, int length) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(length); + channel.position(offset); + readFully(channel, buffer); + return buffer.array(); + } + + private static void validateNext(HistoryCommitMarker previous, + HistoryCommitMarker current) { if (previous == null) { if (current.getPreviousEpoch() >= current.getMeta().getEpoch()) { throw new IllegalArgumentException("Invalid base commit marker previous epoch"); @@ -144,36 +255,32 @@ private void validateNext(HistoryCommitMarker previous, HistoryCommitMarker curr if (current.getMeta().getEpoch() != previous.getMeta().getEpoch() + 1 || current.getPreviousEpoch() != previous.getMeta().getEpoch() || current.getMeta().getBlockNumber() != previous.getMeta().getBlockNumber() + 1 - || !Arrays.equals(current.getMeta().getParentHash(), + || !java.util.Arrays.equals(current.getMeta().getParentHash(), previous.getMeta().getBlockHash())) { throw new IllegalArgumentException("Non-contiguous history commit marker"); } } - private Path markerPath(long epoch) { - return directory.resolve(String.format("%020d%s", epoch, SUFFIX)); - } - - private static long parseEpoch(Path path) { - String name = path.getFileName().toString(); - try { - return Long.parseLong(name.substring(0, name.length() - SUFFIX.length())); - } catch (RuntimeException e) { - throw new IllegalArgumentException("Invalid history commit marker name: " + name, e); + private static void writeFully(FileChannel target, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + target.write(buffer); } } - private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException { + private static void readFully(FileChannel source, ByteBuffer buffer) throws IOException { while (buffer.hasRemaining()) { - channel.write(buffer); + if (source.read(buffer) < 0) { + throw new IOException("Unexpected end of history commit log"); + } } } @Override - public void close() { - // Marker files do not keep open resources. + public synchronized void close() throws IOException { + channel.close(); } + @FunctionalInterface interface DirectorySync { void sync(Path directory) throws IOException; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java index 36e809b312e..3c485849b61 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java @@ -11,7 +11,6 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; /** Append-only authoritative {@code state_history.idx}. */ @@ -42,9 +41,8 @@ public synchronized HistoryIndexLocation append(HistoryIndexRecord record) throw if (scanResult.getInvalidTailOffset() != null) { throw new IllegalStateException("History index has an invalid tail"); } - if (!scanResult.getRecords().isEmpty()) { - validateContinuity(scanResult.getRecords().get(scanResult.getRecords().size() - 1) - .getRecord().getMeta(), record.getMeta()); + if (scanResult.getHead() != null) { + validateContinuity(scanResult.getHead().getRecord().getMeta(), record.getMeta()); } byte[] encoded = codec.encode(record); long offset = channel.size(); @@ -52,9 +50,8 @@ public synchronized HistoryIndexLocation append(HistoryIndexRecord record) throw writeFully(channel, ByteBuffer.wrap(encoded)); HistoryIndexLocation location = new HistoryIndexLocation(offset, encoded.length, sha256(encoded)); - List records = new ArrayList<>(scanResult.getRecords()); - records.add(new ScannedIndexRecord(record, location)); - scanResult = new ScanResult(records, null, null); + scanResult = new ScanResult(scanResult.getRecordCount() + 1, + new ScannedIndexRecord(record, location), null, null); return location; } @@ -105,7 +102,8 @@ public synchronized void truncateAfter(HistoryIndexLocation last) throws IOExcep } private ScanResult scan() throws IOException { - List records = new ArrayList<>(); + long recordCount = 0; + ScannedIndexRecord head = null; Long invalidOffset = null; String invalidReason = null; long offset = 0; @@ -148,11 +146,12 @@ record = codec.decode(encoded); } HistoryIndexLocation location = new HistoryIndexLocation(offset, recordLength, sha256(encoded)); - records.add(new ScannedIndexRecord(record, location)); + head = new ScannedIndexRecord(record, location); + recordCount++; previous = record.getMeta(); offset += recordLength; } - return new ScanResult(records, invalidOffset, invalidReason); + return new ScanResult(recordCount, head, invalidOffset, invalidReason); } private void validateContinuity(BlockSnapshotMeta previous, BlockSnapshotMeta current) { @@ -212,19 +211,25 @@ public HistoryIndexLocation getLocation() { } public static final class ScanResult { - private final List records; + private final long recordCount; + private final ScannedIndexRecord head; private final Long invalidTailOffset; private final String invalidReason; - private ScanResult(List records, Long invalidTailOffset, + private ScanResult(long recordCount, ScannedIndexRecord head, Long invalidTailOffset, String invalidReason) { - this.records = Collections.unmodifiableList(new ArrayList<>(records)); + this.recordCount = recordCount; + this.head = head; this.invalidTailOffset = invalidTailOffset; this.invalidReason = invalidReason; } - public List getRecords() { - return records; + public long getRecordCount() { + return recordCount; + } + + public ScannedIndexRecord getHead() { + return head; } public Long getInvalidTailOffset() { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java index 131e05b2a76..f45dc443252 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java @@ -12,7 +12,6 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -47,9 +46,8 @@ public synchronized HistoryLocation append(BlockReverseDiff diff) throws IOExcep if (scanResult.getInvalidTail() != null) { throw new IllegalStateException("History has an invalid tail which must be truncated first"); } - if (!scanResult.getRecords().isEmpty()) { - validateContinuity(scanResult.getRecords().get(scanResult.getRecords().size() - 1) - .getDiff().getMeta(), diff.getMeta()); + if (scanResult.getHead() != null) { + validateContinuity(scanResult.getHead().getDiff().getMeta(), diff.getMeta()); } byte[] record = codec.encode(diff); long offset = appendChannel.size(); @@ -60,9 +58,8 @@ public synchronized HistoryLocation append(BlockReverseDiff diff) throws IOExcep appendChannel.position(offset); writeFully(appendChannel, ByteBuffer.wrap(record)); HistoryLocation location = location(appendSegmentId, offset, record); - List records = new ArrayList<>(scanResult.getRecords()); - records.add(new ScannedRecord(diff, location)); - scanResult = new ScanResult(records, null); + scanResult = new ScanResult(scanResult.getRecordCount() + 1, + new ScannedRecord(diff, location), null); return location; } @@ -129,7 +126,8 @@ public synchronized void truncateAfter(HistoryLocation last) throws IOException } private ScanResult scan() throws IOException { - List records = new ArrayList<>(); + long recordCount = 0; + ScannedRecord head = null; InvalidTail invalidTail = null; BlockSnapshotMeta previous = null; List segments = listSegments(); @@ -137,7 +135,7 @@ private ScanResult scan() throws IOException { for (Path segment : segments) { int segmentId = parseSegmentId(segment); if (segmentId != expectedSegmentId) { - return new ScanResult(records, new InvalidTail(segmentId, 0, + return new ScanResult(recordCount, head, new InvalidTail(segmentId, 0, "non-contiguous segment id")); } expectedSegmentId++; @@ -176,7 +174,8 @@ private ScanResult scan() throws IOException { break; } HistoryLocation location = location(segmentId, offset, record); - records.add(new ScannedRecord(diff, location)); + head = new ScannedRecord(diff, location); + recordCount++; previous = diff.getMeta(); offset += recordLength; } @@ -185,7 +184,7 @@ private ScanResult scan() throws IOException { break; } } - return new ScanResult(records, invalidTail); + return new ScanResult(recordCount, head, invalidTail); } private void validateContinuity(BlockSnapshotMeta previous, BlockSnapshotMeta current) { @@ -355,16 +354,22 @@ public String getReason() { } public static final class ScanResult { - private final List records; + private final long recordCount; + private final ScannedRecord head; private final InvalidTail invalidTail; - private ScanResult(List records, InvalidTail invalidTail) { - this.records = Collections.unmodifiableList(new ArrayList<>(records)); + private ScanResult(long recordCount, ScannedRecord head, InvalidTail invalidTail) { + this.recordCount = recordCount; + this.head = head; this.invalidTail = invalidTail; } - public List getRecords() { - return records; + public long getRecordCount() { + return recordCount; + } + + public ScannedRecord getHead() { + return head; } public InvalidTail getInvalidTail() { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexCatalog.java b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexCatalog.java new file mode 100644 index 00000000000..a1654663417 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexCatalog.java @@ -0,0 +1,33 @@ +package org.tron.core.db2.archive; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +/** Atomic publication point for immutable serving-key-index generations. */ +public final class ServingKeyIndexCatalog { + + private final AtomicReference current; + + public ServingKeyIndexCatalog(ServingKeyIndexGeneration initial) { + current = new AtomicReference<>(Objects.requireNonNull(initial, "initial")); + } + + /** Pins the current immutable generation by strong reference. */ + public ServingKeyIndexGeneration pin() { + return current.get(); + } + + /** Publishes only if the generation used as the build base is still current. */ + public boolean publish(ServingKeyIndexGeneration expected, + ServingKeyIndexGeneration replacement) { + Objects.requireNonNull(expected, "expected"); + Objects.requireNonNull(replacement, "replacement"); + if (replacement.getIndexedFrom() != expected.getIndexedFrom()) { + throw new IllegalArgumentException("Serving index replacement changes the coverage base"); + } + if (replacement.getIndexedThrough() < expected.getIndexedThrough()) { + throw new IllegalArgumentException("Serving index replacement regresses the watermark"); + } + return current.compareAndSet(expected, replacement); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java new file mode 100644 index 00000000000..d0357504c35 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java @@ -0,0 +1,599 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalLong; + +/** + * Immutable backend-neutral prototype of one derived serving-key-index generation. + * + *

This class deliberately defines no persistent page encoding. A production LSM backend can + * preserve this exact-key, committed-prefix and coverage contract after H1 format approval. + */ +public final class ServingKeyIndexGeneration { + + private final String generationId; + private final long indexedFrom; + private final long indexedThrough; + private final byte[] headHash; + private final byte[] authoritativePrefixDigest; + private final IndexLayout layout; + private final Map changes; + private final Map storeCoverage; + + private ServingKeyIndexGeneration(String generationId, long indexedFrom, long indexedThrough, + byte[] headHash, byte[] authoritativePrefixDigest, IndexLayout layout, + Map changes, Map storeCoverage) { + this.generationId = generationId; + this.indexedFrom = indexedFrom; + this.indexedThrough = indexedThrough; + this.headHash = Arrays.copyOf(headHash, headHash.length); + this.authoritativePrefixDigest = Arrays.copyOf(authoritativePrefixDigest, + authoritativePrefixDigest.length); + this.layout = layout; + this.changes = Collections.unmodifiableMap(new HashMap<>(changes)); + this.storeCoverage = Collections.unmodifiableMap(new HashMap<>(storeCoverage)); + } + + /** Rebuilds a complete immutable generation from commit-marker-proven index records. */ + public static ServingKeyIndexGeneration rebuild(String generationId, long baseEpoch, + byte[] baseHash, List committed, + AuthoritativeIndexReader reader) throws IOException { + return rebuild(generationId, baseEpoch, baseHash, committed, reader, + IndexLayout.prototypeDefaults()); + } + + /** Rebuilds using an explicit prototype layout without defining a persistent page ABI. */ + public static ServingKeyIndexGeneration rebuild(String generationId, long baseEpoch, + byte[] baseHash, List committed, + AuthoritativeIndexReader reader, IndexLayout layout) throws IOException { + return rebuild(generationId, baseEpoch, baseHash, committed, reader, null, layout); + } + + /** Rebuilds against an explicit descriptor participant set, including an empty prefix. */ + public static ServingKeyIndexGeneration rebuild(String generationId, long baseEpoch, + byte[] baseHash, List committed, + AuthoritativeIndexReader reader, List expectedParticipatingDatabases, + IndexLayout layout) throws IOException { + if (generationId == null || generationId.isEmpty()) { + throw new IllegalArgumentException("generationId must not be empty"); + } + if (baseEpoch < 0) { + throw new IllegalArgumentException("baseEpoch must not be negative"); + } + requireHash(baseHash, "baseHash"); + Objects.requireNonNull(committed, "committed"); + Objects.requireNonNull(reader, "reader"); + Objects.requireNonNull(layout, "layout"); + + Map> mutable = new HashMap<>(); + MessageDigest sourceDigest = sha256(); + updateLong(sourceDigest, baseEpoch); + sourceDigest.update(baseHash); + long previousEpoch = baseEpoch; + long previousBlock = baseEpoch; + byte[] previousHash = Arrays.copyOf(baseHash, baseHash.length); + List participatingDatabases = expectedParticipatingDatabases == null ? null + : sortedParticipants(expectedParticipatingDatabases); + if (participatingDatabases != null) { + updateParticipantDigest(sourceDigest, participatingDatabases); + } + + for (HistoryCommitMarker marker : committed) { + Objects.requireNonNull(marker, "committed marker"); + BlockSnapshotMeta meta = marker.getMeta(); + if (marker.getPreviousEpoch() != previousEpoch + || meta.getEpoch() != previousEpoch + 1 + || meta.getBlockNumber() != previousBlock + 1 + || meta.getEpoch() != meta.getBlockNumber() + || !Arrays.equals(meta.getParentHash(), previousHash)) { + throw new IllegalArgumentException("Serving index source commit prefix is not contiguous"); + } + if (participatingDatabases == null) { + participatingDatabases = marker.getDatabases(); + validateParticipantSet(participatingDatabases); + updateParticipantDigest(sourceDigest, participatingDatabases); + } else if (!participatingDatabases.equals(marker.getDatabases())) { + throw new IllegalArgumentException( + "Serving index source participant set changes inside one generation"); + } + + HistoryIndexRecord record = reader.read(marker.getIndexLocation()); + validateMarker(marker, record, participatingDatabases); + addRecord(mutable, record); + updateSourceDigest(sourceDigest, marker); + previousEpoch = meta.getEpoch(); + previousBlock = meta.getBlockNumber(); + previousHash = meta.getBlockHash(); + } + + Map immutable = new HashMap<>(); + mutable.forEach((key, blocks) -> { + immutable.put(key, KeyChangeIndex.from(blocks, layout)); + }); + byte[] prefixDigest = sourceDigest.digest(); + Map coverage = new HashMap<>(); + if (participatingDatabases != null) { + for (String database : participatingDatabases) { + coverage.put(database, new StoreCoverage(database, baseEpoch, previousEpoch, + previousHash, prefixDigest)); + } + } + return new ServingKeyIndexGeneration(generationId, baseEpoch, previousEpoch, previousHash, + prefixDigest, layout, immutable, coverage); + } + + /** Returns the first changed block in {@code (targetBlock, upperBound]}. */ + public OptionalLong firstChangeAfter(String dbName, byte[] rawKey, long targetBlock, + long upperBound) { + Objects.requireNonNull(dbName, "dbName"); + Objects.requireNonNull(rawKey, "rawKey"); + requireStoreCoverage(dbName, targetBlock, upperBound); + KeyChangeIndex index = changes.get(new KeyIdentity(dbName, rawKey)); + return index == null ? OptionalLong.empty() + : index.firstChangeAfter(targetBlock, upperBound); + } + + /** + * Returns sorted exact keys in one database which changed in {@code (targetBlock, upperBound]}. + * + *

The in-memory prototype scans all key metadata. A production ordered LSM must preserve the + * result and budget contract without materializing the entire database keyspace. + */ + public List changesInRange(String dbName, byte[] lowerInclusive, + byte[] upperExclusive, long targetBlock, long upperBound, int maxChangedKeys) { + Objects.requireNonNull(dbName, "dbName"); + Objects.requireNonNull(lowerInclusive, "lowerInclusive"); + if (maxChangedKeys <= 0) { + throw new IllegalArgumentException("maxChangedKeys must be positive"); + } + if (upperExclusive != null + && BlockReverseDiff.compareUnsigned(lowerInclusive, upperExclusive) > 0) { + throw new IllegalArgumentException("lowerInclusive must not exceed upperExclusive"); + } + requireStoreCoverage(dbName, targetBlock, upperBound); + List result = new ArrayList<>(); + for (Map.Entry entry : changes.entrySet()) { + KeyIdentity identity = entry.getKey(); + if (!identity.matchesDatabase(dbName) || !inRange(identity.rawKey, lowerInclusive, + upperExclusive)) { + continue; + } + OptionalLong first = entry.getValue().firstChangeAfter(targetBlock, upperBound); + if (!first.isPresent()) { + continue; + } + if (result.size() == maxChangedKeys) { + throw new ArchiveQueryLimitExceededException("changed-key budget exceeded"); + } + result.add(new ChangedKey(identity.rawKey, first.getAsLong())); + } + result.sort((left, right) -> BlockReverseDiff.compareUnsigned(left.key, right.key)); + return Collections.unmodifiableList(result); + } + + private void validateCoverage(long targetBlock, long upperBound) { + if (targetBlock < indexedFrom || upperBound > indexedThrough || targetBlock > upperBound) { + throw new IllegalArgumentException("Query range is outside serving index coverage"); + } + } + + private void requireStoreCoverage(String dbName, long targetBlock, long upperBound) { + validateCoverage(targetBlock, upperBound); + StoreCoverage coverage = storeCoverage.get(dbName); + if (coverage == null || !coverage.covers(targetBlock, upperBound)) { + throw new IllegalArgumentException("Database is outside serving index coverage: " + dbName); + } + } + + private static boolean inRange(byte[] key, byte[] lowerInclusive, byte[] upperExclusive) { + return BlockReverseDiff.compareUnsigned(key, lowerInclusive) >= 0 + && (upperExclusive == null + || BlockReverseDiff.compareUnsigned(key, upperExclusive) < 0); + } + + private static OptionalLong firstChange(long[] blocks, long targetBlock, long upperBound) { + int low = 0; + int high = blocks.length; + while (low < high) { + int middle = (low + high) >>> 1; + if (blocks[middle] <= targetBlock) { + low = middle + 1; + } else { + high = middle; + } + } + return low < blocks.length && blocks[low] <= upperBound + ? OptionalLong.of(blocks[low]) : OptionalLong.empty(); + } + + public String getGenerationId() { + return generationId; + } + + public long getIndexedFrom() { + return indexedFrom; + } + + public long getIndexedThrough() { + return indexedThrough; + } + + public byte[] getHeadHash() { + return Arrays.copyOf(headHash, headHash.length); + } + + public byte[] getAuthoritativePrefixDigest() { + return Arrays.copyOf(authoritativePrefixDigest, authoritativePrefixDigest.length); + } + + public IndexLayout getLayout() { + return layout; + } + + public Optional getStoreCoverage(String dbName) { + Objects.requireNonNull(dbName, "dbName"); + return Optional.ofNullable(storeCoverage.get(dbName)); + } + + public int getKeyMetadataCount() { + return changes.size(); + } + + public int getInlineKeyCount() { + return (int) changes.values().stream().filter(KeyChangeIndex::isInline).count(); + } + + public int getPagedKeyCount() { + return getKeyMetadataCount() - getInlineKeyCount(); + } + + public int getEpochPageCount() { + return changes.values().stream().mapToInt(KeyChangeIndex::getPageCount).sum(); + } + + public static final class ChangedKey { + private final byte[] key; + private final long firstChangeBlock; + + private ChangedKey(byte[] key, long firstChangeBlock) { + this.key = Arrays.copyOf(key, key.length); + this.firstChangeBlock = firstChangeBlock; + } + + public byte[] getKey() { + return Arrays.copyOf(key, key.length); + } + + public long getFirstChangeBlock() { + return firstChangeBlock; + } + } + + private static void addRecord(Map> changes, + HistoryIndexRecord record) { + String previousDb = null; + for (HistoryIndexRecord.KeyGroup group : record.getGroups()) { + if (previousDb != null && previousDb.compareTo(group.getDbName()) >= 0) { + throw new IllegalArgumentException("Serving index database groups are not sorted"); + } + previousDb = group.getDbName(); + byte[] previousKey = null; + for (byte[] key : group.getKeys()) { + if (previousKey != null && BlockReverseDiff.compareUnsigned(previousKey, key) >= 0) { + throw new IllegalArgumentException("Serving index keys are not sorted"); + } + previousKey = key; + List blocks = changes.computeIfAbsent(new KeyIdentity(group.getDbName(), key), + ignored -> new ArrayList<>()); + long block = record.getMeta().getBlockNumber(); + if (!blocks.isEmpty() && blocks.get(blocks.size() - 1) >= block) { + throw new IllegalArgumentException("Serving index changes are not strictly increasing"); + } + blocks.add(block); + } + } + } + + private static void validateMarker(HistoryCommitMarker marker, HistoryIndexRecord record, + List participatingDatabases) { + if (record == null || !marker.getMeta().equals(record.getMeta()) + || !same(marker.getHistoryLocation(), record.getHistoryLocation())) { + throw new IllegalArgumentException( + "Commit marker does not match authoritative history index record"); + } + for (HistoryIndexRecord.KeyGroup group : record.getGroups()) { + if (Collections.binarySearch(participatingDatabases, group.getDbName()) < 0) { + throw new IllegalArgumentException( + "History index contains a database outside the participant set"); + } + } + } + + private static boolean same(HistoryLocation left, HistoryLocation right) { + return left.getSegmentId() == right.getSegmentId() + && left.getOffset() == right.getOffset() + && left.getRecordLength() == right.getRecordLength() + && left.getBodyChecksum() == right.getBodyChecksum() + && Arrays.equals(left.getBodyDigest(), right.getBodyDigest()); + } + + private static void updateSourceDigest(MessageDigest digest, HistoryCommitMarker marker) { + updateLong(digest, marker.getMeta().getEpoch()); + updateLong(digest, marker.getMeta().getBlockNumber()); + digest.update(marker.getMeta().getBlockHash()); + digest.update(marker.getMeta().getParentHash()); + updateLong(digest, marker.getIndexLocation().getOffset()); + updateLong(digest, marker.getIndexLocation().getRecordLength()); + digest.update(marker.getIndexLocation().getDigest()); + digest.update(marker.getHistoryLocation().getBodyDigest()); + } + + private static void updateParticipantDigest(MessageDigest digest, List databases) { + updateLong(digest, databases.size()); + for (String database : databases) { + byte[] encoded = database.getBytes(StandardCharsets.UTF_8); + updateLong(digest, encoded.length); + digest.update(encoded); + } + } + + private static void validateParticipantSet(List databases) { + if (databases.isEmpty()) { + throw new IllegalArgumentException("Serving index participant set must not be empty"); + } + String previous = null; + for (String database : databases) { + if (database == null || database.isEmpty() + || (previous != null && previous.compareTo(database) >= 0)) { + throw new IllegalArgumentException( + "Serving index participant set must be non-empty, unique, and sorted"); + } + previous = database; + } + } + + private static List sortedParticipants(List databases) { + List result = new ArrayList<>(Objects.requireNonNull(databases, "databases")); + Collections.sort(result); + validateParticipantSet(result); + return Collections.unmodifiableList(result); + } + + private static void updateLong(MessageDigest digest, long value) { + digest.update(ByteBuffer.allocate(Long.BYTES).putLong(value).array()); + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is unavailable", e); + } + } + + private static void requireHash(byte[] hash, String name) { + if (hash == null || hash.length != 32) { + throw new IllegalArgumentException(name + " must be exactly 32 bytes"); + } + } + + @FunctionalInterface + public interface AuthoritativeIndexReader { + HistoryIndexRecord read(HistoryIndexLocation location) throws IOException; + } + + /** Tunable in-memory prototype layout; this is not a persistent format contract. */ + public static final class IndexLayout { + private static final int DEFAULT_INLINE_EPOCH_LIMIT = 4; + private static final int DEFAULT_MAX_EPOCHS_PER_PAGE = 512; + + private final int inlineEpochLimit; + private final int maxEpochsPerPage; + + public IndexLayout(int inlineEpochLimit, int maxEpochsPerPage) { + if (inlineEpochLimit <= 0 || maxEpochsPerPage <= 0) { + throw new IllegalArgumentException("Serving index layout limits must be positive"); + } + this.inlineEpochLimit = inlineEpochLimit; + this.maxEpochsPerPage = maxEpochsPerPage; + } + + public static IndexLayout prototypeDefaults() { + return new IndexLayout(DEFAULT_INLINE_EPOCH_LIMIT, DEFAULT_MAX_EPOCHS_PER_PAGE); + } + + public int getInlineEpochLimit() { + return inlineEpochLimit; + } + + public int getMaxEpochsPerPage() { + return maxEpochsPerPage; + } + } + + /** Completeness identity for one Store in this immutable serving generation. */ + public static final class StoreCoverage { + private final String dbName; + private final long indexedFrom; + private final long indexedThrough; + private final byte[] headHash; + private final byte[] authoritativePrefixDigest; + + private StoreCoverage(String dbName, long indexedFrom, long indexedThrough, + byte[] headHash, byte[] authoritativePrefixDigest) { + this.dbName = dbName; + this.indexedFrom = indexedFrom; + this.indexedThrough = indexedThrough; + this.headHash = Arrays.copyOf(headHash, headHash.length); + this.authoritativePrefixDigest = Arrays.copyOf(authoritativePrefixDigest, + authoritativePrefixDigest.length); + } + + public String getDbName() { + return dbName; + } + + public long getIndexedFrom() { + return indexedFrom; + } + + public long getIndexedThrough() { + return indexedThrough; + } + + public byte[] getHeadHash() { + return Arrays.copyOf(headHash, headHash.length); + } + + public byte[] getAuthoritativePrefixDigest() { + return Arrays.copyOf(authoritativePrefixDigest, + authoritativePrefixDigest.length); + } + + private boolean covers(long targetBlock, long upperBound) { + return targetBlock >= indexedFrom && upperBound <= indexedThrough; + } + } + + private static final class KeyChangeIndex { + private final long firstChangedEpoch; + private final long lastChangedEpoch; + private final long[] inlineEpochs; + private final List pages; + + private KeyChangeIndex(long firstChangedEpoch, long lastChangedEpoch, long[] inlineEpochs, + List pages) { + this.firstChangedEpoch = firstChangedEpoch; + this.lastChangedEpoch = lastChangedEpoch; + this.inlineEpochs = inlineEpochs; + this.pages = pages; + } + + private static KeyChangeIndex from(List epochs, IndexLayout layout) { + if (epochs.isEmpty()) { + throw new IllegalArgumentException("Serving key change list must not be empty"); + } + long first = epochs.get(0); + long last = epochs.get(epochs.size() - 1); + if (epochs.size() <= layout.getInlineEpochLimit()) { + return new KeyChangeIndex(first, last, toArray(epochs), Collections.emptyList()); + } + List pages = new ArrayList<>(); + for (int start = 0; start < epochs.size(); start += layout.getMaxEpochsPerPage()) { + int end = Math.min(start + layout.getMaxEpochsPerPage(), epochs.size()); + pages.add(new EpochPage(toArray(epochs.subList(start, end)))); + } + return new KeyChangeIndex(first, last, null, + Collections.unmodifiableList(pages)); + } + + private OptionalLong firstChangeAfter(long targetBlock, long upperBound) { + if (lastChangedEpoch <= targetBlock || firstChangedEpoch > upperBound) { + return OptionalLong.empty(); + } + if (isInline()) { + return firstChange(inlineEpochs, targetBlock, upperBound); + } + int pageIndex = pageAtOrBefore(targetBlock); + OptionalLong changed = pages.get(pageIndex).firstChangeAfter(targetBlock, upperBound); + if (changed.isPresent() || pageIndex + 1 >= pages.size()) { + return changed; + } + return pages.get(pageIndex + 1).firstChangeAfter(targetBlock, upperBound); + } + + private int pageAtOrBefore(long targetBlock) { + int low = 0; + int high = pages.size(); + while (low < high) { + int middle = (low + high) >>> 1; + if (pages.get(middle).baseEpoch <= targetBlock) { + low = middle + 1; + } else { + high = middle; + } + } + return Math.max(0, low - 1); + } + + private boolean isInline() { + return inlineEpochs != null; + } + + private int getPageCount() { + return pages.size(); + } + + private static long[] toArray(List epochs) { + long[] result = new long[epochs.size()]; + for (int i = 0; i < epochs.size(); i++) { + result[i] = epochs.get(i); + } + return result; + } + } + + private static final class EpochPage { + private final long baseEpoch; + private final long maxEpoch; + private final long[] epochs; + + private EpochPage(long[] epochs) { + this.epochs = epochs; + this.baseEpoch = epochs[0]; + this.maxEpoch = epochs[epochs.length - 1]; + } + + private OptionalLong firstChangeAfter(long targetBlock, long upperBound) { + if (maxEpoch <= targetBlock || baseEpoch > upperBound) { + return OptionalLong.empty(); + } + return firstChange(epochs, targetBlock, upperBound); + } + } + + private static final class KeyIdentity { + private final byte[] dbName; + private final byte[] rawKey; + private final int hashCode; + + private KeyIdentity(String dbName, byte[] rawKey) { + this.dbName = dbName.getBytes(StandardCharsets.UTF_8); + this.rawKey = Arrays.copyOf(rawKey, rawKey.length); + this.hashCode = 31 * Arrays.hashCode(this.dbName) + Arrays.hashCode(this.rawKey); + } + + private boolean matchesDatabase(String candidate) { + return Arrays.equals(dbName, candidate.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof KeyIdentity)) { + return false; + } + KeyIdentity that = (KeyIdentity) other; + return Arrays.equals(dbName, that.dbName) && Arrays.equals(rawKey, that.rawKey); + } + + @Override + public int hashCode() { + return hashCode; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java index 32764dc332c..b377689592c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java @@ -12,7 +12,7 @@ import java.util.Objects; import java.util.Set; import lombok.Getter; -import lombok.Setter; +import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.common.HashDB; import org.tron.core.db2.common.Key; @@ -26,9 +26,11 @@ public class SnapshotImpl extends AbstractSnapshot { protected Snapshot root; @Getter - @Setter private BlockSnapshotMeta blockSnapshotMeta; + @Getter + private BlockReverseDiff preparedArchiveBlock; + SnapshotImpl(Snapshot snapshot) { root = snapshot.getRoot(); synchronized (this) { @@ -42,6 +44,17 @@ public class SnapshotImpl extends AbstractSnapshot { } } + /** + * Publishes the immutable block identity and optional archive payload on this layer. + * + *

The caller performs all validation and materialization first, so this method is the + * non-throwing ownership-transfer point for a successfully committed block session. + */ + void attachArchiveBlock(BlockSnapshotMeta meta, BlockReverseDiff reverseDiff) { + blockSnapshotMeta = meta; + preparedArchiveBlock = reverseDiff; + } + @Override public byte[] get(byte[] key) { return get(this, key); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index 4ac6c3e7a17..dc9fabe5bfc 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -95,6 +95,8 @@ public class SnapshotManager implements RevokingDatabase { private OldValueCollector oldValueCollector; private BlockReverseDiffSink blockReverseDiffSink; + @Getter + private volatile long archiveReadableEpoch = -1; public SnapshotManager(String checkpointPath) { } @@ -247,12 +249,13 @@ public synchronized void commit(BlockSnapshotMeta meta) { throw new IllegalStateException( "Cannot bind block metadata to non-SnapshotImpl head: " + db.getDbName()); } - ((SnapshotImpl) head).setBlockSnapshotMeta(meta); } BlockReverseDiff reverseDiff = null; if (oldValueCollector != null) { - reverseDiff = oldValueCollector.collect(BlockChangeView.capture(meta, dbs)); + reverseDiff = Objects.requireNonNull( + oldValueCollector.collect(BlockChangeView.capture(meta, dbs)), + "archive collector returned null"); } dbs.forEach(db -> { @@ -261,8 +264,11 @@ public synchronized void commit(BlockSnapshotMeta meta) { } }); - if (reverseDiff != null) { - blockReverseDiffSink.accept(reverseDiff); + // All fallible work is complete. From here the prepared payload is owned by the block layer; + // fastPop/reorg can discard it without touching durable archive state. + for (Chainbase db : dbs) { + ((SnapshotImpl) db.getHead()).attachArchiveBlock(meta, + ArchiveStoreScope.isStateDatabase(db.getDbName()) ? reverseDiff : null); } --activeSession; } @@ -306,6 +312,10 @@ public synchronized void installArchiveCollector(OldValueCollector collector, blockReverseDiffSink = Objects.requireNonNull(sink, "sink"); } + public void markArchiveReadableThrough(long epoch) { + archiveReadableEpoch = epoch; + } + public synchronized void pop() { if (activeSession != 0) { throw new RevokingStoreIllegalStateException( @@ -317,28 +327,6 @@ public synchronized void pop() { String.format("there is not snapshot to be popped, current: %d", size)); } - if (blockReverseDiffSink != null) { - BlockSnapshotMeta meta = null; - for (Chainbase db : dbs) { - if (!ArchiveStoreScope.isStateDatabase(db.getDbName())) { - continue; - } - Snapshot head = db.getHead(); - if (Snapshot.isImpl(head)) { - BlockSnapshotMeta candidate = ((SnapshotImpl) head).getBlockSnapshotMeta(); - if (candidate != null && meta != null && !meta.equals(candidate)) { - throw new IllegalStateException("Mismatched block metadata while reverting snapshots"); - } - if (candidate != null) { - meta = candidate; - } - } - } - if (meta != null) { - blockReverseDiffSink.revert(meta); - } - } - disabled = true; try { @@ -465,7 +453,7 @@ public void flush() { if (shouldBeRefreshed()) { try { long start = System.currentTimeMillis(); - Long archiveEpoch = awaitArchiveHistoryForFlush(); + Long archiveEpoch = publishArchiveHistoryForFlush(); if (!isV2Open()) { deleteCheckpoint(); } @@ -474,6 +462,7 @@ public void flush() { long checkPointEnd = System.currentTimeMillis(); refresh(); if (archiveEpoch != null) { + archiveReadableEpoch = archiveEpoch; ((DurableBlockReverseDiffSink) blockReverseDiffSink).releaseThrough(archiveEpoch); } flushCount = 0; @@ -490,7 +479,7 @@ public void flush() { } } - private Long awaitArchiveHistoryForFlush() { + private Long publishArchiveHistoryForFlush() { if (oldValueCollector == null) { return null; } @@ -502,6 +491,7 @@ private Long awaitArchiveHistoryForFlush() { .findFirst() .orElseThrow(() -> new TronDBException("Archive mode has no registered state database")); Snapshot next = stateDatabase.getHead().getRoot(); + List prepared = new ArrayList<>(flushCount); BlockSnapshotMeta last = null; for (int i = 0; i < flushCount; i++) { next = next.getNext(); @@ -515,13 +505,22 @@ private Long awaitArchiveHistoryForFlush() { if (last != null && meta.getEpoch() != last.getEpoch() + 1) { throw new TronDBException("Archive flush range is not epoch-contiguous"); } + BlockReverseDiff reverseDiff = ((SnapshotImpl) next).getPreparedArchiveBlock(); + if (reverseDiff == null || !meta.equals(reverseDiff.getMeta())) { + throw new TronDBException( + "Archive flush range contains a layer without its matching prepared payload"); + } + prepared.add(reverseDiff); last = meta; } if (last == null) { return null; } try { - ((DurableBlockReverseDiffSink) blockReverseDiffSink).awaitCommitted(last.getEpoch()); + DurableBlockReverseDiffSink durableSink = + (DurableBlockReverseDiffSink) blockReverseDiffSink; + durableSink.acceptAll(prepared); + durableSink.awaitCommitted(last.getEpoch()); } catch (RuntimeException e) { throw new TronDBException("Archive history durability gate failed", e); } diff --git a/chainbase/src/main/java/org/tron/core/store/StorageRowKeyCodec.java b/chainbase/src/main/java/org/tron/core/store/StorageRowKeyCodec.java new file mode 100644 index 00000000000..8c29d7aa059 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/store/StorageRowKeyCodec.java @@ -0,0 +1,55 @@ +package org.tron.core.store; + +import static java.lang.System.arraycopy; + +import java.util.Objects; +import org.tron.common.crypto.Hash; +import org.tron.common.utils.ByteUtil; + +/** Canonical physical-key mapping shared by latest and historical contract storage reads. */ +public final class StorageRowKeyCodec { + + public static final int KEY_BYTES = 32; + private static final int HALF_KEY_BYTES = KEY_BYTES / 2; + + private StorageRowKeyCodec() { + } + + /** + * Maps a logical contract address and slot to the existing 32-byte storage-row key. + * + *

The mapping intentionally preserves the current truncated alias/collision semantics. + */ + public static byte[] physicalKey(byte[] address, byte[] logicalSlot, int contractVersion, + byte[] createTransactionHash) { + return physicalKeyFromAddressHash(addressHash(address, createTransactionHash), logicalSlot, + contractVersion); + } + + /** Returns the address-side hash used by the storage-row key mapping. */ + public static byte[] addressHash(byte[] address, byte[] createTransactionHash) { + Objects.requireNonNull(address, "address"); + if (ByteUtil.isNullOrZeroArray(createTransactionHash)) { + return Hash.sha3(address); + } + return Hash.sha3(ByteUtil.merge(address, createTransactionHash)); + } + + /** Maps a precomputed address hash and logical slot to the existing storage-row key. */ + public static byte[] physicalKeyFromAddressHash(byte[] addressHash, byte[] logicalSlot, + int contractVersion) { + Objects.requireNonNull(addressHash, "addressHash"); + Objects.requireNonNull(logicalSlot, "logicalSlot"); + if (addressHash.length != KEY_BYTES) { + throw new IllegalArgumentException("addressHash must be exactly 32 bytes"); + } + if (logicalSlot.length != KEY_BYTES) { + throw new IllegalArgumentException("logicalSlot must be exactly 32 bytes"); + } + byte[] transformedSlot = contractVersion == 1 ? Hash.sha3(logicalSlot) : logicalSlot; + byte[] physicalKey = new byte[KEY_BYTES]; + arraycopy(addressHash, 0, physicalKey, 0, HALF_KEY_BYTES); + arraycopy(transformedSlot, HALF_KEY_BYTES, physicalKey, HALF_KEY_BYTES, HALF_KEY_BYTES); + return physicalKey; + } +} diff --git a/common/src/main/java/org/tron/core/config/args/Storage.java b/common/src/main/java/org/tron/core/config/args/Storage.java index 16dd8295be1..b0f95c38348 100644 --- a/common/src/main/java/org/tron/core/config/args/Storage.java +++ b/common/src/main/java/org/tron/core/config/args/Storage.java @@ -85,6 +85,22 @@ public class Storage { @Setter private boolean checkpointSync; + @Getter + @Setter + private boolean stateArchiveEnabled; + + @Getter + @Setter + private String stateArchiveDirectory; + + @Getter + @Setter + private long stateArchiveMaxSegmentSize; + + @Getter + @Setter + private int stateArchiveQueueCapacity; + private Options defaultDbOptions; @Getter diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index 2c6c3e60a41..2fbbbb1e54a 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -27,6 +27,7 @@ public class StorageConfig { private BalanceConfig balance = new BalanceConfig(); private CheckpointConfig checkpoint = new CheckpointConfig(); private SnapshotConfig snapshot = new SnapshotConfig(); + private StateArchiveConfig stateArchive = new StateArchiveConfig(); private TxCacheConfig txCache = new TxCacheConfig(); // ConfigBeanFactory requires all bean fields present per item, so we parse manually. @Setter(lombok.AccessLevel.NONE) @@ -141,6 +142,34 @@ void postProcess() { } } + @Getter + @Setter + public static class StateArchiveConfig { + + private boolean enabled = false; + private String directory = "state-archive"; + private long maxSegmentSize = 1073741824L; + private int queueCapacity = 256; + + void postProcess() { + if (directory == null || directory.trim().isEmpty()) { + throw new IllegalArgumentException("stateArchive.directory must not be empty"); + } + if (maxSegmentSize <= BlockHistoryLimits.MIN_SEGMENT_SIZE) { + throw new IllegalArgumentException( + "stateArchive.maxSegmentSize must be greater than 64 MiB"); + } + if (queueCapacity <= 0 || queueCapacity > 65536) { + throw new IllegalArgumentException( + "stateArchive.queueCapacity must be in [1, 65536]"); + } + } + } + + private static final class BlockHistoryLimits { + private static final long MIN_SEGMENT_SIZE = 64L * 1024 * 1024; + } + @Getter @Setter public static class TxCacheConfig { @@ -184,6 +213,7 @@ public static StorageConfig fromConfig(Config config) { sc.dbSettings.postProcess(); sc.snapshot.postProcess(); + sc.stateArchive.postProcess(); sc.txCache.postProcess(); return sc; } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 25fc4832e55..45f08744dcc 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -132,6 +132,12 @@ storage { # Number of blocks flushed to db in each batch during node syncing. snapshot.maxFlushCount = 1 + # Experimental block-boundary state history. Disabled by default. + stateArchive.enabled = false + stateArchive.directory = "state-archive" + stateArchive.maxSegmentSize = 1073741824 # 1 GiB + stateArchive.queueCapacity = 256 + # Data root setting, for check data, currently only reward-vi is used. # merkleRoot = { # reward-vi = 9debcb9924055500aaae98cdee10501c5c39d4daa75800a996f4bdda73dbccd8 // main-net diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java index e3f1925a763..0efee8ade5c 100644 --- a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -56,6 +56,28 @@ public void testCheckpointDefaults() { assertTrue(sc.getCheckpoint().isSync()); } + @Test + public void testStateArchiveDefaultsAndOverrides() { + StorageConfig defaults = StorageConfig.fromConfig(withRef()); + assertFalse(defaults.getStateArchive().isEnabled()); + assertEquals("state-archive", defaults.getStateArchive().getDirectory()); + assertEquals(1073741824L, defaults.getStateArchive().getMaxSegmentSize()); + assertEquals(256, defaults.getStateArchive().getQueueCapacity()); + + StorageConfig configured = StorageConfig.fromConfig(withRef( + "storage.stateArchive { enabled = true, directory = archive-test, " + + "maxSegmentSize = 134217728, queueCapacity = 8 }")); + assertTrue(configured.getStateArchive().isEnabled()); + assertEquals("archive-test", configured.getStateArchive().getDirectory()); + assertEquals(134217728L, configured.getStateArchive().getMaxSegmentSize()); + assertEquals(8, configured.getStateArchive().getQueueCapacity()); + } + + @Test(expected = IllegalArgumentException.class) + public void testStateArchiveRejectsSmallSegments() { + StorageConfig.fromConfig(withRef("storage.stateArchive.maxSegmentSize = 1024")); + } + @Test public void testDbSettingsDefaults() { // These defaults must match develop's Args.initRocksDbSettings() fallbacks so that diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index ac54cb2b7ff..b99142e1d43 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -4432,6 +4432,30 @@ public BalanceContract.AccountBalanceResponse getAccountBalance( return builder.build(); } + /** Experimental state-archive balance query used for trace-oracle validation. */ + public BalanceContract.AccountBalanceResponse getAccountBalanceFromStateArchive( + BalanceContract.AccountBalanceRequest request) + throws ItemNotFoundException, BadItemException { + BalanceContract.AccountIdentifier accountIdentifier = request.getAccountIdentifier(); + checkAccountIdentifier(accountIdentifier); + BlockBalanceTrace.BlockIdentifier blockIdentifier = request.getBlockIdentifier(); + checkBlockIdentifier(blockIdentifier); + BlockId blockId = chainBaseManager.getBlockIndexStore().get(blockIdentifier.getNumber()); + if (!blockId.getByteString().equals(blockIdentifier.getHash())) { + throw new IllegalArgumentException("number and hash do not match"); + } + org.tron.core.db2.archive.HistoricalAccountBalanceReader.Result result = + dbManager.getArchiveAccountBalance(blockIdentifier.getNumber(), + accountIdentifier.getAddress().toByteArray()); + if (!result.isPresent()) { + throw new ItemNotFoundException("Account is absent at the requested block"); + } + return BalanceContract.AccountBalanceResponse.newBuilder() + .setBlockIdentifier(blockIdentifier) + .setBalance(result.getBalance()) + .build(); + } + public BalanceContract.BlockBalanceTrace getBlockBalance( BlockBalanceTrace.BlockIdentifier request) throws ItemNotFoundException, BadItemException { checkBlockIdentifier(request); diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 0bca242606e..facff23293f 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -217,6 +217,10 @@ private static void applyStorageConfig(StorageConfig sc) { // contractParse is set in applyConfigParams alongside event config, not here PARAMETER.storage.setCheckpointVersion(sc.getCheckpoint().getVersion()); PARAMETER.storage.setCheckpointSync(sc.getCheckpoint().isSync()); + PARAMETER.storage.setStateArchiveEnabled(sc.getStateArchive().isEnabled()); + PARAMETER.storage.setStateArchiveDirectory(sc.getStateArchive().getDirectory()); + PARAMETER.storage.setStateArchiveMaxSegmentSize(sc.getStateArchive().getMaxSegmentSize()); + PARAMETER.storage.setStateArchiveQueueCapacity(sc.getStateArchive().getQueueCapacity()); // estimatedTransactions / maxFlushCount clamping & validation run inside // TxCacheConfig.postProcess / SnapshotConfig.postProcess during bean load. @@ -1315,4 +1319,3 @@ private static Map getOptionGroup() { return optionGroupMap; } } - diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index f9a4047802d..ee03768c8ca 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -15,6 +15,8 @@ import com.google.common.primitives.Longs; import com.google.protobuf.ByteString; import io.prometheus.client.Histogram; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -113,7 +115,13 @@ import org.tron.core.db.api.MigrateTurkishKeyHelper; import org.tron.core.db.api.MoveAbiHelper; import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.AccountAssetArchiveProjector; +import org.tron.core.db2.archive.ArchiveHistoryWriter; +import org.tron.core.db2.archive.ArchiveStoreScope; +import org.tron.core.db2.archive.AsyncArchiveHistorySink; import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.HistoricalAccountBalanceReader; +import org.tron.core.db2.archive.SnapshotOldValueCollector; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.exception.AccountResourceInsufficientException; @@ -188,6 +196,8 @@ public class Manager { private static final int SLEEP_TIME_OUT = 50; private static final int TX_ID_CACHE_SIZE = 100_000; private static final int SLEEP_FOR_WAIT_LOCK = 10; + @Getter + private ArchiveHistoryWriter archiveHistoryWriter; private static final int NO_BLOCK_WAITING_LOCK = 0; private final int shieldedTransInPendingMaxCounts = Args.getInstance().getShieldedTransInPendingMaxCounts(); @@ -566,6 +576,8 @@ public void init() { // init liteFullNode initLiteNode(); + initStateArchive(); + long headNum = chainBaseManager.getDynamicPropertiesStore().getLatestBlockHeaderNumber(); logger.info("Current headNum is: {}.", headNum); boolean isLite = chainBaseManager.isLiteNode(); @@ -607,6 +619,70 @@ public void init() { maxFlushCount = CommonParameter.getInstance().getStorage().getMaxFlushCount(); } + private void initStateArchive() { + org.tron.core.config.args.Storage storage = Args.getInstance().getStorage(); + if (!storage.isStateArchiveEnabled()) { + return; + } + if (!(revokingStore instanceof SnapshotManager)) { + throw new IllegalStateException("State archive requires SnapshotManager"); + } + Path archiveDirectory = Paths.get(Args.getInstance().getOutputDirectory(), + storage.getStateArchiveDirectory()).normalize(); + try { + ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archiveDirectory, + storage.getStateArchiveMaxSegmentSize(), ArchiveStoreScope.getStateDatabases()); + BlockSnapshotMeta archiveHead = writer.committedHeadMeta(); + if (archiveHead != null + && (archiveHead.getBlockNumber() + != getDynamicPropertiesStore().getLatestBlockHeaderNumber() + || !Arrays.equals(archiveHead.getBlockHash(), + getDynamicPropertiesStore().getLatestBlockHeaderHash().getBytes()))) { + writer.close(); + throw new IllegalStateException( + "State archive committed head differs from the persisted state root"); + } + AsyncArchiveHistorySink sink = new AsyncArchiveHistorySink(writer, + storage.getStateArchiveQueueCapacity()); + AccountAssetArchiveProjector projector = new AccountAssetArchiveProjector( + chainBaseManager.getAccountAssetStore(), + () -> getDynamicPropertiesStore().supportAllowAccountAssetOptimization()); + ((SnapshotManager) revokingStore).installArchiveCollector( + new SnapshotOldValueCollector(projector), sink); + archiveHistoryWriter = writer; + if (archiveHead != null) { + ((SnapshotManager) revokingStore).markArchiveReadableThrough(archiveHead.getEpoch()); + } + logger.info("Experimental state archive enabled: directory={}, maxSegmentSize={}, queue={}", + archiveDirectory, storage.getStateArchiveMaxSegmentSize(), + storage.getStateArchiveQueueCapacity()); + } catch (java.io.IOException failure) { + throw new IllegalStateException("Failed to initialize experimental state archive", failure); + } + } + + public HistoricalAccountBalanceReader.Result getArchiveAccountBalance(long blockNumber, + byte[] address) throws ItemNotFoundException, BadItemException { + ArchiveHistoryWriter writer = archiveHistoryWriter; + if (writer == null) { + throw new IllegalStateException("Experimental state archive is disabled"); + } + BlockSnapshotMeta archiveHead = writer.committedHeadMeta(); + if (archiveHead == null + || ((SnapshotManager) revokingStore).getArchiveReadableEpoch() != archiveHead.getEpoch()) { + throw new IllegalStateException("State archive has no readable committed root"); + } + AccountCapsule rootAccount; + try { + rootAccount = chainBaseManager.getAccountStore().getFromRoot(address); + } catch (ItemNotFoundException missing) { + rootAccount = null; + } + org.tron.core.db2.archive.OldValue value = writer.readAccountAt(blockNumber, address, + rootAccount == null ? null : rootAccount.getData()); + return HistoricalAccountBalanceReader.decode(blockNumber, address, value); + } + /** * init genesis block. */ diff --git a/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java b/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java index 5a3b86cb396..e3096d78f54 100644 --- a/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java +++ b/framework/src/main/java/org/tron/core/services/http/FullNodeHttpApiService.java @@ -257,6 +257,8 @@ public class FullNodeHttpApiService extends HttpService { @Autowired private GetAccountBalanceServlet getAccountBalanceServlet; + @Autowired + private GetAccountBalanceFromArchiveServlet getAccountBalanceFromArchiveServlet; @Autowired private GetBlockBalanceServlet getBlockBalanceServlet; @@ -488,6 +490,8 @@ protected void addServlet(ServletContextHandler context) { context.addServlet(new ServletHolder(getAccountBalanceServlet), "/wallet/getaccountbalance"); + context.addServlet(new ServletHolder(getAccountBalanceFromArchiveServlet), + "/wallet/getaccountbalancefromarchive"); context.addServlet(new ServletHolder(getBlockBalanceServlet), "/wallet/getblockbalance"); context.addServlet(new ServletHolder(getBurnTrxServlet), "/wallet/getburntrx"); diff --git a/framework/src/main/java/org/tron/core/services/http/GetAccountBalanceFromArchiveServlet.java b/framework/src/main/java/org/tron/core/services/http/GetAccountBalanceFromArchiveServlet.java new file mode 100644 index 00000000000..50b6bcaf845 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/http/GetAccountBalanceFromArchiveServlet.java @@ -0,0 +1,32 @@ +package org.tron.core.services.http; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.tron.core.Wallet; +import org.tron.protos.contract.BalanceContract; + +/** Experimental single-purpose endpoint for archive-versus-trace balance validation. */ +@Component +@Slf4j(topic = "API") +public class GetAccountBalanceFromArchiveServlet extends RateLimiterServlet { + + @Autowired + private Wallet wallet; + + protected void doPost(HttpServletRequest request, HttpServletResponse response) { + try { + PostParams params = PostParams.getPostParams(request); + BalanceContract.AccountBalanceRequest.Builder builder = + BalanceContract.AccountBalanceRequest.newBuilder(); + JsonFormat.merge(params.getParams(), builder, params.isVisible()); + BalanceContract.AccountBalanceResponse reply = + wallet.getAccountBalanceFromStateArchive(builder.build()); + response.getWriter().println(JsonFormat.printToString(reply, params.isVisible())); + } catch (Exception failure) { + Util.processError(failure, response); + } + } +} diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 1176dd46311..48102c26395 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -37,6 +37,12 @@ storage { balance.history.lookup = false + # Experimental block-boundary state history. Keep disabled outside dedicated validation nodes. + stateArchive.enabled = false + stateArchive.directory = "state-archive" + stateArchive.maxSegmentSize = 1073741824 + stateArchive.queueCapacity = 256 + # If true, transaction cache initialization will be faster. Default: false txCache.initOptimization = true } diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java index 6aad551db81..bd93a002dae 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java @@ -1,10 +1,12 @@ package org.tron.core.db2.archive; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.google.protobuf.ByteString; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -18,6 +20,7 @@ import org.tron.core.db2.archive.ArchiveHistoryWriter.Stage; import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.protos.Protocol.Account; public class ArchiveHistoryWriterTest { @@ -109,18 +112,79 @@ public void rejectsNonContiguousCanonicalInput() throws Exception { } @Test - public void ignoresUncommittedTemporaryMarkerFiles() throws Exception { + public void persistsAccountSeekIndexAndRecoversItAcrossRestart() throws Exception { + Path archive = temporaryFolder.newFolder("account-index").toPath(); + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = 7; + BlockReverseDiff created = accountDiff(1, address, OldValue.absent()); + BlockReverseDiff changed = accountDiff(2, address, + OldValue.present(account(address, 10))); + BlockReverseDiff unchanged = new BlockReverseDiff(new BlockSnapshotMeta( + 3, 3, hash(3), hash(2), 9_000), Collections.emptyList()); + byte[] committedAccount = account(address, 20); + + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + writer.acceptAll(Arrays.asList(created, changed, unchanged)); + assertFalse(writer.readAccountAt(0, address, committedAccount).isPresent()); + assertEquals(10, Account.parseFrom( + writer.readAccountAt(1, address, committedAccount).getValue()).getBalance()); + assertEquals(20, Account.parseFrom( + writer.readAccountAt(2, address, committedAccount).getValue()).getBalance()); + } + + try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter(archive, 4096, databases())) { + assertEquals(20, Account.parseFrom( + reopened.readAccountAt(2, address, committedAccount).getValue()).getBalance()); + try (java.util.stream.Stream files = Files.list(archive.resolve("commits"))) { + assertEquals(1, files.count()); + } + } + } + + @Test + public void truncatesIncompleteCommitLogTailOnRestart() throws Exception { Path archive = temporaryFolder.newFolder("temporary-marker").toPath(); - Files.createDirectories(archive.resolve("commits")); - Files.write(archive.resolve("commits").resolve(".tmp-interrupted"), new byte[]{1, 2, 3}); try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { - assertNull(writer.committedHead()); - assertThrows(IllegalArgumentException.class, () -> writer.readCommitted(1)); + writer.accept(diff(1)); + } + Files.write(archive.resolve("commits").resolve("commit.log"), new byte[]{1, 2, 3}, + java.nio.file.StandardOpenOption.APPEND); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + assertEquals(1, writer.committedHead().getMeta().getEpoch()); + assertEquals(diff(1).getMeta(), writer.readCommitted(1).getMeta()); + } + } + + @Test + public void persistsBatchedPrefixWithoutPerBlockFilesAndResumes() throws Exception { + Path archive = temporaryFolder.newFolder("batched-prefix").toPath(); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + for (int start = 1; start <= 1_000; start += 100) { + List batch = new ArrayList<>(100); + for (int number = start; number < start + 100; number++) { + batch.add(diff(number)); + } + writer.acceptAll(batch); + } + assertEquals(1_000, writer.committedHead().getMeta().getEpoch()); + } + + try (java.util.stream.Stream commits = Files.list(archive.resolve("commits")); + java.util.stream.Stream segments = Files.list(archive.resolve("history"))) { + assertEquals(1, commits.count()); + assertTrue(segments.count() < 1_000); + } + try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter(archive, 4096, databases())) { + assertEquals(1_000, reopened.committedHead().getMeta().getEpoch()); + assertEquals(diff(500).getMeta(), reopened.readCommitted(500).getMeta()); + reopened.accept(diff(1_001)); + assertEquals(1_001, reopened.committedHead().getMeta().getEpoch()); } } @Test - public void markerDirectorySyncFailurePreservesBodyAndIndexAsUncertain() throws Exception { + public void commitLogForceBoundaryFailurePreservesRecordAsUncertain() throws Exception { Path archive = temporaryFolder.newFolder("uncertain-marker").toPath(); HistoryCommitMarker marker = new HistoryCommitMarker(diff(1).getMeta(), 0, new HistoryLocation(0, 0, 100, 17, new byte[32]), @@ -134,11 +198,7 @@ archive, new HistoryCommitMarkerCodec(), directorySync)) { assertThrows(java.io.IOException.class, () -> commits.commit(marker)); assertNull(commits.head()); assertTrue(commits.mayContain(1)); - try (java.util.stream.Stream paths = Files.list(archive.resolve("commits"))) { - assertEquals(1, paths - .filter(path -> path.getFileName().toString().endsWith(".commit")) - .count()); - } + assertTrue(Files.size(archive.resolve("commits").resolve("commit.log")) > 0); assertThrows(IllegalStateException.class, () -> commits.removeHead(marker.getMeta())); } @@ -155,6 +215,17 @@ private static BlockReverseDiff diff(int number) { new Entry(bytes("key-" + number), OldValue.present(bytes("value-" + number))))))); } + private static BlockReverseDiff accountDiff(int number, byte[] address, OldValue oldValue) { + return new BlockReverseDiff(new BlockSnapshotMeta(number, number, hash(number), + hash(number - 1), number * 3_000L), Collections.singletonList( + new DbGroup("account", Collections.singletonList(new Entry(address, oldValue))))); + } + + private static byte[] account(byte[] address, long balance) { + return Account.newBuilder().setAddress(ByteString.copyFrom(address)).setBalance(balance) + .build().toByteArray(); + } + private static byte[] hash(int suffix) { byte[] hash = new byte[32]; hash[31] = (byte) suffix; diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java new file mode 100644 index 00000000000..b5eb6fb5599 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java @@ -0,0 +1,434 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveReadContext.HistoricalStore; +import org.tron.core.db2.archive.ArchiveReadContext.StoreAdapter; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.HistoricalRangeOverlay.KeyRange; +import org.tron.core.db2.archive.HistoricalRangeOverlay.Limits; +import org.tron.core.db2.archive.HistoryIndexRecord.KeyGroup; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +public class ArchiveReadSnapshotTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void readsPointAndRangeFromOnePinnedPhysicalKeyGeneration() throws Exception { + try (Fixture fixture = new Fixture(temporaryFolder.newFolder("snapshot").toPath())) { + fixture.append(diff(1, entry("p/c", OldValue.absent()))); + fixture.append(diff(2, + entry("p/a", "a1"), entry("p/b", "b1"), entry("p/e", OldValue.absent()), + entry("p/f", OldValue.present(new byte[0])))); + fixture.append(diff(3, entry("p/a", "a2"))); + fixture.sync(); + + Map latestValues = new HashMap<>(); + latestValues.put("p/a", bytes("a3")); + latestValues.put("p/c", bytes("c1")); + latestValues.put("p/e", bytes("e2")); + InMemoryLatest latest = new InMemoryLatest(3, hash(3), latestValues); + try (ArchiveReadSnapshot snapshot = fixture.snapshot(1, latest)) { + assertValue(snapshot.get("account", bytes("p/a")), "a1"); + assertValue(snapshot.get("account", bytes("p/b")), "b1"); + assertValue(snapshot.get("account", bytes("p/c")), "c1"); + assertFalse(snapshot.get("account", bytes("p/e")).isPresent()); + assertArrayEquals(new byte[0], + snapshot.get("account", bytes("p/f")).getValue()); + + List range = snapshot.range("account", + KeyRange.prefix(bytes("p/")), new Limits(10, 10, 10)); + assertEquals(Arrays.asList("p/a", "p/b", "p/c", "p/f"), keys(range)); + } + assertTrue(latest.closed); + } + } + + @Test + public void rejectsMixedGenerationOrCoverageBeforeReading() throws Exception { + try (Fixture fixture = new Fixture(temporaryFolder.newFolder("identity").toPath())) { + fixture.append(diff(1, entry("key", "old"))); + fixture.sync(); + ServingKeyIndexGeneration serving = fixture.serving(); + CommittedHistoryReader history = fixture.history(); + InMemoryLatest wrongHash = new InMemoryLatest(1, hash(99), Collections.emptyMap()); + + assertThrows(IllegalArgumentException.class, () -> ArchiveReadSnapshot.pin( + 0, 1, hash(1), serving, wrongHash, history)); + assertTrue(wrongHash.closed); + + CommittedHistoryReader secondHistory = fixture.history(); + InMemoryLatest wrongCoverage = new InMemoryLatest(0, hash(0), Collections.emptyMap()); + assertThrows(IllegalArgumentException.class, () -> ArchiveReadSnapshot.pin( + 0, 0, hash(0), serving, wrongCoverage, secondHistory)); + assertTrue(wrongCoverage.closed); + } + } + + @Test + public void rejectsWrongKeyBetweenAuthoritativeIndexAndBody() throws Exception { + try (Fixture fixture = new Fixture(temporaryFolder.newFolder("wrong-key").toPath())) { + BlockReverseDiff body = diff(1, entry("actual", "old")); + HistoryLocation location = fixture.bodies.append(body); + HistoryIndexRecord wrongIndex = new HistoryIndexRecord(body.getMeta(), location, + Collections.singletonList(new KeyGroup("account", + Collections.singletonList(bytes("indexed"))))); + HistoryIndexLocation indexLocation = fixture.index.append(wrongIndex); + fixture.markers.add(marker(body.getMeta(), location, indexLocation)); + fixture.sync(); + + try (ArchiveReadSnapshot snapshot = fixture.snapshot(0, + new InMemoryLatest(1, hash(1), Collections.emptyMap()))) { + assertThrows(ArchivePersistenceException.class, + () -> snapshot.get("account", bytes("indexed"))); + } + } + } + + @Test + public void rejectsDigestMismatchAndMissingSegment() throws Exception { + assertUnreadableBody("digest", location -> new HistoryLocation(location.getSegmentId(), + location.getOffset(), location.getRecordLength(), location.getBodyChecksum(), hash(99)), + IllegalArgumentException.class); + assertUnreadableBody("missing", location -> new HistoryLocation(99, location.getOffset(), + location.getRecordLength(), location.getBodyChecksum(), location.getBodyDigest()), + IOException.class); + } + + @Test + public void releasesHistoryPinWhenCommittedPrefixValidationFails() throws Exception { + try (Fixture fixture = new Fixture(temporaryFolder.newFolder("failed-history-pin").toPath())) { + fixture.append(diff(1, entry("key", "old"))); + fixture.sync(); + AtomicBoolean released = new AtomicBoolean(); + ServingKeyIndexGeneration.AuthoritativeIndexReader failingIndex = location -> { + throw new IOException("injected index failure"); + }; + + assertThrows(IOException.class, () -> new CommittedHistoryReader(0, hash(0), + fixture.markers, failingIndex, fixture.bodies::read, () -> released.set(true))); + assertTrue(released.get()); + } + } + + @Test + public void bindsEveryVersionedPhysicalStoreToOneRequestSnapshot() throws Exception { + try (Fixture fixture = new Fixture(temporaryFolder.newFolder("read-context").toPath())) { + fixture.append(diff(1, entry("key", "old"))); + fixture.sync(); + Map latestValues = new HashMap<>(); + latestValues.put("key", bytes("new")); + InMemoryLatest latest = new InMemoryLatest(1, hash(1), latestValues); + AdapterSet adapterSet = rawAdapters(); + + try (ArchiveReadContext context = ArchiveReadContext.open( + fixture.snapshot(0, latest), adapterSet.adapters)) { + HistoricalStore account = context.store(adapterSet.account); + assertArrayEquals(bytes("old"), account.get(bytes("key")).orElseThrow(AssertionError::new)); + assertFalse(account.has(bytes("missing"))); + assertEquals(0, context.getTargetBlock()); + assertEquals(1, context.getPinnedBlock()); + assertTrue(context.getAdapterDbNames().contains("account-asset")); + assertFalse(context.getAdapterDbNames().contains("accountTrie")); + } + assertTrue(latest.closed); + } + } + + @Test + public void rejectsIncompleteOrDerivedStoreAdaptersAndReleasesSnapshot() throws Exception { + assertThrows(IllegalArgumentException.class, + () -> StoreAdapter.define("accountTrie", value -> value)); + try (Fixture fixture = new Fixture(temporaryFolder.newFolder("adapter-set").toPath())) { + fixture.append(diff(1, entry("key", "old"))); + fixture.sync(); + InMemoryLatest latest = new InMemoryLatest(1, hash(1), Collections.emptyMap()); + AdapterSet adapterSet = rawAdapters(); + adapterSet.adapters.remove(adapterSet.account); + + assertThrows(IllegalArgumentException.class, () -> ArchiveReadContext.open( + fixture.snapshot(0, latest), adapterSet.adapters)); + assertTrue(latest.closed); + } + } + + @Test + public void resolvesLogicalStorageWithHistoricalContractFromTheSameContext() throws Exception { + byte[] address = Hex.decode("410102030405060708090a0b0c0d0e0f1011121314"); + byte[] slot = Hex.decode( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + byte[] transactionHash = Hex.decode( + "f0e0d0c0b0a090807060504030201000112233445566778899aabbccddeeff00"); + byte[] physicalKey = Hex.decode( + "9397a7a785754542ff19d0968c0f92d4dea5e526567e92b0321816a4e895bd2d"); + SmartContract historicalContract = SmartContract.newBuilder().setVersion(1) + .setTrxHash(ByteString.copyFrom(transactionHash)).build(); + SmartContract latestContract = SmartContract.newBuilder().setVersion(0).build(); + + try (Fixture fixture = new Fixture(temporaryFolder.newFolder("logical-storage").toPath())) { + fixture.append(diff(1, + new DbGroup("contract", Collections.singletonList( + new Entry(address, OldValue.present(historicalContract.toByteArray())))), + new DbGroup("storage-row", Collections.singletonList( + new Entry(physicalKey, OldValue.present(bytes("historical-word"))))))); + fixture.sync(); + Map latestValues = new HashMap<>(); + latestValues.put(text(address), latestContract.toByteArray()); + AdapterSet adapterSet = rawAdapters(); + + try (ArchiveReadContext context = ArchiveReadContext.open( + fixture.snapshot(0, new InMemoryLatest(1, hash(1), latestValues)), + adapterSet.adapters)) { + assertArrayEquals(bytes("historical-word"), + context.getStorage(address, slot).orElseThrow(AssertionError::new)); + } + } + } + + @Test + public void logicalStorageFailsClosedForMissingCorruptOrClosedContractContext() + throws Exception { + byte[] address = Hex.decode("410102030405060708090a0b0c0d0e0f1011121314"); + byte[] slot = new byte[32]; + AdapterSet adapterSet = rawAdapters(); + try (Fixture fixture = new Fixture(temporaryFolder.newFolder("missing-contract").toPath())) { + ArchiveReadContext context = ArchiveReadContext.open( + fixture.snapshot(0, new InMemoryLatest(0, hash(0), Collections.emptyMap())), + adapterSet.adapters); + assertThrows(ArchivePersistenceException.class, () -> context.getStorage(address, slot)); + context.close(); + assertThrows(IllegalStateException.class, () -> context.getStorage(address, slot)); + } + + adapterSet = rawAdapters(); + try (Fixture fixture = new Fixture(temporaryFolder.newFolder("corrupt-contract").toPath())) { + fixture.append(diff(1, new DbGroup("contract", Collections.singletonList( + new Entry(address, OldValue.present(new byte[]{(byte) 0x80})))))); + fixture.sync(); + try (ArchiveReadContext context = ArchiveReadContext.open( + fixture.snapshot(0, new InMemoryLatest(1, hash(1), Collections.emptyMap())), + adapterSet.adapters)) { + assertThrows(ArchivePersistenceException.class, () -> context.getStorage(address, slot)); + } + } + } + + private void assertUnreadableBody(String name, LocationMutation mutation, + Class error) throws Exception { + try (Fixture fixture = new Fixture(temporaryFolder.newFolder(name).toPath())) { + BlockReverseDiff body = diff(1, entry("key", "old")); + HistoryLocation actual = fixture.bodies.append(body); + HistoryLocation referenced = mutation.apply(actual); + HistoryIndexLocation indexLocation = fixture.index.append( + new HistoryIndexRecord(body.getMeta(), referenced, + Collections.singletonList(new KeyGroup("account", + Collections.singletonList(bytes("key")))))); + fixture.markers.add(marker(body.getMeta(), referenced, indexLocation)); + fixture.sync(); + + try (ArchiveReadSnapshot snapshot = fixture.snapshot(0, + new InMemoryLatest(1, hash(1), Collections.emptyMap()))) { + assertThrows(error, () -> snapshot.get("account", bytes("key"))); + } + } + } + + private static BlockReverseDiff diff(int block, Entry... entries) { + return new BlockReverseDiff(new BlockSnapshotMeta(block, block, hash(block), + hash(block - 1), block * 3_000L), Collections.singletonList( + new DbGroup("account", Arrays.asList(entries)))); + } + + private static BlockReverseDiff diff(int block, DbGroup... groups) { + return new BlockReverseDiff(new BlockSnapshotMeta(block, block, hash(block), + hash(block - 1), block * 3_000L), Arrays.asList(groups)); + } + + private static Entry entry(String key, String oldValue) { + return entry(key, OldValue.present(bytes(oldValue))); + } + + private static Entry entry(String key, OldValue oldValue) { + return new Entry(bytes(key), oldValue); + } + + private static HistoryCommitMarker marker(BlockSnapshotMeta meta, HistoryLocation body, + HistoryIndexLocation index) { + return new HistoryCommitMarker(meta, meta.getEpoch() - 1, body, index, new byte[16], + new ArrayList<>(ArchiveStoreScope.getStateDatabases())); + } + + private static void assertValue(OldValue value, String expected) { + assertTrue(value.isPresent()); + assertArrayEquals(bytes(expected), value.getValue()); + } + + private static List keys(List entries) { + List result = new ArrayList<>(); + entries.forEach(entry -> result.add(text(entry.getKey()))); + return result; + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static String text(byte[] value) { + return new String(value, StandardCharsets.UTF_8); + } + + private static AdapterSet rawAdapters() { + List> adapters = new ArrayList<>(); + StoreAdapter account = StoreAdapter.define("account", + value -> Arrays.copyOf(value, value.length)); + for (String dbName : ArchiveStoreScope.getStateDatabases()) { + adapters.add("account".equals(dbName) ? account : StoreAdapter.define(dbName, + value -> Arrays.copyOf(value, value.length))); + } + return new AdapterSet(account, adapters); + } + + @FunctionalInterface + private interface LocationMutation { + HistoryLocation apply(HistoryLocation location); + } + + private static final class AdapterSet { + private final StoreAdapter account; + private final List> adapters; + + private AdapterSet(StoreAdapter account, List> adapters) { + this.account = account; + this.adapters = adapters; + } + } + + private static final class Fixture implements AutoCloseable { + private final HistorySegmentStore bodies; + private final HistoryIndexStore index; + private final List markers = new ArrayList<>(); + + private Fixture(Path archive) throws IOException { + bodies = new HistorySegmentStore(archive, new BlockHistoryCodec(), 4096); + index = new HistoryIndexStore(archive, new HistoryIndexCodec()); + } + + private void append(BlockReverseDiff diff) throws IOException { + HistoryLocation body = bodies.append(diff); + HistoryIndexLocation indexLocation = index.append(HistoryIndexRecord.from(diff, body)); + List databases = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + markers.add(new HistoryCommitMarker(diff.getMeta(), diff.getMeta().getEpoch() - 1, body, + indexLocation, new byte[16], databases)); + } + + private void sync() throws IOException { + bodies.sync(); + index.sync(); + } + + private ServingKeyIndexGeneration serving() throws IOException { + return ServingKeyIndexGeneration.rebuild( + "read-generation", 0, hash(0), markers, index::read, + new ArrayList<>(ArchiveStoreScope.getStateDatabases()), + ServingKeyIndexGeneration.IndexLayout.prototypeDefaults()); + } + + private CommittedHistoryReader history() throws IOException { + return new CommittedHistoryReader(0, hash(0), markers, index::read, bodies::read, + new ArrayList<>(ArchiveStoreScope.getStateDatabases())); + } + + private ArchiveReadSnapshot snapshot(long target, InMemoryLatest latest) throws IOException { + return ArchiveReadSnapshot.pin(target, markers.size(), hash(markers.size()), serving(), + latest, history()); + } + + @Override + public void close() throws Exception { + index.close(); + bodies.close(); + } + } + + private static final class InMemoryLatest implements PinnedLatestState { + private final long block; + private final byte[] hash; + private final Map values; + private boolean closed; + + private InMemoryLatest(long block, byte[] hash, Map values) { + this.block = block; + this.hash = Arrays.copyOf(hash, hash.length); + this.values = new HashMap<>(); + values.forEach((key, value) -> this.values.put(key, Arrays.copyOf(value, value.length))); + } + + @Override + public long getBlockNumber() { + return block; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(hash, hash.length); + } + + @Override + public OldValue get(String dbName, byte[] physicalRawKey) { + return OldValue.fromNullable(values.get(text(physicalRawKey))); + } + + @Override + public List range(String dbName, byte[] lowerInclusive, + byte[] upperExclusive) { + List result = new ArrayList<>(); + values.forEach((key, value) -> { + byte[] rawKey = bytes(key); + if (BlockReverseDiff.compareUnsigned(rawKey, lowerInclusive) >= 0 + && (upperExclusive == null + || BlockReverseDiff.compareUnsigned(rawKey, upperExclusive) < 0)) { + result.add(new HistoricalRangeOverlay.Entry(rawKey, value)); + } + }); + result.sort(Comparator.comparing(HistoricalRangeOverlay.Entry::getKey, + BlockReverseDiff::compareUnsigned)); + return result; + } + + @Override + public void close() { + closed = true; + } + + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java b/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java index 9b7a274a1cf..cd5bc38d71a 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertTrue; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Set; @@ -38,6 +39,24 @@ public void waitsForDurabilityAndRevertsCommittedHead() throws Exception { } } + @Test + public void persistsOneFlushRangeAsOneDurabilityBatch() throws Exception { + Path archive = temporaryFolder.newFolder("async-batch").toPath(); + java.util.List stages = new ArrayList<>(); + ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases(), + (stage, meta) -> stages.add(stage)); + try (AsyncArchiveHistorySink sink = new AsyncArchiveHistorySink(writer, 2)) { + sink.acceptAll(Arrays.asList(diff(1), diff(2), diff(3))); + sink.awaitCommitted(3); + assertEquals(3, writer.committedHead().getMeta().getEpoch()); + assertEquals(3, stages.stream().filter(stage -> stage == Stage.APPEND_BODY).count()); + assertEquals(3, stages.stream().filter(stage -> stage == Stage.APPEND_INDEX).count()); + assertEquals(1, stages.stream().filter(stage -> stage == Stage.SYNC_BODY).count()); + assertEquals(1, stages.stream().filter(stage -> stage == Stage.SYNC_INDEX).count()); + assertEquals(3, stages.stream().filter(stage -> stage == Stage.COMMIT_MARKER).count()); + } + } + @Test public void removesQueuedForkHeadWithoutPublishingIt() throws Exception { Path archive = temporaryFolder.newFolder("queued-reorg").toPath(); diff --git a/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountBalanceReaderTest.java b/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountBalanceReaderTest.java new file mode 100644 index 00000000000..b875990806c --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountBalanceReaderTest.java @@ -0,0 +1,150 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; +import org.tron.protos.Protocol.Account; +import org.tron.protos.Protocol.AccountType; + +public class HistoricalAccountBalanceReaderTest { + + @Test + public void distinguishesHistoricalBalanceZeroFromAbsentAccount() throws Exception { + byte[] existing = address(1); + byte[] absent = address(2); + ArchiveReadSnapshot snapshot = snapshot(existing, account(existing, 0)); + + HistoricalAccountBalanceReader.Result present = + HistoricalAccountBalanceReader.read(snapshot, existing); + assertTrue(present.isPresent()); + assertEquals(0, present.getBalance()); + assertEquals(7, present.getBlockNumber()); + + HistoricalAccountBalanceReader.Result missing = + HistoricalAccountBalanceReader.read(snapshot, absent); + assertFalse(missing.isPresent()); + assertThrows(IllegalStateException.class, missing::getBalance); + snapshot.close(); + } + + @Test + public void rejectsMalformedOrWrongKeyAccountValues() throws Exception { + byte[] address = address(3); + ArchiveReadSnapshot malformed = snapshot(address, new byte[]{1, 2, 3}); + assertThrows(ArchivePersistenceException.class, + () -> HistoricalAccountBalanceReader.read(malformed, address)); + malformed.close(); + + ArchiveReadSnapshot wrongKey = snapshot(address, account(address(4), 99)); + assertThrows(ArchivePersistenceException.class, + () -> HistoricalAccountBalanceReader.read(wrongKey, address)); + wrongKey.close(); + } + + private static ArchiveReadSnapshot snapshot(byte[] key, byte[] value) throws Exception { + byte[] hash = hash(7); + ServingKeyIndexGeneration serving = ServingKeyIndexGeneration.rebuild( + "account-balance", 0, hash(0), Collections.emptyList(), ignored -> null, + Collections.singletonList("account"), + ServingKeyIndexGeneration.IndexLayout.prototypeDefaults()); + // Empty-prefix coverage ends at the base. Build the pinned fixture at block 0 then expose the + // requested target through a seven-block no-change committed prefix. + java.util.List markers = new java.util.ArrayList<>(); + java.util.Map indexes = new java.util.HashMap<>(); + for (int block = 1; block <= 7; block++) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(block, block, hash(block), hash(block - 1), + block * 3_000L); + HistoryLocation body = new HistoryLocation(0, block * 100L, 10, block, hash(block + 20)); + HistoryIndexRecord index = new HistoryIndexRecord(meta, body, Collections.emptyList()); + HistoryIndexLocation location = new HistoryIndexLocation(block * 80L, 20, + hash(block + 40)); + markers.add(new HistoryCommitMarker(meta, block - 1, body, location, new byte[16], + Collections.singletonList("account"))); + indexes.put(location.getOffset(), index); + } + serving = ServingKeyIndexGeneration.rebuild("account-balance", 0, hash(0), markers, + location -> indexes.get(location.getOffset())); + ServingKeyIndexGeneration finalServing = serving; + ArchiveReadSnapshot.PinnedLatestState latest = new ArchiveReadSnapshot.PinnedLatestState() { + @Override + public long getBlockNumber() { + return 7; + } + + @Override + public byte[] getBlockHash() { + return hash; + } + + @Override + public OldValue get(String dbName, byte[] rawKey) { + return Arrays.equals(key, rawKey) ? OldValue.present(value) : OldValue.absent(); + } + + @Override + public java.util.List range(String dbName, byte[] lower, + byte[] upper) { + return Collections.emptyList(); + } + + @Override + public void close() { + } + }; + ArchiveReadSnapshot.PinnedHistory history = new ArchiveReadSnapshot.PinnedHistory() { + @Override + public long getIndexedFrom() { + return finalServing.getIndexedFrom(); + } + + @Override + public long getIndexedThrough() { + return 7; + } + + @Override + public byte[] getHeadHash() { + return hash; + } + + @Override + public byte[] getAuthoritativePrefixDigest() { + return finalServing.getAuthoritativePrefixDigest(); + } + + @Override + public OldValue read(String dbName, byte[] rawKey, long firstChangeBlock) { + throw new AssertionError("No key changes are expected in this fixture"); + } + + @Override + public void close() { + } + }; + return ArchiveReadSnapshot.pin(7, 7, hash, serving, latest, history); + } + + private static byte[] account(byte[] address, long balance) { + return Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .setType(AccountType.Normal).setBalance(balance).build().toByteArray(); + } + + private static byte[] address(int suffix) { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = (byte) suffix; + return address; + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/HistoricalRangeOverlayTest.java b/framework/src/test/java/org/tron/core/db2/archive/HistoricalRangeOverlayTest.java new file mode 100644 index 00000000000..ce523a44bf8 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/HistoricalRangeOverlayTest.java @@ -0,0 +1,151 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.HistoricalRangeOverlay.Entry; +import org.tron.core.db2.archive.HistoricalRangeOverlay.KeyRange; +import org.tron.core.db2.archive.HistoricalRangeOverlay.Limits; +import org.tron.core.db2.archive.HistoryIndexRecord.KeyGroup; + +public class HistoricalRangeOverlayTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void overlaysOneStoreWithoutLosingDeletedOrEmptyValues() throws Exception { + ServingKeyIndexGeneration index = buildIndex("overlay"); + List latest = Arrays.asList( + entry("p/a", "a3"), + entry("p/c", "c1"), + entry("p/d", "d1"), + entry("p/e", "e2")); + Map oldValues = new HashMap<>(); + oldValues.put("p/a", OldValue.present(bytes("a1"))); + oldValues.put("p/b", OldValue.present(bytes("b1"))); + oldValues.put("p/e", OldValue.absent()); + oldValues.put("p/f", OldValue.present(new byte[0])); + Map readAt = new HashMap<>(); + + List result = HistoricalRangeOverlay.materialize("account", 1, 3, + KeyRange.prefix(bytes("p/")), latest, index, (dbName, key, firstChange) -> { + assertEquals("account", dbName); + readAt.put(text(key), firstChange); + return oldValues.get(text(key)); + }, new Limits(10, 10, 10)); + + assertEntries(result, "p/a", "a1", "p/b", "b1", "p/c", "c1", "p/d", "d1"); + assertEquals(5, result.size()); + assertArrayEquals(bytes("p/f"), result.get(4).getKey()); + assertArrayEquals(new byte[0], result.get(4).getValue()); + assertEquals(Long.valueOf(2), readAt.get("p/a")); + assertEquals(Long.valueOf(2), readAt.get("p/b")); + assertEquals(Long.valueOf(2), readAt.get("p/e")); + assertEquals(Long.valueOf(2), readAt.get("p/f")); + } + + @Test + public void rejectsEveryBudgetOverflowInsteadOfReturningPartialResults() throws Exception { + ServingKeyIndexGeneration index = buildIndex("limits"); + List latest = Arrays.asList(entry("p/a", "a3"), entry("p/c", "c1")); + HistoricalRangeOverlay.HistoricalValueReader history = + (dbName, key, block) -> OldValue.present(bytes("old")); + + assertThrows(ArchiveQueryLimitExceededException.class, + () -> materialize(index, latest, history, new Limits(3, 10, 10))); + assertThrows(ArchiveQueryLimitExceededException.class, + () -> materialize(index, latest, history, new Limits(10, 4, 10))); + assertThrows(ArchiveQueryLimitExceededException.class, + () -> materialize(index, latest, history, new Limits(10, 10, 3))); + } + + @Test + public void rejectsUnsortedOrOutOfRangeLatestInput() throws Exception { + ServingKeyIndexGeneration index = buildIndex("validation"); + HistoricalRangeOverlay.HistoricalValueReader history = + (dbName, key, block) -> OldValue.absent(); + + assertThrows(IllegalArgumentException.class, + () -> materialize(index, Arrays.asList(entry("p/b", "b"), entry("p/a", "a")), + history, new Limits(10, 10, 10))); + assertThrows(IllegalArgumentException.class, + () -> materialize(index, Collections.singletonList(entry("q/a", "a")), history, + new Limits(10, 10, 10))); + } + + private ServingKeyIndexGeneration buildIndex(String name) throws Exception { + Path archive = temporaryFolder.newFolder(name).toPath(); + List markers = new ArrayList<>(); + try (HistoryIndexStore authoritative = new HistoryIndexStore( + archive, new HistoryIndexCodec())) { + markers.add(append(authoritative, 1, group("account", bytes("p/c")))); + markers.add(append(authoritative, 2, + group("account", bytes("p/a"), bytes("p/b"), bytes("p/e"), bytes("p/f")), + group("other", bytes("p/a")))); + markers.add(append(authoritative, 3, + group("account", bytes("p/a"), bytes("q/z")))); + authoritative.sync(); + return ServingKeyIndexGeneration.rebuild( + "generation-" + name, 0, hash(0), markers, authoritative::read); + } + } + + private static List materialize(ServingKeyIndexGeneration index, List latest, + HistoricalRangeOverlay.HistoricalValueReader history, Limits limits) throws Exception { + return HistoricalRangeOverlay.materialize("account", 1, 3, + KeyRange.prefix(bytes("p/")), latest, index, history, limits); + } + + private static HistoryCommitMarker append(HistoryIndexStore authoritative, int block, + KeyGroup... groups) throws Exception { + BlockSnapshotMeta meta = new BlockSnapshotMeta(block, block, hash(block), hash(block - 1), + block * 3_000L); + HistoryLocation body = new HistoryLocation(0, block * 100L, 80, block, hash(block)); + HistoryIndexLocation location = authoritative.append( + new HistoryIndexRecord(meta, body, Arrays.asList(groups))); + return new HistoryCommitMarker(meta, block - 1L, body, location, new byte[16], + Arrays.asList("account", "other")); + } + + private static KeyGroup group(String dbName, byte[]... keys) { + return new KeyGroup(dbName, Arrays.asList(keys)); + } + + private static Entry entry(String key, String value) { + return new Entry(bytes(key), bytes(value)); + } + + private static void assertEntries(List entries, String... keyValues) { + for (int i = 0; i < keyValues.length; i += 2) { + assertArrayEquals(bytes(keyValues[i]), entries.get(i / 2).getKey()); + assertArrayEquals(bytes(keyValues[i + 1]), entries.get(i / 2).getValue()); + } + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static String text(byte[] value) { + return new String(value, StandardCharsets.UTF_8); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/HistoryIndexStoreTest.java b/framework/src/test/java/org/tron/core/db2/archive/HistoryIndexStoreTest.java index 2d0c3ecb7ad..155fa71e06e 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/HistoryIndexStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/HistoryIndexStoreTest.java @@ -49,8 +49,8 @@ public void locatesAndBackReferencesHistoryBody() throws Exception { try (HistoryIndexStore reopened = new HistoryIndexStore(archive, new HistoryIndexCodec())) { assertNull(reopened.getScanResult().getInvalidTailOffset()); - assertEquals(1, reopened.getScanResult().getRecords().size()); - assertArrayEquals(indexLocation.getDigest(), reopened.getScanResult().getRecords().get(0) + assertEquals(1, reopened.getScanResult().getRecordCount()); + assertArrayEquals(indexLocation.getDigest(), reopened.getScanResult().getHead() .getLocation().getDigest()); } } @@ -83,7 +83,7 @@ public void detectsAndTruncatesCorruptIndexTail() throws Exception { new HistoryLocation(0, 0, 1, 0, new byte[32])))); index.truncateInvalidTail(); assertNull(index.getScanResult().getInvalidTailOffset()); - assertEquals(0, index.getScanResult().getRecords().size()); + assertEquals(0, index.getScanResult().getRecordCount()); } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/HistorySegmentStoreTest.java b/framework/src/test/java/org/tron/core/db2/archive/HistorySegmentStoreTest.java index 3d893b40689..cbcee3b767d 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/HistorySegmentStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/HistorySegmentStoreTest.java @@ -34,14 +34,14 @@ public void appendsRotatesScansAndReadsRecords() throws Exception { store.sync(); assertEquals(0, first.getSegmentId()); assertEquals(1, second.getSegmentId()); - assertEquals(2, store.getScanResult().getRecords().size()); + assertEquals(2, store.getScanResult().getRecordCount()); assertEquals(2, store.read(second).getMeta().getBlockNumber()); } try (HistorySegmentStore reopened = new HistorySegmentStore(archive, codec, 250)) { assertNull(reopened.getScanResult().getInvalidTail()); - assertEquals(2, reopened.getScanResult().getRecords().size()); - assertArrayEquals(second.getBodyDigest(), reopened.getScanResult().getRecords().get(1) + assertEquals(2, reopened.getScanResult().getRecordCount()); + assertArrayEquals(second.getBodyDigest(), reopened.getScanResult().getHead() .getLocation().getBodyDigest()); } } @@ -66,7 +66,7 @@ public void findsAndTruncatesPartialTailBeforeAppending() throws Exception { assertNull(store.getScanResult().getInvalidTail()); store.append(diff(2, bytes("two"))); store.sync(); - assertEquals(2, store.getScanResult().getRecords().size()); + assertEquals(2, store.getScanResult().getRecordCount()); } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/ServingKeyIndexGenerationTest.java b/framework/src/test/java/org/tron/core/db2/archive/ServingKeyIndexGenerationTest.java new file mode 100644 index 00000000000..bf92a1dc64a --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ServingKeyIndexGenerationTest.java @@ -0,0 +1,229 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.OptionalLong; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.HistoryIndexRecord.KeyGroup; +import org.tron.core.db2.archive.ServingKeyIndexGeneration.IndexLayout; +import org.tron.core.db2.archive.ServingKeyIndexGeneration.StoreCoverage; + +public class ServingKeyIndexGenerationTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void rebuildsExactKeysAndSeeksOnlyInsideCompleteCoverage() throws Exception { + Path archive = temporaryFolder.newFolder("serving-index").toPath(); + List markers = new ArrayList<>(); + byte[] accountKey = bytes("same-key"); + byte[] mutableInput = Arrays.copyOf(accountKey, accountKey.length); + byte[] collisionLeft = new byte[]{0, 31}; + byte[] collisionRight = new byte[]{1, 0}; + assertEquals(Arrays.hashCode(collisionLeft), Arrays.hashCode(collisionRight)); + + try (HistoryIndexStore authoritative = new HistoryIndexStore( + archive, new HistoryIndexCodec())) { + markers.add(append(authoritative, 1, + groups(group("account", mutableInput), group("account-asset", accountKey)))); + markers.add(append(authoritative, 2, + groups(group("account", collisionLeft, collisionRight, bytes("other-key"))))); + markers.add(append(authoritative, 3, + groups(group("account", accountKey)))); + append(authoritative, 4, groups(group("account", accountKey))); // not committed + authoritative.sync(); + + ServingKeyIndexGeneration generation = ServingKeyIndexGeneration.rebuild( + "generation-1", 0, hash(0), markers, authoritative::read); + mutableInput[0] ^= 0x7f; + + assertEquals(0, generation.getIndexedFrom()); + assertEquals(3, generation.getIndexedThrough()); + assertArrayEquals(hash(3), generation.getHeadHash()); + assertEquals(5, generation.getKeyMetadataCount()); + assertEquals(5, generation.getInlineKeyCount()); + assertEquals(0, generation.getPagedKeyCount()); + assertEquals(0, generation.getEpochPageCount()); + StoreCoverage accountCoverage = generation.getStoreCoverage("account").get(); + assertEquals(0, accountCoverage.getIndexedFrom()); + assertEquals(3, accountCoverage.getIndexedThrough()); + assertArrayEquals(hash(3), accountCoverage.getHeadHash()); + assertArrayEquals(generation.getAuthoritativePrefixDigest(), + accountCoverage.getAuthoritativePrefixDigest()); + assertTrue(generation.getStoreCoverage("account-asset").isPresent()); + assertEquals(1, change(generation, "account", accountKey, 0, 3)); + assertEquals(3, change(generation, "account", accountKey, 1, 3)); + assertEquals(1, change(generation, "account-asset", accountKey, 0, 3)); + assertEquals(2, change(generation, "account", collisionLeft, 0, 3)); + assertEquals(2, change(generation, "account", collisionRight, 0, 3)); + assertFalse(generation.firstChangeAfter("account", accountKey, 1, 2).isPresent()); + assertFalse(generation.firstChangeAfter("account", bytes("missing"), 0, 3) + .isPresent()); + assertThrows(IllegalArgumentException.class, + () -> generation.firstChangeAfter("account", accountKey, -1, 3)); + assertThrows(IllegalArgumentException.class, + () -> generation.firstChangeAfter("account", accountKey, 3, 4)); + assertThrows(IllegalArgumentException.class, + () -> generation.firstChangeAfter("properties", accountKey, 0, 3)); + } + } + + @Test + public void usesInlineMetadataAndBaseEpochPagesWithoutChangingSeekSemantics() + throws Exception { + Path archive = temporaryFolder.newFolder("hybrid-pages").toPath(); + List markers = new ArrayList<>(); + byte[] hot = bytes("hot"); + byte[] cold = bytes("cold"); + try (HistoryIndexStore authoritative = new HistoryIndexStore( + archive, new HistoryIndexCodec())) { + for (int block = 1; block <= 6; block++) { + markers.add(append(authoritative, block, groups(block == 1 + ? group("account", cold, hot) : group("account", hot)))); + } + authoritative.sync(); + + ServingKeyIndexGeneration generation = ServingKeyIndexGeneration.rebuild( + "generation-hybrid", 0, hash(0), markers, authoritative::read, + new IndexLayout(2, 2)); + + assertEquals(2, generation.getKeyMetadataCount()); + assertEquals(1, generation.getInlineKeyCount()); + assertEquals(1, generation.getPagedKeyCount()); + assertEquals(3, generation.getEpochPageCount()); + assertEquals(1, change(generation, "account", cold, 0, 6)); + assertFalse(generation.firstChangeAfter("account", cold, 1, 6).isPresent()); + assertEquals(1, change(generation, "account", hot, 0, 6)); + assertEquals(3, change(generation, "account", hot, 2, 6)); + assertEquals(5, change(generation, "account", hot, 4, 6)); + assertEquals(6, change(generation, "account", hot, 5, 6)); + assertFalse(generation.firstChangeAfter("account", hot, 6, 6).isPresent()); + } + } + + @Test + public void rejectsParticipantSetChangesInsideOneGeneration() throws Exception { + Path archive = temporaryFolder.newFolder("participant-change").toPath(); + try (HistoryIndexStore authoritative = new HistoryIndexStore( + archive, new HistoryIndexCodec())) { + HistoryCommitMarker first = append(authoritative, 1, + groups(group("account", bytes("key-1")))); + HistoryCommitMarker second = append(authoritative, 2, + groups(group("account", bytes("key-2")))); + second = new HistoryCommitMarker(second.getMeta(), second.getPreviousEpoch(), + second.getHistoryLocation(), second.getIndexLocation(), second.getBatchId(), + Collections.singletonList("account")); + authoritative.sync(); + + HistoryCommitMarker changedParticipants = second; + assertThrows(IllegalArgumentException.class, () -> ServingKeyIndexGeneration.rebuild( + "generation-bad-participants", 0, hash(0), + Arrays.asList(first, changedParticipants), authoritative::read)); + } + } + + @Test + public void rebuildIsDeterministicAndCatalogRejectsStalePublication() throws Exception { + Path archive = temporaryFolder.newFolder("generation-cas").toPath(); + List markers = new ArrayList<>(); + try (HistoryIndexStore authoritative = new HistoryIndexStore( + archive, new HistoryIndexCodec())) { + markers.add(append(authoritative, 1, groups(group("account", bytes("key-1"))))); + markers.add(append(authoritative, 2, groups(group("account", bytes("key-2"))))); + authoritative.sync(); + + ServingKeyIndexGeneration first = ServingKeyIndexGeneration.rebuild( + "generation-1", 0, hash(0), markers, authoritative::read); + ServingKeyIndexGeneration replacement = ServingKeyIndexGeneration.rebuild( + "generation-2", 0, hash(0), markers, authoritative::read); + ServingKeyIndexGeneration stale = ServingKeyIndexGeneration.rebuild( + "generation-stale", 0, hash(0), markers, authoritative::read); + ServingKeyIndexGeneration regressed = ServingKeyIndexGeneration.rebuild( + "generation-regressed", 0, hash(0), Collections.emptyList(), authoritative::read); + assertArrayEquals(first.getAuthoritativePrefixDigest(), + replacement.getAuthoritativePrefixDigest()); + + ServingKeyIndexCatalog catalog = new ServingKeyIndexCatalog(first); + ServingKeyIndexGeneration pinned = catalog.pin(); + assertThrows(IllegalArgumentException.class, () -> catalog.publish(first, regressed)); + assertTrue(catalog.publish(first, replacement)); + assertFalse(catalog.publish(first, stale)); + assertSame(replacement, catalog.pin()); + assertEquals(1, change(pinned, "account", bytes("key-1"), 0, 2)); + } + } + + @Test + public void rejectsMarkerMismatchWithoutReplacingCurrentGeneration() throws Exception { + Path archive = temporaryFolder.newFolder("corrupt-source").toPath(); + try (HistoryIndexStore authoritative = new HistoryIndexStore( + archive, new HistoryIndexCodec())) { + HistoryCommitMarker valid = append(authoritative, 1, + groups(group("account", bytes("key")))); + authoritative.sync(); + ServingKeyIndexGeneration current = ServingKeyIndexGeneration.rebuild( + "generation-1", 0, hash(0), Collections.singletonList(valid), authoritative::read); + ServingKeyIndexCatalog catalog = new ServingKeyIndexCatalog(current); + + HistoryLocation wrongBody = bodyLocation(99); + HistoryCommitMarker mismatched = new HistoryCommitMarker(valid.getMeta(), 0, wrongBody, + valid.getIndexLocation(), new byte[16], Collections.singletonList("account")); + assertThrows(IllegalArgumentException.class, () -> ServingKeyIndexGeneration.rebuild( + "generation-bad", 0, hash(0), Collections.singletonList(mismatched), + authoritative::read)); + assertSame(current, catalog.pin()); + } + } + + private static HistoryCommitMarker append(HistoryIndexStore authoritative, int block, + List groups) throws Exception { + BlockSnapshotMeta meta = new BlockSnapshotMeta(block, block, hash(block), hash(block - 1), + block * 3_000L); + HistoryLocation body = bodyLocation(block); + HistoryIndexLocation index = authoritative.append(new HistoryIndexRecord(meta, body, groups)); + return new HistoryCommitMarker(meta, block - 1L, body, index, new byte[16], + Arrays.asList("account", "account-asset")); + } + + private static List groups(KeyGroup... groups) { + return Arrays.asList(groups); + } + + private static KeyGroup group(String dbName, byte[]... keys) { + return new KeyGroup(dbName, Arrays.asList(keys)); + } + + private static HistoryLocation bodyLocation(int block) { + return new HistoryLocation(0, block * 100L, 80, block, hash(block)); + } + + private static long change(ServingKeyIndexGeneration generation, String dbName, byte[] key, + long target, long upperBound) { + OptionalLong changed = generation.firstChangeAfter(dbName, key, target, upperBound); + assertTrue(changed.isPresent()); + return changed.getAsLong(); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index 26a6b549705..addf7a83ff6 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -18,6 +18,7 @@ import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -28,10 +29,12 @@ import java.util.Set; import org.junit.Test; import org.tron.common.BaseMethodTest; +import org.tron.core.db.common.DbSourceInter; import org.tron.core.db2.ISession; import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; import org.tron.core.db2.archive.BlockReverseDiff.Entry; import org.tron.core.db2.common.DB; +import org.tron.core.db2.common.Flusher; import org.tron.core.db2.common.WrappedByteArray; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotImpl; @@ -60,8 +63,8 @@ public void collectsBlockPreStateAfterNestedSessionsFinish() { Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); manager.add(database); manager.enable(); - List captured = new ArrayList<>(); - manager.installArchiveCollector(new SnapshotOldValueCollector(), captured::add); + BlockReverseDiffSink sink = mock(BlockReverseDiffSink.class); + manager.installArchiveCollector(new SnapshotOldValueCollector(), sink); byte[] hash = new byte[32]; hash[31] = 1; @@ -83,8 +86,8 @@ public void collectsBlockPreStateAfterNestedSessionsFinish() { block.commit(meta); } - assertEquals(1, captured.size()); - BlockReverseDiff diff = captured.get(0); + BlockReverseDiff diff = prepared(database); + verify(sink, never()).accept(any(BlockReverseDiff.class)); assertEquals(meta, diff.getMeta()); assertEquals(meta, ((SnapshotImpl) database.getHead()).getBlockSnapshotMeta()); assertEquals(1, diff.getGroups().size()); @@ -101,6 +104,41 @@ public void collectsBlockPreStateAfterNestedSessionsFinish() { manager.shutdown(); } + @Test + public void preservesStorageRowPhysicalKeyWithoutLogicalProjection() { + MemoryDb memoryDb = new MemoryDb("storage-row"); + byte[] addressHash = new byte[32]; + byte[] slotPart = new byte[32]; + for (int i = 0; i < 32; i++) { + addressHash[i] = (byte) i; + slotPart[i] = (byte) (0x80 + i); + } + byte[] physicalKey = new byte[32]; + System.arraycopy(addressHash, 0, physicalKey, 0, 16); + System.arraycopy(slotPart, 16, physicalKey, 16, 16); + memoryDb.put(physicalKey, bytes("old-word")); + + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + + try (ISession block = manager.buildSession()) { + database.put(physicalKey, bytes("new-word")); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + + BlockReverseDiff diff = prepared(database); + assertEquals(1, diff.getGroups().size()); + DbGroup group = diff.getGroups().get(0); + assertEquals("storage-row", group.getDbName()); + assertEquals(1, group.getEntries().size()); + assertArrayEquals(physicalKey, group.getEntries().get(0).getKey()); + assertArrayEquals(bytes("old-word"), group.getEntries().get(0).getOldValue().getValue()); + manager.shutdown(); + } + @Test public void preservesPresentEmptyAndEmitsNoopBlockMetadata() { MemoryDb memoryDb = new MemoryDb("abi"); @@ -110,22 +148,21 @@ public void preservesPresentEmptyAndEmitsNoopBlockMetadata() { Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); manager.add(database); manager.enable(); - List captured = new ArrayList<>(); - manager.installArchiveCollector(new SnapshotOldValueCollector(), captured::add); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); try (ISession block = manager.buildSession()) { database.put(key, bytes("value")); block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); } - assertTrue(find(captured.get(0).getGroups().get(0), key).getOldValue().isPresent()); + BlockReverseDiff first = prepared(database); + assertTrue(find(first.getGroups().get(0), key).getOldValue().isPresent()); assertEquals(0, - find(captured.get(0).getGroups().get(0), key).getOldValue().getValue().length); + find(first.getGroups().get(0), key).getOldValue().getValue().length); try (ISession block = manager.buildSession()) { block.commit(BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L)); } - assertEquals(2, captured.size()); - assertTrue(captured.get(1).getGroups().isEmpty()); + assertTrue(prepared(database).getGroups().isEmpty()); manager.shutdown(); } @@ -160,8 +197,7 @@ public void matchesReferenceStateForRandomBlockOperations() { Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); manager.add(database); manager.enable(); - List captured = new ArrayList<>(); - manager.installArchiveCollector(new SnapshotOldValueCollector(), captured::add); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); Random random = new Random(0x5a17L); Map reference = new HashMap<>(); @@ -190,7 +226,7 @@ public void matchesReferenceStateForRandomBlockOperations() { hash(blockNumber - 1), blockNumber)); } - BlockReverseDiff diff = captured.get(captured.size() - 1); + BlockReverseDiff diff = prepared(database); Map actual = new HashMap<>(); if (!diff.getGroups().isEmpty()) { diff.getGroups().get(0).getEntries().forEach(entry -> actual.put( @@ -238,17 +274,17 @@ public void projectsAccountAssetTransitionBeforeRootMerge() { Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); manager.add(database); manager.enable(); - List captured = new ArrayList<>(); AccountAssetArchiveProjector projector = new AccountAssetArchiveProjector(assetStore, () -> true); - manager.installArchiveCollector(new SnapshotOldValueCollector(projector), captured::add); + manager.installArchiveCollector(new SnapshotOldValueCollector(projector), diff -> { }); try (ISession block = manager.buildSession()) { database.put(address, postAccount.toByteArray()); block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); } - DbGroup accountGroup = captured.get(0).getGroups().stream() + BlockReverseDiff diff = prepared(database); + DbGroup accountGroup = diff.getGroups().stream() .filter(group -> "account".equals(group.getDbName())) .findFirst().orElseThrow(AssertionError::new); Account archivedAccount; @@ -260,7 +296,7 @@ public void projectsAccountAssetTransitionBeforeRootMerge() { assertFalse(archivedAccount.getAssetOptimized()); assertEquals(100L, archivedAccount.getAssetV2Map().get("1000001").longValue()); - DbGroup assetGroup = captured.get(0).getGroups().stream() + DbGroup assetGroup = diff.getGroups().stream() .filter(group -> "account-asset".equals(group.getDbName())) .findFirst().orElseThrow(AssertionError::new); Entry assetEntry = find(assetGroup, Bytes.concat(address, token)); @@ -290,18 +326,18 @@ public void projectsOldPhysicalAssetValueForOptimizedAccount() { Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); manager.add(database); manager.enable(); - List captured = new ArrayList<>(); manager.installArchiveCollector(new SnapshotOldValueCollector( - new AccountAssetArchiveProjector(assetStore, () -> true)), captured::add); + new AccountAssetArchiveProjector(assetStore, () -> true)), diff -> { }); try (ISession block = manager.buildSession()) { database.put(address, postAccount.toByteArray()); block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); } - assertFalse(captured.get(0).getGroups().stream() + BlockReverseDiff diff = prepared(database); + assertFalse(diff.getGroups().stream() .anyMatch(group -> "account".equals(group.getDbName()))); - DbGroup assetGroup = captured.get(0).getGroups().stream() + DbGroup assetGroup = diff.getGroups().stream() .filter(group -> "account-asset".equals(group.getDbName())) .findFirst().orElseThrow(AssertionError::new); assertArrayEquals(Longs.toByteArray(100L), @@ -325,18 +361,107 @@ public void archiveDurabilityFailurePreventsCheckpointAndRefresh() throws Except database.put(bytes("key"), bytes("value")); block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); } - java.lang.reflect.Field flushCount = SnapshotManager.class.getDeclaredField("flushCount"); - flushCount.setAccessible(true); - flushCount.setInt(manager, 1); + setFlushCount(manager, 1); doThrow(new ArchivePersistenceException("injected")) .when(sink).awaitCommitted(1L); assertThrows(TronError.class, manager::flush); + verify(sink).accept(prepared(database)); verify(sink).awaitCommitted(1L); verify(checkpoint, never()).updateByBatch(any(Map.class)); manager.shutdown(); } + @Test + public void fastPopDiscardsPreparedPayloadWithoutRevertingDurableHistory() { + MemoryDb memoryDb = new MemoryDb("abi"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + BlockReverseDiffSink sink = mock(BlockReverseDiffSink.class); + manager.installArchiveCollector(new SnapshotOldValueCollector(), sink); + + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + try (ISession block = manager.buildSession()) { + database.put(bytes("key"), bytes("value")); + block.commit(meta); + } + + assertEquals(meta, prepared(database).getMeta()); + manager.fastPop(); + + assertEquals(0, manager.size()); + assertTrue(database.getHead() instanceof SnapshotRoot); + verify(sink, never()).accept(any(BlockReverseDiff.class)); + verify(sink, never()).revert(any(BlockSnapshotMeta.class)); + manager.shutdown(); + } + + @Test + public void collectorFailureLeavesSessionOwnedSoCloseRevokesLayer() { + MemoryDb memoryDb = new MemoryDb("abi"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + OldValueCollector collector = mock(OldValueCollector.class); + BlockReverseDiffSink sink = mock(BlockReverseDiffSink.class); + when(collector.collect(any(BlockChangeView.class))) + .thenThrow(new IllegalStateException("injected collector failure")); + manager.installArchiveCollector(collector, sink); + + assertThrows(IllegalStateException.class, () -> { + try (ISession block = manager.buildSession()) { + database.put(bytes("key"), bytes("value")); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + }); + + assertEquals(0, manager.getActiveSession()); + assertEquals(0, manager.size()); + assertTrue(database.getHead() instanceof SnapshotRoot); + verify(sink, never()).accept(any(BlockReverseDiff.class)); + manager.shutdown(); + } + + @Test + public void flushPublishesOnlyTheNonRevertibleRange() throws Exception { + MemoryDb memoryDb = new MemoryDb("abi"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.setUnChecked(false); + CheckTmpStore checkpoint = mock(CheckTmpStore.class); + DbSourceInter checkpointDb = mock(DbSourceInter.class); + when(checkpointDb.iterator()).thenReturn(Collections.emptyIterator()); + when(checkpoint.getDbSource()).thenReturn(checkpointDb); + manager.setCheckTmpStore(checkpoint); + DurableBlockReverseDiffSink sink = mock(DurableBlockReverseDiffSink.class); + manager.installArchiveCollector(new SnapshotOldValueCollector(), sink); + + try (ISession block = manager.buildSession()) { + database.put(bytes("key-1"), bytes("value-1")); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + BlockReverseDiff first = prepared(database); + try (ISession block = manager.buildSession()) { + database.put(bytes("key-2"), bytes("value-2")); + block.commit(BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L)); + } + BlockReverseDiff second = prepared(database); + setFlushCount(manager, 1); + + manager.flush(); + + verify(sink).accept(first); + verify(sink, never()).accept(second); + verify(sink).awaitCommitted(1L); + verify(sink).releaseThrough(1L); + manager.shutdown(); + } + private static Entry find(DbGroup group, byte[] key) { return group.getEntries().stream() .filter(entry -> Arrays.equals(entry.getKey(), key)) @@ -344,6 +469,16 @@ private static Entry find(DbGroup group, byte[] key) { .orElseThrow(AssertionError::new); } + private static BlockReverseDiff prepared(Chainbase database) { + return ((SnapshotImpl) database.getHead()).getPreparedArchiveBlock(); + } + + private static void setFlushCount(SnapshotManager manager, int count) throws Exception { + java.lang.reflect.Field flushCount = SnapshotManager.class.getDeclaredField("flushCount"); + flushCount.setAccessible(true); + flushCount.setInt(manager, count); + } + private static boolean contains(DbGroup group, byte[] key) { return group.getEntries().stream().anyMatch(entry -> Arrays.equals(entry.getKey(), key)); } @@ -385,7 +520,7 @@ private static Map copy(Map source) { return copy; } - private static final class MemoryDb implements DB { + private static final class MemoryDb implements DB, Flusher { private final String name; private final Map values = new LinkedHashMap<>(); @@ -432,6 +567,22 @@ public void close() { values.clear(); } + @Override + public void flush(Map batch) { + batch.forEach((key, value) -> { + if (value == null || value.getBytes() == null) { + values.remove(key); + } else { + values.put(WrappedByteArray.copyOf(key.getBytes()), value.getBytes()); + } + }); + } + + @Override + public void reset() { + values.clear(); + } + @Override public String getDbName() { return name; diff --git a/framework/src/test/java/org/tron/core/db2/archive/StorageRowKeyCodecTest.java b/framework/src/test/java/org/tron/core/db2/archive/StorageRowKeyCodecTest.java new file mode 100644 index 00000000000..2b0693dc0ee --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StorageRowKeyCodecTest.java @@ -0,0 +1,74 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.tron.common.runtime.vm.DataWord; +import org.tron.core.capsule.StorageRowCapsule; +import org.tron.core.store.StorageRowKeyCodec; +import org.tron.core.vm.program.Storage; + +public class StorageRowKeyCodecTest { + + private static final byte[] ADDRESS = Hex.decode( + "410102030405060708090a0b0c0d0e0f1011121314"); + private static final byte[] SLOT = Hex.decode( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + private static final byte[] TRANSACTION_HASH = Hex.decode( + "f0e0d0c0b0a090807060504030201000112233445566778899aabbccddeeff00"); + + @Test + public void matchesLegacyNormalVersionAndCreate2GoldenVectors() { + assertArrayEquals(Hex.decode( + "20ca5ae32eacb5480afc3d6566816bdb101112131415161718191a1b1c1d1e1f"), + StorageRowKeyCodec.physicalKey(ADDRESS, SLOT, 0, null)); + assertArrayEquals(Hex.decode( + "20ca5ae32eacb5480afc3d6566816bdbdea5e526567e92b0321816a4e895bd2d"), + StorageRowKeyCodec.physicalKey(ADDRESS, SLOT, 1, null)); + assertArrayEquals(Hex.decode( + "9397a7a785754542ff19d0968c0f92d4101112131415161718191a1b1c1d1e1f"), + StorageRowKeyCodec.physicalKey(ADDRESS, SLOT, 0, TRANSACTION_HASH)); + assertArrayEquals(Hex.decode( + "89ab580a96974c01d754858e702bc237101112131415161718191a1b1c1d1e1f"), + StorageRowKeyCodec.physicalKey(ADDRESS, SLOT, 0, new byte[32])); + } + + @Test + public void latestVmStorageUsesTheSharedCodec() { + Storage storage = new Storage(ADDRESS, null); + storage.setContractVersion(1); + storage.generateAddrHash(TRANSACTION_HASH); + DataWord slot = new DataWord(SLOT); + storage.put(slot, new DataWord(1)); + + StorageRowCapsule row = storage.getRowCache().get(slot); + assertArrayEquals(StorageRowKeyCodec.physicalKey(ADDRESS, SLOT, 1, TRANSACTION_HASH), + row.getRowKey()); + } + + @Test + public void copiesOutputsAndRejectsInvalidComponentLengths() { + byte[] address = Arrays.copyOf(ADDRESS, ADDRESS.length); + byte[] slot = Arrays.copyOf(SLOT, SLOT.length); + byte[] transactionHash = Arrays.copyOf(TRANSACTION_HASH, TRANSACTION_HASH.length); + byte[] first = StorageRowKeyCodec.physicalKey(address, slot, 1, transactionHash); + first[0] ^= 1; + + assertArrayEquals(ADDRESS, address); + assertArrayEquals(SLOT, slot); + assertArrayEquals(TRANSACTION_HASH, transactionHash); + assertArrayEquals(Hex.decode( + "9397a7a785754542ff19d0968c0f92d4dea5e526567e92b0321816a4e895bd2d"), + StorageRowKeyCodec.physicalKey(address, slot, 1, transactionHash)); + assertEquals(StorageRowKeyCodec.KEY_BYTES, + StorageRowKeyCodec.physicalKey(address, slot, 0, null).length); + assertThrows(IllegalArgumentException.class, + () -> StorageRowKeyCodec.physicalKey(address, new byte[31], 0, null)); + assertThrows(IllegalArgumentException.class, + () -> StorageRowKeyCodec.physicalKeyFromAddressHash(new byte[31], slot, 0)); + } +} From ad99a4bb385c50a0e7a61d8bface275d1417b00d Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 14 Aug 2026 11:30:15 +0800 Subject: [PATCH 004/161] feat(block): add offline block file tools --- .../java/org/tron/common/utils/BlockFile.java | 317 ++++++++++++++++++ .../org/tron/common/utils/BlockFileTest.java | 85 +++++ .../main/java/common/org/tron/plugins/Db.java | 3 +- .../java/common/org/tron/plugins/DbBlock.java | 10 + .../org/tron/plugins/DbBlockExport.java | 94 ++++++ .../org/tron/plugins/DbBlockExportTest.java | 78 +++++ 6 files changed, 586 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/org/tron/common/utils/BlockFile.java create mode 100644 common/src/test/java/org/tron/common/utils/BlockFileTest.java create mode 100644 plugins/src/main/java/common/org/tron/plugins/DbBlock.java create mode 100644 plugins/src/main/java/common/org/tron/plugins/DbBlockExport.java create mode 100644 plugins/src/test/java/org/tron/plugins/DbBlockExportTest.java diff --git a/common/src/main/java/org/tron/common/utils/BlockFile.java b/common/src/main/java/org/tron/common/utils/BlockFile.java new file mode 100644 index 00000000000..d2a06df98f8 --- /dev/null +++ b/common/src/main/java/org/tron/common/utils/BlockFile.java @@ -0,0 +1,317 @@ +package org.tron.common.utils; + +import com.google.protobuf.InvalidProtocolBufferException; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.zip.CRC32; +import org.tron.protos.Protocol.Block; + +/** + * Versioned container for a consecutive range of raw protobuf blocks. + */ +public final class BlockFile { + + private static final byte[] MAGIC = new byte[] {'T', 'R', 'O', 'N', 'B', 'L', 'K', '1'}; + private static final int VERSION = 1; + private static final int BLOCK_ID_LENGTH = 32; + private static final int MAX_BLOCK_LENGTH = 64 * 1024 * 1024; + + private BlockFile() { + } + + /** Supplies one source record for the requested height. */ + @FunctionalInterface + public interface RecordSource { + Record get(long height) throws IOException; + } + + /** Writes an inclusive, consecutive block range without keeping the range in memory. */ + public static Header write(Path output, long start, long end, boolean overwrite, + RecordSource source) throws IOException { + validateRange(start, end); + Path absoluteOutput = output.toAbsolutePath().normalize(); + if (Files.exists(absoluteOutput) && !overwrite) { + throw new IOException("Output file already exists: " + absoluteOutput); + } + Path parent = absoluteOutput.getParent(); + if (parent == null) { + throw new IOException("Output file must have a parent directory: " + absoluteOutput); + } + Files.createDirectories(parent); + Path temporary = Files.createTempFile(parent, absoluteOutput.getFileName().toString(), ".tmp"); + boolean completed = false; + Header header = new Header(start, end, end - start + 1); + try { + try (DataOutputStream outputStream = new DataOutputStream(new BufferedOutputStream( + Files.newOutputStream(temporary, StandardOpenOption.TRUNCATE_EXISTING)))) { + writeHeader(outputStream, header); + byte[] previousBlockId = null; + for (long height = start; height <= end; height++) { + Record record = source.get(height); + validateRecord(record, height, previousBlockId); + writeRecord(outputStream, record); + previousBlockId = record.getBlockId(); + if (height == Long.MAX_VALUE) { + break; + } + } + } + move(temporary, absoluteOutput, overwrite); + completed = true; + return header; + } finally { + if (!completed) { + Files.deleteIfExists(temporary); + } + } + } + + /** Opens a streaming reader. The caller must close it. */ + public static Reader open(Path input) throws IOException { + return new Reader(input); + } + + private static void validateRange(long start, long end) { + if (start < 0) { + throw new IllegalArgumentException("Start height must be non-negative"); + } + if (end < start) { + throw new IllegalArgumentException("End height must be greater than or equal to start"); + } + if (end - start == Long.MAX_VALUE) { + throw new IllegalArgumentException("Block range is too large"); + } + } + + private static void writeHeader(DataOutputStream output, Header header) throws IOException { + output.write(MAGIC); + output.writeInt(VERSION); + output.writeLong(header.getStart()); + output.writeLong(header.getEnd()); + output.writeLong(header.getCount()); + } + + private static Header readHeader(DataInputStream input) throws IOException { + byte[] magic = new byte[MAGIC.length]; + input.readFully(magic); + if (!Arrays.equals(MAGIC, magic)) { + throw new IOException("Not a java-tron block file"); + } + int version = input.readInt(); + if (version != VERSION) { + throw new IOException("Unsupported block file version: " + version); + } + long start = input.readLong(); + long end = input.readLong(); + long count = input.readLong(); + try { + validateRange(start, end); + } catch (IllegalArgumentException e) { + throw new IOException("Invalid block file range", e); + } + if (count != end - start + 1) { + throw new IOException("Block file count does not match its range"); + } + return new Header(start, end, count); + } + + private static void writeRecord(DataOutputStream output, Record record) throws IOException { + byte[] blockData = record.getBlockData(); + output.writeLong(record.getHeight()); + output.write(record.getBlockId()); + output.writeInt(blockData.length); + output.write(blockData); + output.writeInt(checksum(blockData)); + } + + private static Record readRecord(DataInputStream input) throws IOException { + long height = input.readLong(); + byte[] blockId = new byte[BLOCK_ID_LENGTH]; + input.readFully(blockId); + int blockLength = input.readInt(); + if (blockLength <= 0 || blockLength > MAX_BLOCK_LENGTH) { + throw new IOException("Invalid block length " + blockLength + " at height " + height); + } + byte[] blockData = new byte[blockLength]; + input.readFully(blockData); + int expectedChecksum = input.readInt(); + if (checksum(blockData) != expectedChecksum) { + throw new IOException("Block checksum mismatch at height " + height); + } + return new Record(height, blockId, blockData); + } + + private static void validateRecord(Record record, long expectedHeight, byte[] previousBlockId) + throws IOException { + if (record == null) { + throw new IOException("Missing block at height " + expectedHeight); + } + if (record.getHeight() != expectedHeight) { + throw new IOException("Expected block " + expectedHeight + " but got " + record.getHeight()); + } + Block block = record.getBlock(); + long protoHeight = block.getBlockHeader().getRawData().getNumber(); + if (protoHeight != expectedHeight) { + throw new IOException("Block protobuf height " + protoHeight + + " does not match record height " + expectedHeight); + } + if (previousBlockId != null && !Arrays.equals(previousBlockId, + block.getBlockHeader().getRawData().getParentHash().toByteArray())) { + throw new IOException("Block parent mismatch at height " + expectedHeight); + } + } + + private static int checksum(byte[] value) { + CRC32 crc32 = new CRC32(); + crc32.update(value); + return (int) crc32.getValue(); + } + + private static void move(Path source, Path target, boolean overwrite) throws IOException { + StandardCopyOption[] atomicOptions = overwrite + ? new StandardCopyOption[] {StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING} + : new StandardCopyOption[] {StandardCopyOption.ATOMIC_MOVE}; + try { + Files.move(source, target, atomicOptions); + } catch (AtomicMoveNotSupportedException e) { + StandardCopyOption[] options = overwrite + ? new StandardCopyOption[] {StandardCopyOption.REPLACE_EXISTING} + : new StandardCopyOption[0]; + Files.move(source, target, options); + } + } + + /** Immutable file header. */ + public static final class Header { + private final long start; + private final long end; + private final long count; + + private Header(long start, long end, long count) { + this.start = start; + this.end = end; + this.count = count; + } + + public long getStart() { + return start; + } + + public long getEnd() { + return end; + } + + public long getCount() { + return count; + } + } + + /** One raw block and the ID used as its source database key. */ + public static final class Record { + private final long height; + private final byte[] blockId; + private final byte[] blockData; + + public Record(long height, byte[] blockId, byte[] blockData) { + if (blockId == null || blockId.length != BLOCK_ID_LENGTH) { + throw new IllegalArgumentException("Block ID must be 32 bytes"); + } + if (blockData == null || blockData.length == 0 || blockData.length > MAX_BLOCK_LENGTH) { + throw new IllegalArgumentException("Block data length is invalid"); + } + this.height = height; + this.blockId = Arrays.copyOf(blockId, blockId.length); + this.blockData = Arrays.copyOf(blockData, blockData.length); + } + + public long getHeight() { + return height; + } + + public byte[] getBlockId() { + return Arrays.copyOf(blockId, blockId.length); + } + + public byte[] getBlockData() { + return Arrays.copyOf(blockData, blockData.length); + } + + public Block getBlock() throws IOException { + try { + return Block.parseFrom(blockData); + } catch (InvalidProtocolBufferException e) { + throw new IOException("Invalid block protobuf at height " + height, e); + } + } + } + + /** Streaming block reader with structural and chain-continuity validation. */ + public static final class Reader implements Closeable { + private final DataInputStream input; + private final Header header; + private long recordsRead; + private byte[] previousBlockId; + private boolean endChecked; + + private Reader(Path inputFile) throws IOException { + input = new DataInputStream(new BufferedInputStream(Files.newInputStream(inputFile))); + try { + header = readHeader(input); + } catch (IOException e) { + input.close(); + throw e; + } + } + + public Header getHeader() { + return header; + } + + public boolean hasNext() throws IOException { + if (recordsRead < header.getCount()) { + return true; + } + if (!endChecked) { + endChecked = true; + if (input.read() != -1) { + throw new IOException("Unexpected trailing data after block records"); + } + } + return false; + } + + public Record next() throws IOException { + if (!hasNext()) { + throw new EOFException("No more block records"); + } + long expectedHeight = header.getStart() + recordsRead; + Record record; + try { + record = readRecord(input); + } catch (EOFException e) { + throw new IOException("Truncated block file at height " + expectedHeight, e); + } + validateRecord(record, expectedHeight, previousBlockId); + previousBlockId = record.getBlockId(); + recordsRead++; + return record; + } + + @Override + public void close() throws IOException { + input.close(); + } + } +} diff --git a/common/src/test/java/org/tron/common/utils/BlockFileTest.java b/common/src/test/java/org/tron/common/utils/BlockFileTest.java new file mode 100644 index 00000000000..122ab426a1d --- /dev/null +++ b/common/src/test/java/org/tron/common/utils/BlockFileTest.java @@ -0,0 +1,85 @@ +package org.tron.common.utils; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +import com.google.protobuf.ByteString; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.BlockHeader; + +public class BlockFileTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void shouldRoundTripConsecutiveBlocks() throws Exception { + Path output = temporaryFolder.getRoot().toPath().resolve("blocks.dat"); + BlockFile.Header written = BlockFile.write(output, 10, 12, false, + height -> record(height)); + + assertEquals(10, written.getStart()); + assertEquals(12, written.getEnd()); + assertEquals(3, written.getCount()); + try (BlockFile.Reader reader = BlockFile.open(output)) { + assertEquals(3, reader.getHeader().getCount()); + for (long height = 10; height <= 12; height++) { + BlockFile.Record record = reader.next(); + assertEquals(height, record.getHeight()); + assertArrayEquals(blockId(height), record.getBlockId()); + assertEquals(height, record.getBlock().getBlockHeader().getRawData().getNumber()); + } + assertFalse(reader.hasNext()); + } + } + + @Test + public void shouldRejectCorruptedBlockData() throws Exception { + Path output = temporaryFolder.getRoot().toPath().resolve("corrupted.dat"); + BlockFile.write(output, 10, 10, false, BlockFileTest::record); + byte[] bytes = Files.readAllBytes(output); + int firstBlockByte = 8 + Integer.BYTES + Long.BYTES * 3 + + Long.BYTES + 32 + Integer.BYTES; + bytes[firstBlockByte] ^= 1; + Files.write(output, bytes); + + try (BlockFile.Reader reader = BlockFile.open(output)) { + assertThrows(IOException.class, reader::next); + } + } + + @Test + public void shouldNotOverwriteByDefault() throws Exception { + Path output = temporaryFolder.newFile("existing.dat").toPath(); + assertThrows(IOException.class, + () -> BlockFile.write(output, 1, 1, false, BlockFileTest::record)); + } + + private static BlockFile.Record record(long height) { + byte[] parent = height == 10 ? new byte[32] : blockId(height - 1); + Block block = Block.newBuilder() + .setBlockHeader(BlockHeader.newBuilder() + .setRawData(BlockHeader.raw.newBuilder() + .setNumber(height) + .setTimestamp(height * 3000) + .setParentHash(ByteString.copyFrom(parent)))) + .build(); + return new BlockFile.Record(height, blockId(height), block.toByteArray()); + } + + private static byte[] blockId(long height) { + byte[] value = new byte[32]; + byte[] heightBytes = ByteArray.fromLong(height); + System.arraycopy(heightBytes, 0, value, 0, heightBytes.length); + value[31] = (byte) height; + return value; + } +} diff --git a/plugins/src/main/java/common/org/tron/plugins/Db.java b/plugins/src/main/java/common/org/tron/plugins/Db.java index 84654dca934..daf153ee8b1 100644 --- a/plugins/src/main/java/common/org/tron/plugins/Db.java +++ b/plugins/src/main/java/common/org/tron/plugins/Db.java @@ -12,7 +12,8 @@ DbConvert.class, DbLite.class, DbCopy.class, - DbRoot.class + DbRoot.class, + DbBlock.class }, commandListHeading = "%nCommands:%n%nThe most commonly used db commands are:%n" ) diff --git a/plugins/src/main/java/common/org/tron/plugins/DbBlock.java b/plugins/src/main/java/common/org/tron/plugins/DbBlock.java new file mode 100644 index 00000000000..99a08706fb9 --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/DbBlock.java @@ -0,0 +1,10 @@ +package org.tron.plugins; + +import picocli.CommandLine; + +@CommandLine.Command(name = "block", + mixinStandardHelpOptions = true, + description = "Export block data for offline replay.", + subcommands = {CommandLine.HelpCommand.class, DbBlockExport.class}) +public class DbBlock { +} diff --git a/plugins/src/main/java/common/org/tron/plugins/DbBlockExport.java b/plugins/src/main/java/common/org/tron/plugins/DbBlockExport.java new file mode 100644 index 00000000000..aa6e7a26852 --- /dev/null +++ b/plugins/src/main/java/common/org/tron/plugins/DbBlockExport.java @@ -0,0 +1,94 @@ +package org.tron.plugins; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.Callable; +import org.tron.common.utils.BlockFile; +import org.tron.plugins.utils.ByteArray; +import org.tron.plugins.utils.db.DBInterface; +import org.tron.plugins.utils.db.DbTool; +import picocli.CommandLine; + +@CommandLine.Command(name = "export", + mixinStandardHelpOptions = true, + description = "Export an inclusive range of consecutive blocks from a stopped node.") +public class DbBlockExport implements Callable { + + private static final String BLOCK_DB_NAME = "block"; + private static final String BLOCK_INDEX_DB_NAME = "block-index"; + + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @CommandLine.Option(names = {"-d", "--database-directory"}, required = true, + description = "Node database directory containing block and block-index.") + private Path databaseDirectory; + + @CommandLine.Option(names = "--start", required = true, + description = "First block height, inclusive.") + private long start; + + @CommandLine.Option(names = "--end", required = true, + description = "Last block height, inclusive.") + private long end; + + @CommandLine.Option(names = {"-o", "--output"}, required = true, + description = "Destination block file.") + private Path output; + + @CommandLine.Option(names = "--overwrite", + description = "Replace an existing destination file.") + private boolean overwrite; + + @Override + public Integer call() throws Exception { + Path database = databaseDirectory.toAbsolutePath().normalize(); + requireExistingDatabase(database, BLOCK_DB_NAME); + requireExistingDatabase(database, BLOCK_INDEX_DB_NAME); + + String databasePath = database.toString(); + DBInterface blockIndex = null; + DBInterface block = null; + try { + blockIndex = DbTool.getDB(databasePath, BLOCK_INDEX_DB_NAME); + block = DbTool.getDB(databasePath, BLOCK_DB_NAME); + DBInterface sourceBlockIndex = blockIndex; + DBInterface sourceBlock = block; + BlockFile.Header header = BlockFile.write(output, start, end, overwrite, height -> { + byte[] blockId = sourceBlockIndex.get(ByteArray.fromLong(height)); + if (blockId == null) { + throw new IOException("Block index is missing height " + height); + } + byte[] blockData = sourceBlock.get(blockId); + if (blockData == null) { + throw new IOException("Block data is missing height " + height); + } + return new BlockFile.Record(height, blockId, blockData); + }); + spec.commandLine().getOut().printf( + "Exported %d blocks [%d, %d] to %s%n", + header.getCount(), header.getStart(), header.getEnd(), + output.toAbsolutePath().normalize()); + return 0; + } finally { + close(databasePath, BLOCK_DB_NAME, block); + close(databasePath, BLOCK_INDEX_DB_NAME, blockIndex); + } + } + + private void requireExistingDatabase(Path database, String name) { + Path path = database.resolve(name); + if (!Files.isDirectory(path)) { + throw new CommandLine.ParameterException(spec.commandLine(), + "Database does not exist: " + path); + } + } + + private static void close(String database, String name, DBInterface db) throws IOException { + if (db != null) { + DbTool.closeDB(database, name); + } + } +} diff --git a/plugins/src/test/java/org/tron/plugins/DbBlockExportTest.java b/plugins/src/test/java/org/tron/plugins/DbBlockExportTest.java new file mode 100644 index 00000000000..3d28f4debdf --- /dev/null +++ b/plugins/src/test/java/org/tron/plugins/DbBlockExportTest.java @@ -0,0 +1,78 @@ +package org.tron.plugins; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import com.google.protobuf.ByteString; +import java.nio.file.Path; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.utils.BlockFile; +import org.tron.plugins.utils.ByteArray; +import org.tron.plugins.utils.db.DBInterface; +import org.tron.plugins.utils.db.DbTool; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.BlockHeader; +import picocli.CommandLine; + +public class DbBlockExportTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @After + public void tearDown() { + DbTool.close(); + } + + @Test + public void shouldExportBlocksFromDatabase() throws Exception { + Path database = temporaryFolder.newFolder("database").toPath(); + DBInterface blockIndex = DbTool.getDB(database.toString(), "block-index", + DbTool.DbType.RocksDB); + DBInterface blockDb = DbTool.getDB(database.toString(), "block", DbTool.DbType.RocksDB); + byte[] previousId = new byte[32]; + for (long height = 20; height <= 21; height++) { + byte[] id = blockId(height); + Block block = block(height, previousId); + blockIndex.put(ByteArray.fromLong(height), id); + blockDb.put(id, block.toByteArray()); + previousId = id; + } + DbTool.closeDB(database.toString(), "block-index"); + DbTool.closeDB(database.toString(), "block"); + + Path output = temporaryFolder.getRoot().toPath().resolve("blocks.dat"); + CommandLine cli = new CommandLine(new Toolkit()); + assertEquals(0, cli.execute("db", "block", "export", + "-d", database.toString(), "--start", "20", "--end", "21", + "-o", output.toString())); + + try (BlockFile.Reader reader = BlockFile.open(output)) { + assertEquals(2, reader.getHeader().getCount()); + assertEquals(20, reader.next().getHeight()); + assertEquals(21, reader.next().getHeight()); + assertFalse(reader.hasNext()); + } + } + + private static Block block(long height, byte[] parent) { + return Block.newBuilder() + .setBlockHeader(BlockHeader.newBuilder() + .setRawData(BlockHeader.raw.newBuilder() + .setNumber(height) + .setTimestamp(height * 3000) + .setParentHash(ByteString.copyFrom(parent)))) + .build(); + } + + private static byte[] blockId(long height) { + byte[] value = new byte[32]; + byte[] heightBytes = ByteArray.fromLong(height); + System.arraycopy(heightBytes, 0, value, 0, heightBytes.length); + value[31] = (byte) height; + return value; + } +} From 036e09805e93d06b45c367de345daebbad5eaba2 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 14 Aug 2026 11:31:11 +0800 Subject: [PATCH 005/161] feat(block): add offline block replay --- .../tron/core/service/RewardViCalService.java | 54 ++-- .../main/java/org/tron/core/db/Manager.java | 1 + .../java/org/tron/program/BlockReplay.java | 242 ++++++++++++++++++ .../RewardViCalServiceLifecycleTest.java | 65 +++++ .../org/tron/program/BlockReplayTest.java | 134 ++++++++++ 5 files changed, 481 insertions(+), 15 deletions(-) create mode 100644 framework/src/main/java/org/tron/program/BlockReplay.java create mode 100644 framework/src/test/java/org/tron/core/service/RewardViCalServiceLifecycleTest.java create mode 100644 framework/src/test/java/org/tron/program/BlockReplayTest.java diff --git a/chainbase/src/main/java/org/tron/core/service/RewardViCalService.java b/chainbase/src/main/java/org/tron/core/service/RewardViCalService.java index f88fd02c539..acc45e9cf81 100644 --- a/chainbase/src/main/java/org/tron/core/service/RewardViCalService.java +++ b/chainbase/src/main/java/org/tron/core/service/RewardViCalService.java @@ -3,7 +3,6 @@ import static org.tron.core.store.DelegationStore.DECIMAL_OF_VI_REWARD; import static org.tron.core.store.DelegationStore.REMARK; -import com.google.common.collect.Streams; import com.google.common.primitives.Bytes; import com.google.protobuf.ByteString; import java.math.BigInteger; @@ -14,8 +13,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.LongStream; import javax.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.encoders.Hex; @@ -65,6 +62,8 @@ public class RewardViCalService { private final ScheduledExecutorService es = ExecutorServiceManager .newSingleThreadScheduledExecutor("rewardViCalService"); + private volatile boolean stopped; + @Autowired public RewardViCalService(@Autowired DynamicPropertiesStore propertiesStore, @@ -99,6 +98,9 @@ private boolean isDone() { } private void maybeRun() { + if (stopped) { + return; + } try { if (enableNewRewardAlgorithm()) { if (this.newRewardCalStartCycle > 1) { @@ -135,8 +137,10 @@ private void clearUp(boolean isDone) { } @PreDestroy - private void destroy() { + public void stop() { + stopped = true; es.shutdownNow(); + ExecutorServiceManager.shutdownAndAwaitTermination(es, "rewardViCalService"); } @@ -173,11 +177,20 @@ public long getNewRewardAlgorithmReward(long beginCycle, long endCycle, private void calcMerkleRoot() { logger.info("calcMerkleRoot start"); - DBIterator iterator = rewardViStore.iterator(); - iterator.seekToFirst(); - ArrayList ids = Streams.stream(iterator) - .map(this::getHash) - .collect(Collectors.toCollection(ArrayList::new)); + ArrayList ids; + try (DBIterator iterator = rewardViStore.iterator()) { + iterator.seekToFirst(); + ids = new ArrayList<>(); + while (!stopped && iterator.hasNext()) { + ids.add(getHash(iterator.next())); + } + } catch (Exception e) { + throw new TronDBException(e); + } + + if (stopped) { + return; + } Sha256Hash rewardViRootLocal = MerkleRoot.root(ids); if (!Objects.equals(rewardViRoot, rewardViRootLocal)) { @@ -198,9 +211,17 @@ private Sha256Hash getHash(Map.Entry entry) { private void startRewardCal() { logger.info("rewardViCalService start"); rewardViStore.reset(); - DBIterator iterator = (DBIterator) witnessStore.iterator(); - iterator.seekToFirst(); - iterator.forEachRemaining(e -> accumulateWitnessReward(e.getKey())); + try (DBIterator iterator = (DBIterator) witnessStore.iterator()) { + iterator.seekToFirst(); + while (!stopped && iterator.hasNext()) { + accumulateWitnessReward(iterator.next().getKey()); + } + } catch (Exception e) { + throw new TronDBException(e); + } + if (stopped) { + return; + } rewardViStore.put(IS_DONE_KEY, IS_DONE_VALUE); logger.info("rewardViCalService is done"); @@ -208,11 +229,15 @@ private void startRewardCal() { private void accumulateWitnessReward(byte[] witness) { long startCycle = 1; - LongStream.range(startCycle, newRewardCalStartCycle) - .forEach(cycle -> accumulateWitnessVi(cycle, witness)); + for (long cycle = startCycle; cycle < newRewardCalStartCycle && !stopped; cycle++) { + accumulateWitnessVi(cycle, witness); + } } private void accumulateWitnessVi(long cycle, byte[] address) { + if (stopped) { + return; + } BigInteger preVi = getWitnessVi(cycle - 1, address); long voteCount = getWitnessVote(cycle, address); long reward = getReward(cycle, address); @@ -284,4 +309,3 @@ private long getLatestBlockHeaderNumber() { return value == null ? 1 : ByteArray.toLong(value); } } - diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index ee03768c8ca..f4dd479c159 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -2745,6 +2745,7 @@ public void close() { EventPluginLoader.getInstance().stopPlugin(); stopFilterProcessThread(); stopValidateSignThread(); + rewardViCalService.stop(); chainBaseManager.shutdown(); revokingStore.shutdown(); session.reset(); diff --git a/framework/src/main/java/org/tron/program/BlockReplay.java b/framework/src/main/java/org/tron/program/BlockReplay.java new file mode 100644 index 00000000000..a650501bc6e --- /dev/null +++ b/framework/src/main/java/org/tron/program/BlockReplay.java @@ -0,0 +1,242 @@ +package org.tron.program; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParameterException; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Locale; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.tron.common.application.TronApplicationContext; +import org.tron.common.log.LogService; +import org.tron.common.prometheus.Metrics; +import org.tron.common.utils.BlockFile; +import org.tron.core.ChainBaseManager; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.config.DefaultConfig; +import org.tron.core.config.args.Args; +import org.tron.core.consensus.ConsensusService; +import org.tron.core.net.TronNetDelegate; + +/** + * Offline block replay entry point for fixed-window database benchmarks. + */ +@Slf4j(topic = "app") +public final class BlockReplay { + + private BlockReplay() { + } + + public static void main(String[] args) { + int exitCode = execute(args, System.out, System.err); + if (exitCode != 0) { + System.exit(exitCode); + } + } + + static int execute(String[] args, PrintStream output, PrintStream error) { + Options options = new Options(); + JCommander commander = JCommander.newBuilder() + .addObject(options) + .programName("BlockReplay") + .build(); + try { + commander.parse(args); + if (options.help) { + commander.usage(); + return 0; + } + options.validate(); + ReplayResult result = options.apply + ? apply(options) + : verify(options); + output.println(result.format()); + return 0; + } catch (ParameterException | IllegalArgumentException e) { + error.println(e.getMessage()); + commander.usage(); + return 2; + } catch (Exception e) { + logger.error("Block replay failed", e); + error.println("Block replay failed: " + e.getMessage()); + return 1; + } finally { + Args.clearParam(); + } + } + + private static ReplayResult verify(Options options) throws Exception { + Args.setParam(new String[] {"-c", options.config}, "config.conf"); + return replay(Paths.get(options.input), null, null, options.warmupBlocks, + options.maxBlocks, false); + } + + private static ReplayResult apply(Options options) throws Exception { + Path outputDirectory = Paths.get(options.outputDirectory).toAbsolutePath().normalize(); + if (!Files.isDirectory(outputDirectory)) { + throw new IllegalArgumentException( + "Output directory must be an existing D0 snapshot: " + outputDirectory); + } + Args.setParam(new String[] {"-c", options.config, "-d", outputDirectory.toString(), + "--p2p-disable", "true"}, "config.conf"); + LogService.load(Args.getInstance().getLogbackPath()); + Metrics.init(); + + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.setAllowCircularReferences(false); + TronApplicationContext context = new TronApplicationContext(beanFactory); + try { + context.register(DefaultConfig.class); + context.refresh(); + startConsensus(context); + return replay(Paths.get(options.input), context.getBean(TronNetDelegate.class), + context.getBean(ChainBaseManager.class), options.warmupBlocks, + options.maxBlocks, true); + } finally { + context.close(); + } + } + + static void startConsensus(TronApplicationContext context) { + context.getBean(ConsensusService.class).start(); + } + + static ReplayResult replay(Path input, TronNetDelegate tronNetDelegate, + ChainBaseManager chainBaseManager, + long warmupBlocks, long maxBlocks, boolean apply) throws Exception { + try (BlockFile.Reader reader = BlockFile.open(input)) { + BlockFile.Header header = reader.getHeader(); + long selectedCount = Math.min(header.getCount(), maxBlocks); + if (apply) { + long expectedStart = chainBaseManager.getHeadBlockNum() + 1; + if (header.getStart() != expectedStart) { + throw new IllegalArgumentException("Block file starts at " + header.getStart() + + " but D0 requires block " + expectedStart); + } + } + + long processed = 0; + long measured = 0; + long measuredNanos = 0; + long lastHeight = header.getStart() - 1; + while (processed < selectedCount && reader.hasNext()) { + BlockFile.Record record = reader.next(); + BlockCapsule block = new BlockCapsule(record.getBlock()); + if (!Arrays.equals(record.getBlockId(), block.getBlockId().getBytes())) { + throw new IOException("Computed block ID mismatch at height " + record.getHeight()); + } + if (apply && processed == 0 && !block.getParentHash().equals( + chainBaseManager.getHeadBlockId())) { + throw new IllegalArgumentException("First block parent does not match D0 head"); + } + + long startNanos = apply ? System.nanoTime() : 0; + if (apply) { + tronNetDelegate.processBlock(block, true); + if (chainBaseManager.getHeadBlockNum() != record.getHeight() + || !chainBaseManager.getHeadBlockId().equals(block.getBlockId())) { + throw new IllegalStateException( + "D0 head did not advance to block " + record.getHeight()); + } + } + long elapsedNanos = apply ? System.nanoTime() - startNanos : 0; + if (processed >= warmupBlocks) { + measuredNanos += elapsedNanos; + measured++; + } + processed++; + lastHeight = record.getHeight(); + } + if (selectedCount == header.getCount()) { + reader.hasNext(); + } + return new ReplayResult(apply, header.getStart(), lastHeight, processed, + Math.min(warmupBlocks, processed), measured, measuredNanos); + } + } + + static final class Options { + @Parameter(names = {"-i", "--input"}, description = "Block file to verify or replay.") + private String input; + + @Parameter(names = {"-d", "--output-directory"}, + description = "Existing stopped-node D0 output directory. Required with --apply.") + private String outputDirectory; + + @Parameter(names = {"-c", "--config"}, description = "Node config file.") + private String config = "config.conf"; + + @Parameter(names = "--apply", + description = "Apply blocks to D0. Without this flag the command only verifies the file.") + private boolean apply; + + @Parameter(names = "--warmup-blocks", + description = "Exclude this many leading blocks from measured time.") + private long warmupBlocks; + + @Parameter(names = "--max-blocks", + description = "Process at most this many blocks from the file.") + private long maxBlocks = Long.MAX_VALUE; + + @Parameter(names = {"-h", "--help"}, help = true) + private boolean help; + + private void validate() { + if (input == null || input.trim().isEmpty()) { + throw new ParameterException("--input is required"); + } + if (!Files.isRegularFile(Paths.get(input))) { + throw new ParameterException("Block file does not exist: " + input); + } + if (config == null || config.trim().isEmpty()) { + throw new ParameterException("--config must not be empty"); + } + if (apply && (outputDirectory == null || outputDirectory.trim().isEmpty())) { + throw new ParameterException("--output-directory is required with --apply"); + } + if (warmupBlocks < 0) { + throw new ParameterException("--warmup-blocks must be non-negative"); + } + if (maxBlocks <= 0) { + throw new ParameterException("--max-blocks must be positive"); + } + } + } + + static final class ReplayResult { + private final boolean applied; + private final long start; + private final long end; + private final long processed; + private final long warmup; + private final long measured; + private final long measuredNanos; + + private ReplayResult(boolean applied, long start, long end, long processed, + long warmup, long measured, long measuredNanos) { + this.applied = applied; + this.start = start; + this.end = end; + this.processed = processed; + this.warmup = warmup; + this.measured = measured; + this.measuredNanos = measuredNanos; + } + + String format() { + double elapsedMs = measuredNanos / 1_000_000.0; + double blocksPerSecond = measuredNanos == 0 ? 0.0 + : measured * 1_000_000_000.0 / measuredNanos; + return String.format(Locale.ROOT, + "mode=%s range=[%d,%d] processed=%d warmup=%d measured=%d " + + "elapsed_ms=%.3f blocks_per_second=%.3f", + applied ? "apply" : "verify", start, end, processed, warmup, measured, + elapsedMs, blocksPerSecond); + } + } +} diff --git a/framework/src/test/java/org/tron/core/service/RewardViCalServiceLifecycleTest.java b/framework/src/test/java/org/tron/core/service/RewardViCalServiceLifecycleTest.java new file mode 100644 index 00000000000..3cfdbec225e --- /dev/null +++ b/framework/src/test/java/org/tron/core/service/RewardViCalServiceLifecycleTest.java @@ -0,0 +1,65 @@ +package org.tron.core.service; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.tron.common.TestConstants; +import org.tron.common.utils.ReflectUtils; +import org.tron.core.config.args.Args; +import org.tron.core.db2.common.DB; +import org.tron.core.store.DelegationStore; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.core.store.WitnessStore; + +public class RewardViCalServiceLifecycleTest { + + @Test + public void shouldInterruptAndAwaitWorkerBeforeReturningFromStop() throws Exception { + Args.setParam(new String[0], TestConstants.TEST_CONF); + RewardViCalService service = null; + try { + DynamicPropertiesStore propertiesStore = mock(DynamicPropertiesStore.class); + DelegationStore delegationStore = mock(DelegationStore.class); + WitnessStore witnessStore = mock(WitnessStore.class); + DB db = mockDb(); + when(propertiesStore.getDb()).thenReturn(db); + when(delegationStore.getDb()).thenReturn(db); + when(witnessStore.getDb()).thenReturn(db); + service = new RewardViCalService(propertiesStore, delegationStore, witnessStore); + ScheduledExecutorService executor = ReflectUtils.getFieldValue(service, "es"); + CountDownLatch started = new CountDownLatch(1); + CountDownLatch interrupted = new CountDownLatch(1); + executor.execute(() -> { + started.countDown(); + try { + new CountDownLatch(1).await(); + } catch (InterruptedException e) { + interrupted.countDown(); + Thread.currentThread().interrupt(); + } + }); + assertTrue(started.await(5, TimeUnit.SECONDS)); + + service.stop(); + service.stop(); + + assertTrue(interrupted.await(5, TimeUnit.SECONDS)); + assertTrue(executor.isTerminated()); + } finally { + if (service != null) { + service.stop(); + } + Args.clearParam(); + } + } + + @SuppressWarnings("unchecked") + private static DB mockDb() { + return mock(DB.class); + } +} diff --git a/framework/src/test/java/org/tron/program/BlockReplayTest.java b/framework/src/test/java/org/tron/program/BlockReplayTest.java new file mode 100644 index 00000000000..156839316fa --- /dev/null +++ b/framework/src/test/java/org/tron/program/BlockReplayTest.java @@ -0,0 +1,134 @@ +package org.tron.program; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.file.Path; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.ArgumentCaptor; +import org.tron.common.application.TronApplicationContext; +import org.tron.common.utils.BlockFile; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.ChainBaseManager; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.BlockCapsule.BlockId; +import org.tron.core.consensus.ConsensusService; +import org.tron.core.net.TronNetDelegate; + +public class BlockReplayTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void shouldVerifyACompleteFileWithoutApplying() throws Exception { + BlockId parent = new BlockId(Sha256Hash.ZERO_HASH, 9); + BlockCapsule[] blocks = blocks(parent, 10, 2); + Path input = write(blocks); + + BlockReplay.ReplayResult result = BlockReplay.replay(input, null, null, 1, + Long.MAX_VALUE, false); + + assertTrue(result.format().contains("mode=verify")); + assertTrue(result.format().contains("processed=2")); + assertTrue(result.format().contains("measured=1")); + } + + @Test + public void shouldVerifyThroughCommandLine() throws Exception { + BlockId parent = new BlockId(Sha256Hash.ZERO_HASH, 9); + Path input = write(blocks(parent, 10, 2)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ByteArrayOutputStream error = new ByteArrayOutputStream(); + + int exitCode = BlockReplay.execute(new String[] {"--input", input.toString()}, + new PrintStream(output), new PrintStream(error)); + + assertEquals(error.toString(), 0, exitCode); + assertTrue(output.toString().contains("mode=verify")); + assertTrue(output.toString().contains("processed=2")); + } + + @Test + public void shouldApplyBlocksThroughSyncPath() throws Exception { + BlockId parent = new BlockId(Sha256Hash.ZERO_HASH, 9); + BlockCapsule[] blocks = blocks(parent, 10, 2); + Path input = write(blocks); + TronNetDelegate tronNetDelegate = mock(TronNetDelegate.class); + ChainBaseManager chainBaseManager = mock(ChainBaseManager.class); + when(chainBaseManager.getHeadBlockNum()).thenReturn(9L, 10L, 11L); + when(chainBaseManager.getHeadBlockId()).thenReturn(parent, blocks[0].getBlockId(), + blocks[1].getBlockId()); + + BlockReplay.ReplayResult result = BlockReplay.replay(input, tronNetDelegate, chainBaseManager, + 0, Long.MAX_VALUE, true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BlockCapsule.class); + verify(tronNetDelegate, times(2)).processBlock(captor.capture(), eq(true)); + assertEquals(blocks[0].getBlockId(), captor.getAllValues().get(0).getBlockId()); + assertEquals(blocks[1].getBlockId(), captor.getAllValues().get(1).getBlockId()); + assertTrue(result.format().contains("mode=apply")); + assertTrue(result.format().contains("processed=2")); + } + + @Test + public void shouldStartConsensusBeforeApplyingBlocks() { + TronApplicationContext context = mock(TronApplicationContext.class); + ConsensusService consensusService = mock(ConsensusService.class); + when(context.getBean(ConsensusService.class)).thenReturn(consensusService); + + BlockReplay.startConsensus(context); + + verify(consensusService).start(); + } + + @Test + public void shouldRejectD0WithDifferentHead() throws Exception { + BlockId fileParent = new BlockId(Sha256Hash.ZERO_HASH, 9); + BlockCapsule[] blocks = blocks(fileParent, 10, 1); + Path input = write(blocks); + TronNetDelegate tronNetDelegate = mock(TronNetDelegate.class); + ChainBaseManager chainBaseManager = mock(ChainBaseManager.class); + when(chainBaseManager.getHeadBlockNum()).thenReturn(9L); + when(chainBaseManager.getHeadBlockId()) + .thenReturn(new BlockId(Sha256Hash.of(true, new byte[] {1}), 9)); + + assertThrows(IllegalArgumentException.class, + () -> BlockReplay.replay(input, tronNetDelegate, chainBaseManager, + 0, Long.MAX_VALUE, true)); + verify(tronNetDelegate, times(0)).processBlock(any(BlockCapsule.class), eq(true)); + } + + private Path write(BlockCapsule[] blocks) throws Exception { + Path input = temporaryFolder.getRoot().toPath().resolve("blocks.dat"); + BlockFile.write(input, blocks[0].getNum(), blocks[blocks.length - 1].getNum(), false, + height -> { + BlockCapsule block = blocks[(int) (height - blocks[0].getNum())]; + return new BlockFile.Record(height, block.getBlockId().getBytes(), block.getData()); + }); + return input; + } + + private static BlockCapsule[] blocks(BlockId parent, long start, int count) { + BlockCapsule[] blocks = new BlockCapsule[count]; + BlockId previous = parent; + for (int i = 0; i < count; i++) { + long height = start + i; + blocks[i] = new BlockCapsule(height, previous, height * 3000, ByteString.EMPTY); + previous = blocks[i].getBlockId(); + } + return blocks; + } +} From 1fcd7b167154425606f123e72b7735d23f14659d Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 14 Aug 2026 11:38:36 +0800 Subject: [PATCH 006/161] fix(block): flush replay snapshot tail --- .../org/tron/core/db/RevokingDatabase.java | 2 ++ .../tron/core/db2/core/SnapshotManager.java | 13 +++++++-- .../java/org/tron/program/BlockReplay.java | 9 +++++- .../tron/core/db2/SnapshotManagerTest.java | 28 +++++++++++++++++++ .../org/tron/program/BlockReplayTest.java | 12 ++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db/RevokingDatabase.java b/chainbase/src/main/java/org/tron/core/db/RevokingDatabase.java index ff2f026192c..7a6b13bf6be 100755 --- a/chainbase/src/main/java/org/tron/core/db/RevokingDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db/RevokingDatabase.java @@ -39,6 +39,8 @@ public interface RevokingDatabase { void setMaxFlushCount(int maxFlushCount); + void flushPending(); + void shutdown(); } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index dc9fabe5bfc..38f34307b1f 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -446,11 +446,20 @@ private void refreshOne(Chainbase db) { } public void flush() { - if (unChecked) { + flush(false); + } + + @Override + public void flushPending() { + flush(true); + } + + private synchronized void flush(boolean force) { + if (unChecked || (force && flushCount == 0)) { return; } - if (shouldBeRefreshed()) { + if (force || shouldBeRefreshed()) { try { long start = System.currentTimeMillis(); Long archiveEpoch = publishArchiveHistoryForFlush(); diff --git a/framework/src/main/java/org/tron/program/BlockReplay.java b/framework/src/main/java/org/tron/program/BlockReplay.java index a650501bc6e..d3b3fdb424e 100644 --- a/framework/src/main/java/org/tron/program/BlockReplay.java +++ b/framework/src/main/java/org/tron/program/BlockReplay.java @@ -21,6 +21,7 @@ import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; import org.tron.core.consensus.ConsensusService; +import org.tron.core.db.RevokingDatabase; import org.tron.core.net.TronNetDelegate; /** @@ -94,9 +95,11 @@ private static ReplayResult apply(Options options) throws Exception { context.register(DefaultConfig.class); context.refresh(); startConsensus(context); - return replay(Paths.get(options.input), context.getBean(TronNetDelegate.class), + ReplayResult result = replay(Paths.get(options.input), context.getBean(TronNetDelegate.class), context.getBean(ChainBaseManager.class), options.warmupBlocks, options.maxBlocks, true); + flushPending(context); + return result; } finally { context.close(); } @@ -106,6 +109,10 @@ static void startConsensus(TronApplicationContext context) { context.getBean(ConsensusService.class).start(); } + static void flushPending(TronApplicationContext context) { + context.getBean(RevokingDatabase.class).flushPending(); + } + static ReplayResult replay(Path input, TronNetDelegate tronNetDelegate, ChainBaseManager chainBaseManager, long warmupBlocks, long maxBlocks, boolean apply) throws Exception { diff --git a/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java b/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java index ae16776a7c6..8830c472e8e 100644 --- a/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java +++ b/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java @@ -95,6 +95,34 @@ public synchronized void testClose() { } + @Test + public synchronized void testFlushPendingBelowBatchThreshold() { + while (revokingDatabase.size() != 0) { + revokingDatabase.pop(); + } + + revokingDatabase.setUnChecked(false); + revokingDatabase.setMaxSize(5); + revokingDatabase.setMaxFlushCount(20); + ProtoCapsuleTest key = new ProtoCapsuleTest("flush-pending".getBytes()); + for (int i = 1; i <= 12; i++) { + try (ISession session = revokingDatabase.buildSession()) { + tronDatabase.put(key.getData(), new ProtoCapsuleTest(("value" + i).getBytes())); + session.commit(); + } + } + + Assert.assertFalse(revokingDatabase.shouldBeRefreshed()); + revokingDatabase.setMaxFlushCount(1); + Assert.assertTrue(revokingDatabase.shouldBeRefreshed()); + revokingDatabase.setMaxFlushCount(20); + + revokingDatabase.flushPending(); + + revokingDatabase.setMaxFlushCount(1); + Assert.assertFalse(revokingDatabase.shouldBeRefreshed()); + } + @Test public void testCheckError() { SnapshotManager manager = spy(new SnapshotManager("")); diff --git a/framework/src/test/java/org/tron/program/BlockReplayTest.java b/framework/src/test/java/org/tron/program/BlockReplayTest.java index 156839316fa..b690525b426 100644 --- a/framework/src/test/java/org/tron/program/BlockReplayTest.java +++ b/framework/src/test/java/org/tron/program/BlockReplayTest.java @@ -25,6 +25,7 @@ import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.consensus.ConsensusService; +import org.tron.core.db.RevokingDatabase; import org.tron.core.net.TronNetDelegate; public class BlockReplayTest { @@ -94,6 +95,17 @@ public void shouldStartConsensusBeforeApplyingBlocks() { verify(consensusService).start(); } + @Test + public void shouldFlushPendingSnapshotsAfterSuccessfulReplay() { + TronApplicationContext context = mock(TronApplicationContext.class); + RevokingDatabase revokingDatabase = mock(RevokingDatabase.class); + when(context.getBean(RevokingDatabase.class)).thenReturn(revokingDatabase); + + BlockReplay.flushPending(context); + + verify(revokingDatabase).flushPending(); + } + @Test public void shouldRejectD0WithDifferentHead() throws Exception { BlockId fileParent = new BlockId(Sha256Hash.ZERO_HASH, 9); From 5548731c93d14e831afce0b3f9460116dd55e256 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 18 Aug 2026 18:48:39 +0800 Subject: [PATCH 007/161] feat(block): add resumable HTTP export --- .../org/tron/program/HttpBlockExport.java | 325 ++++++++++++++++++ .../org/tron/program/HttpBlockExportTest.java | 89 +++++ 2 files changed, 414 insertions(+) create mode 100644 framework/src/main/java/org/tron/program/HttpBlockExport.java create mode 100644 framework/src/test/java/org/tron/program/HttpBlockExportTest.java diff --git a/framework/src/main/java/org/tron/program/HttpBlockExport.java b/framework/src/main/java/org/tron/program/HttpBlockExport.java new file mode 100644 index 00000000000..c7a719759bf --- /dev/null +++ b/framework/src/main/java/org/tron/program/HttpBlockExport.java @@ -0,0 +1,325 @@ +package org.tron.program; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParameterException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.tron.common.utils.BlockFile; +import org.tron.common.utils.ByteArray; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.services.http.JsonFormat; +import org.tron.json.JSONArray; +import org.tron.json.JSONObject; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.BlockHeader; +import org.tron.protos.Protocol.Transaction; + +/** Downloads consecutive blocks from a java-tron HTTP API into resumable block-file chunks. */ +public final class HttpBlockExport { + + static final int MAX_CHUNK_SIZE = 100; + + private HttpBlockExport() { + } + + public static void main(String[] args) { + int exitCode = execute(args, System.out, System.err); + if (exitCode != 0) { + System.exit(exitCode); + } + } + + static int execute(String[] args, java.io.PrintStream output, java.io.PrintStream error) { + Options options = new Options(); + JCommander commander = JCommander.newBuilder() + .addObject(options) + .programName("HttpBlockExport") + .build(); + try { + commander.parse(args); + if (options.help) { + commander.usage(); + return 0; + } + options.validate(); + Files.createDirectories(Paths.get(options.outputDirectory)); + export(options, output); + return 0; + } catch (ParameterException | IllegalArgumentException e) { + error.println(e.getMessage()); + commander.usage(); + return 2; + } catch (Exception e) { + error.println("HTTP block export failed: " + e.getMessage()); + return 1; + } + } + + private static void export(Options options, java.io.PrintStream output) throws IOException { + long next = options.start; + while (next <= options.end) { + long chunkEnd = Math.min(options.end, next + options.chunkSize - 1L); + Path target = Paths.get(options.outputDirectory) + .resolve(String.format("%d-%d.dat", next, chunkEnd)); + if (Files.isRegularFile(target) && !options.overwrite) { + verifyChunk(target, next, chunkEnd); + output.printf("Reused verified chunk [%d, %d] %s%n", next, chunkEnd, target); + } else { + List records = fetchChunk(options, next, chunkEnd); + long chunkStart = next; + BlockFile.write(target, next, chunkEnd, options.overwrite, + height -> records.get(Math.toIntExact(height - chunkStart))); + output.printf("Downloaded chunk [%d, %d] %s%n", next, chunkEnd, target); + } + if (chunkEnd == Long.MAX_VALUE) { + break; + } + next = chunkEnd + 1; + } + } + + static void verifyChunk(Path input, long expectedStart, long expectedEnd) throws IOException { + try (BlockFile.Reader reader = BlockFile.open(input)) { + BlockFile.Header header = reader.getHeader(); + if (header.getStart() != expectedStart || header.getEnd() != expectedEnd) { + throw new IOException("Existing chunk has unexpected range: " + input); + } + while (reader.hasNext()) { + BlockFile.Record record = reader.next(); + BlockCapsule block = new BlockCapsule(record.getBlock()); + if (!Arrays.equals(record.getBlockId(), block.getBlockId().getBytes())) { + throw new IOException("Computed block ID mismatch at height " + record.getHeight()); + } + } + } + } + + private static List fetchChunk(Options options, long start, long end) + throws IOException { + IOException lastFailure = null; + for (int attempt = 1; attempt <= options.retries; attempt++) { + try { + String response = request(options, start, end); + List records = parseResponse(response); + if (records.size() != end - start + 1) { + throw new IOException("Expected " + (end - start + 1) + " blocks but received " + + records.size()); + } + for (int i = 0; i < records.size(); i++) { + if (records.get(i).getHeight() != start + i) { + throw new IOException("Non-consecutive HTTP response at block " + (start + i)); + } + } + return records; + } catch (IOException | RuntimeException e) { + lastFailure = e instanceof IOException ? (IOException) e + : new IOException(e.getMessage(), e); + if (attempt < options.retries) { + try { + Thread.sleep(Math.min(5000L, attempt * 1000L)); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while retrying HTTP request", interrupted); + } + } + } + } + throw new IOException("Failed to download blocks [" + start + ", " + end + "] after " + + options.retries + " attempts", lastFailure); + } + + private static String request(Options options, long start, long end) throws IOException { + URL url = new URL(trimTrailingSlash(options.endpoint) + "/wallet/getblockbylimitnext"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setConnectTimeout(options.timeoutMillis); + connection.setReadTimeout(options.timeoutMillis); + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", "application/json"); + byte[] request = ("{\"startNum\":" + start + ",\"endNum\":" + (end + 1) + "}") + .getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(request.length); + try { + try (OutputStream stream = connection.getOutputStream()) { + stream.write(request); + } + int status = connection.getResponseCode(); + InputStream response = status >= 200 && status < 300 + ? connection.getInputStream() : connection.getErrorStream(); + String body = readAll(response); + if (status < 200 || status >= 300) { + throw new IOException("HTTP " + status + " for blocks [" + start + ", " + end + + "]: " + abbreviate(body)); + } + return body; + } finally { + connection.disconnect(); + } + } + + static List parseResponse(String response) throws IOException { + JSONObject root = JSONObject.parseObject(response); + JSONArray blocks = root.getJSONArray("block"); + if (blocks == null) { + throw new IOException("HTTP response does not contain a block array: " + + abbreviate(response)); + } + List records = new ArrayList<>(blocks.size()); + for (int i = 0; i < blocks.size(); i++) { + JSONObject source = blocks.getJSONObject(i); + String expectedBlockId = source.getString("blockID"); + JSONObject headerJson = source.getJSONObject("block_header"); + if (expectedBlockId == null || headerJson == null) { + throw new IOException("Block response is missing blockID or block_header"); + } + BlockHeader.Builder header = BlockHeader.newBuilder(); + JsonFormat.merge(headerJson.toJSONString(), header, false); + Block.Builder block = Block.newBuilder().setBlockHeader(header); + JSONArray transactions = source.getJSONArray("transactions"); + if (transactions != null) { + for (int transactionIndex = 0; transactionIndex < transactions.size(); + transactionIndex++) { + JSONObject transactionJson = transactions.getJSONObject(transactionIndex); + Transaction transaction = parseTransaction(transactionJson, transactionIndex); + String expectedTransactionId = transactionJson.getString("txID"); + if (expectedTransactionId != null && !expectedTransactionId.equalsIgnoreCase( + ByteArray.toHexString(new TransactionCapsule(transaction).getTransactionId() + .getBytes()))) { + throw new IOException("Computed transaction ID mismatch at transaction " + + transactionIndex); + } + block.addTransactions(transaction); + } + } + Block parsed = block.build(); + BlockCapsule capsule = new BlockCapsule(parsed); + byte[] blockId = capsule.getBlockId().getBytes(); + if (!expectedBlockId.equalsIgnoreCase(ByteArray.toHexString(blockId))) { + throw new IOException("Computed block ID mismatch at height " + + parsed.getBlockHeader().getRawData().getNumber()); + } + records.add(new BlockFile.Record( + parsed.getBlockHeader().getRawData().getNumber(), blockId, parsed.toByteArray())); + } + return records; + } + + private static Transaction parseTransaction(JSONObject source, int index) throws IOException { + String rawDataHex = source.getString("raw_data_hex"); + if (rawDataHex == null) { + throw new IOException("Transaction " + index + " does not contain raw_data_hex"); + } + try { + Transaction.raw raw = Transaction.raw.parseFrom(ByteArray.fromHexString(rawDataHex)); + JSONObject envelope = JSONObject.parseObject(source.toJSONString()); + envelope.remove("txID"); + envelope.remove("raw_data"); + envelope.remove("raw_data_hex"); + envelope.remove("visible"); + Transaction.Builder transaction = Transaction.newBuilder(); + JsonFormat.merge(envelope.toJSONString(), transaction, false); + return transaction.setRawData(raw).build(); + } catch (RuntimeException e) { + throw new IOException("Cannot parse transaction " + index, e); + } + } + + private static String readAll(InputStream input) throws IOException { + if (input == null) { + return ""; + } + StringBuilder result = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(input, StandardCharsets.UTF_8))) { + char[] buffer = new char[8192]; + int read; + while ((read = reader.read(buffer)) != -1) { + result.append(buffer, 0, read); + } + } + return result.toString(); + } + + private static String trimTrailingSlash(String endpoint) { + int end = endpoint.length(); + while (end > 0 && endpoint.charAt(end - 1) == '/') { + end--; + } + return endpoint.substring(0, end); + } + + private static String abbreviate(String value) { + return value.length() <= 256 ? value : value.substring(0, 256); + } + + static final class Options { + @Parameter(names = "--endpoint", description = "Base URL of a java-tron HTTP API.") + private String endpoint = "https://api.trongrid.io"; + + @Parameter(names = "--start", description = "First block height, inclusive.") + private long start = -1; + + @Parameter(names = "--end", description = "Last block height, inclusive.") + private long end = -1; + + @Parameter(names = {"-o", "--output-directory"}, + description = "Directory for atomic block-file chunks.") + private String outputDirectory; + + @Parameter(names = "--chunk-size", description = "Blocks per output file, at most 100.") + private int chunkSize = MAX_CHUNK_SIZE; + + @Parameter(names = "--timeout-millis", description = "HTTP connect and read timeout.") + private int timeoutMillis = 120000; + + @Parameter(names = "--retries", description = "Attempts for each HTTP chunk.") + private int retries = 5; + + @Parameter(names = "--overwrite", description = "Replace existing chunks.") + private boolean overwrite; + + @Parameter(names = {"-h", "--help"}, help = true) + private boolean help; + + private void validate() { + if (endpoint == null || endpoint.trim().isEmpty()) { + throw new ParameterException("--endpoint must not be empty"); + } + if (start < 0) { + throw new ParameterException("--start must be non-negative"); + } + if (end < start) { + throw new ParameterException("--end must be greater than or equal to --start"); + } + if (end == Long.MAX_VALUE) { + throw new ParameterException("--end must be less than Long.MAX_VALUE"); + } + if (outputDirectory == null || outputDirectory.trim().isEmpty()) { + throw new ParameterException("--output-directory is required"); + } + if (chunkSize <= 0 || chunkSize > MAX_CHUNK_SIZE) { + throw new ParameterException("--chunk-size must be between 1 and " + MAX_CHUNK_SIZE); + } + if (timeoutMillis <= 0) { + throw new ParameterException("--timeout-millis must be positive"); + } + if (retries <= 0) { + throw new ParameterException("--retries must be positive"); + } + } + } +} diff --git a/framework/src/test/java/org/tron/program/HttpBlockExportTest.java b/framework/src/test/java/org/tron/program/HttpBlockExportTest.java new file mode 100644 index 00000000000..8d995c12141 --- /dev/null +++ b/framework/src/test/java/org/tron/program/HttpBlockExportTest.java @@ -0,0 +1,89 @@ +package org.tron.program; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import java.nio.file.Path; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.utils.BlockFile; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.services.http.Util; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.BlockHeader; +import org.tron.protos.Protocol.Transaction; +import org.tron.protos.Protocol.Transaction.Contract.ContractType; +import org.tron.protos.contract.BalanceContract.TransferContract; + +public class HttpBlockExportTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void parsePrintedBlockResponseAndVerifyChunk() throws Exception { + Block block = block(100, new byte[32], transferTransaction()); + String response = "{\"block\":[" + Util.printBlock(block, false) + "]}"; + + List records = HttpBlockExport.parseResponse(response); + + assertEquals(1, records.size()); + assertEquals(100, records.get(0).getHeight()); + assertEquals(block, records.get(0).getBlock()); + + Path output = temporaryFolder.newFile("100-100.dat").toPath(); + BlockFile.write(output, 100, 100, true, height -> records.get(0)); + HttpBlockExport.verifyChunk(output, 100, 100); + } + + @Test + public void rejectMismatchedBlockId() { + Block block = block(100, new byte[32], transferTransaction()); + String response = "{\"block\":[" + Util.printBlock(block, false) + .replaceFirst("\\\"blockID\\\":\\\".", "\\\"blockID\\\":\\\"f") + "]}"; + + try { + HttpBlockExport.parseResponse(response); + } catch (Exception e) { + assertTrue(e.getMessage().contains("Computed block ID mismatch")); + return; + } + throw new AssertionError("Expected mismatched block ID to fail"); + } + + private static Block block(long height, byte[] parentHash, Transaction transaction) { + BlockHeader.raw raw = BlockHeader.raw.newBuilder() + .setNumber(height) + .setParentHash(com.google.protobuf.ByteString.copyFrom(parentHash)) + .setTimestamp(1_700_000_000_000L + height) + .build(); + BlockHeader header = BlockHeader.newBuilder().setRawData(raw).build(); + Block block = Block.newBuilder().setBlockHeader(header).addTransactions(transaction).build(); + assertEquals(height, new BlockCapsule(block).getNum()); + return block; + } + + private static Transaction transferTransaction() { + TransferContract transfer = TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(new byte[] {0x41, 1})) + .setToAddress(ByteString.copyFrom(new byte[] {0x41, 2})) + .setAmount(10) + .build(); + Transaction.Contract contract = Transaction.Contract.newBuilder() + .setType(ContractType.TransferContract) + .setParameter(Any.pack(transfer)) + .build(); + Transaction.raw raw = Transaction.raw.newBuilder() + .addContract(contract) + .setTimestamp(1_700_000_000_000L) + .setExpiration(1_700_000_060_000L) + .build(); + return Transaction.newBuilder().setRawData(raw) + .addSignature(ByteString.copyFrom(new byte[] {1, 2, 3})) + .build(); + } +} From 08c254b1a1d6ce7680527601fdbf02d71e1adb1c Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 18 Aug 2026 19:04:08 +0800 Subject: [PATCH 008/161] fix(block): parse large HTTP block ranges --- .../org/tron/program/HttpBlockExport.java | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/framework/src/main/java/org/tron/program/HttpBlockExport.java b/framework/src/main/java/org/tron/program/HttpBlockExport.java index c7a719759bf..578146dc2d0 100644 --- a/framework/src/main/java/org/tron/program/HttpBlockExport.java +++ b/framework/src/main/java/org/tron/program/HttpBlockExport.java @@ -3,6 +3,12 @@ import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -22,8 +28,6 @@ import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.TransactionCapsule; import org.tron.core.services.http.JsonFormat; -import org.tron.json.JSONArray; -import org.tron.json.JSONObject; import org.tron.protos.Protocol.Block; import org.tron.protos.Protocol.BlockHeader; import org.tron.protos.Protocol.Transaction; @@ -32,6 +36,12 @@ public final class HttpBlockExport { static final int MAX_CHUNK_SIZE = 100; + private static final long MAX_RESPONSE_CHARS = 256L * 1024 * 1024; + private static final ObjectMapper HTTP_MAPPER = JsonMapper.builder( + JsonFactory.builder().streamReadConstraints(StreamReadConstraints.builder() + .maxNestingDepth(1000) + .maxTokenCount(10_000_000L) + .build()).build()).build(); private HttpBlockExport() { } @@ -139,7 +149,7 @@ private static List fetchChunk(Options options, long start, lo } } throw new IOException("Failed to download blocks [" + start + ", " + end + "] after " - + options.retries + " attempts", lastFailure); + + options.retries + " attempts: " + lastFailure.getMessage(), lastFailure); } private static String request(Options options, long start, long end) throws IOException { @@ -172,30 +182,34 @@ private static String request(Options options, long start, long end) throws IOEx } static List parseResponse(String response) throws IOException { - JSONObject root = JSONObject.parseObject(response); - JSONArray blocks = root.getJSONArray("block"); - if (blocks == null) { + JsonNode root = HTTP_MAPPER.readTree(response); + JsonNode blocks = root == null ? null : root.get("block"); + if (blocks == null || !blocks.isArray()) { throw new IOException("HTTP response does not contain a block array: " + abbreviate(response)); } List records = new ArrayList<>(blocks.size()); for (int i = 0; i < blocks.size(); i++) { - JSONObject source = blocks.getJSONObject(i); - String expectedBlockId = source.getString("blockID"); - JSONObject headerJson = source.getJSONObject("block_header"); - if (expectedBlockId == null || headerJson == null) { + JsonNode source = blocks.get(i); + JsonNode blockIdJson = source.get("blockID"); + JsonNode headerJson = source.get("block_header"); + if (blockIdJson == null || !blockIdJson.isTextual() || headerJson == null + || !headerJson.isObject()) { throw new IOException("Block response is missing blockID or block_header"); } + String expectedBlockId = blockIdJson.asText(); BlockHeader.Builder header = BlockHeader.newBuilder(); - JsonFormat.merge(headerJson.toJSONString(), header, false); + JsonFormat.merge(headerJson.toString(), header, false); Block.Builder block = Block.newBuilder().setBlockHeader(header); - JSONArray transactions = source.getJSONArray("transactions"); - if (transactions != null) { + JsonNode transactions = source.get("transactions"); + if (transactions != null && transactions.isArray()) { for (int transactionIndex = 0; transactionIndex < transactions.size(); transactionIndex++) { - JSONObject transactionJson = transactions.getJSONObject(transactionIndex); + JsonNode transactionJson = transactions.get(transactionIndex); Transaction transaction = parseTransaction(transactionJson, transactionIndex); - String expectedTransactionId = transactionJson.getString("txID"); + JsonNode transactionIdJson = transactionJson.get("txID"); + String expectedTransactionId = transactionIdJson == null ? null + : transactionIdJson.asText(); if (expectedTransactionId != null && !expectedTransactionId.equalsIgnoreCase( ByteArray.toHexString(new TransactionCapsule(transaction).getTransactionId() .getBytes()))) { @@ -218,20 +232,21 @@ static List parseResponse(String response) throws IOException return records; } - private static Transaction parseTransaction(JSONObject source, int index) throws IOException { - String rawDataHex = source.getString("raw_data_hex"); - if (rawDataHex == null) { + private static Transaction parseTransaction(JsonNode source, int index) throws IOException { + JsonNode rawDataHexJson = source.get("raw_data_hex"); + if (rawDataHexJson == null || !rawDataHexJson.isTextual()) { throw new IOException("Transaction " + index + " does not contain raw_data_hex"); } try { + String rawDataHex = rawDataHexJson.asText(); Transaction.raw raw = Transaction.raw.parseFrom(ByteArray.fromHexString(rawDataHex)); - JSONObject envelope = JSONObject.parseObject(source.toJSONString()); + ObjectNode envelope = (ObjectNode) source.deepCopy(); envelope.remove("txID"); envelope.remove("raw_data"); envelope.remove("raw_data_hex"); envelope.remove("visible"); Transaction.Builder transaction = Transaction.newBuilder(); - JsonFormat.merge(envelope.toJSONString(), transaction, false); + JsonFormat.merge(envelope.toString(), transaction, false); return transaction.setRawData(raw).build(); } catch (RuntimeException e) { throw new IOException("Cannot parse transaction " + index, e); @@ -249,6 +264,9 @@ private static String readAll(InputStream input) throws IOException { int read; while ((read = reader.read(buffer)) != -1) { result.append(buffer, 0, read); + if (result.length() > MAX_RESPONSE_CHARS) { + throw new IOException("HTTP response exceeds " + MAX_RESPONSE_CHARS + " characters"); + } } } return result.toString(); From c245c955165a1de856b8f03175bf989b6dd60e70 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 19 Aug 2026 10:59:07 +0800 Subject: [PATCH 009/161] feat(block): replay consecutive block chunks --- .../java/org/tron/program/BlockReplay.java | 111 ++++++++++++++++-- .../org/tron/program/BlockReplayTest.java | 33 ++++++ 2 files changed, 137 insertions(+), 7 deletions(-) diff --git a/framework/src/main/java/org/tron/program/BlockReplay.java b/framework/src/main/java/org/tron/program/BlockReplay.java index d3b3fdb424e..c1d05edd6a6 100644 --- a/framework/src/main/java/org/tron/program/BlockReplay.java +++ b/framework/src/main/java/org/tron/program/BlockReplay.java @@ -8,8 +8,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; +import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.tron.common.application.TronApplicationContext; @@ -73,7 +78,7 @@ static int execute(String[] args, PrintStream output, PrintStream error) { private static ReplayResult verify(Options options) throws Exception { Args.setParam(new String[] {"-c", options.config}, "config.conf"); - return replay(Paths.get(options.input), null, null, options.warmupBlocks, + return replayInput(Paths.get(options.input), null, null, options.warmupBlocks, options.maxBlocks, false); } @@ -95,7 +100,8 @@ private static ReplayResult apply(Options options) throws Exception { context.register(DefaultConfig.class); context.refresh(); startConsensus(context); - ReplayResult result = replay(Paths.get(options.input), context.getBean(TronNetDelegate.class), + ReplayResult result = replayInput(Paths.get(options.input), + context.getBean(TronNetDelegate.class), context.getBean(ChainBaseManager.class), options.warmupBlocks, options.maxBlocks, true); flushPending(context); @@ -116,6 +122,12 @@ static void flushPending(TronApplicationContext context) { static ReplayResult replay(Path input, TronNetDelegate tronNetDelegate, ChainBaseManager chainBaseManager, long warmupBlocks, long maxBlocks, boolean apply) throws Exception { + return replay(input, tronNetDelegate, chainBaseManager, warmupBlocks, maxBlocks, apply, null); + } + + private static ReplayResult replay(Path input, TronNetDelegate tronNetDelegate, + ChainBaseManager chainBaseManager, long warmupBlocks, long maxBlocks, boolean apply, + byte[] expectedParentBlockId) throws Exception { try (BlockFile.Reader reader = BlockFile.open(input)) { BlockFile.Header header = reader.getHeader(); long selectedCount = Math.min(header.getCount(), maxBlocks); @@ -131,12 +143,18 @@ static ReplayResult replay(Path input, TronNetDelegate tronNetDelegate, long measured = 0; long measuredNanos = 0; long lastHeight = header.getStart() - 1; + byte[] lastBlockId = expectedParentBlockId; while (processed < selectedCount && reader.hasNext()) { BlockFile.Record record = reader.next(); BlockCapsule block = new BlockCapsule(record.getBlock()); if (!Arrays.equals(record.getBlockId(), block.getBlockId().getBytes())) { throw new IOException("Computed block ID mismatch at height " + record.getHeight()); } + if (processed == 0 && expectedParentBlockId != null && !Arrays.equals( + expectedParentBlockId, block.getParentHash().getBytes())) { + throw new IllegalArgumentException("Block file parent mismatch at " + + record.getHeight()); + } if (apply && processed == 0 && !block.getParentHash().equals( chainBaseManager.getHeadBlockId())) { throw new IllegalArgumentException("First block parent does not match D0 head"); @@ -158,17 +176,81 @@ static ReplayResult replay(Path input, TronNetDelegate tronNetDelegate, } processed++; lastHeight = record.getHeight(); + lastBlockId = record.getBlockId(); } if (selectedCount == header.getCount()) { reader.hasNext(); } return new ReplayResult(apply, header.getStart(), lastHeight, processed, - Math.min(warmupBlocks, processed), measured, measuredNanos); + Math.min(warmupBlocks, processed), measured, measuredNanos, lastBlockId); } } + static ReplayResult replayInput(Path input, TronNetDelegate tronNetDelegate, + ChainBaseManager chainBaseManager, long warmupBlocks, long maxBlocks, boolean apply) + throws Exception { + if (Files.isRegularFile(input)) { + return replay(input, tronNetDelegate, chainBaseManager, warmupBlocks, maxBlocks, apply); + } + List inputs = listInputFiles(input); + long processed = 0; + long warmup = 0; + long measured = 0; + long measuredNanos = 0; + long start = inputs.get(0).start; + long end = start - 1; + byte[] previousBlockId = null; + for (InputFile inputFile : inputs) { + long remaining = maxBlocks - processed; + if (remaining <= 0) { + break; + } + ReplayResult part = replay(inputFile.path, tronNetDelegate, chainBaseManager, + Math.max(0, warmupBlocks - processed), remaining, apply, previousBlockId); + processed += part.processed; + warmup += part.warmup; + measured += part.measured; + measuredNanos += part.measuredNanos; + end = part.end; + previousBlockId = part.lastBlockId; + } + return new ReplayResult(apply, start, end, processed, warmup, measured, measuredNanos, + previousBlockId); + } + + private static List listInputFiles(Path input) throws IOException { + if (!Files.isDirectory(input)) { + throw new IllegalArgumentException("Block input does not exist: " + input); + } + List paths; + try (Stream stream = Files.list(input)) { + paths = stream.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith(".dat")) + .collect(Collectors.toList()); + } + List inputs = new ArrayList<>(paths.size()); + for (Path path : paths) { + try (BlockFile.Reader reader = BlockFile.open(path)) { + BlockFile.Header header = reader.getHeader(); + inputs.add(new InputFile(path, header.getStart(), header.getEnd())); + } + } + inputs.sort(Comparator.comparingLong(value -> value.start)); + if (inputs.isEmpty()) { + throw new IllegalArgumentException("Block input directory has no .dat files: " + input); + } + for (int i = 1; i < inputs.size(); i++) { + if (inputs.get(i).start != inputs.get(i - 1).end + 1) { + throw new IllegalArgumentException("Block input files are not consecutive between " + + inputs.get(i - 1).path + " and " + inputs.get(i).path); + } + } + return inputs; + } + static final class Options { - @Parameter(names = {"-i", "--input"}, description = "Block file to verify or replay.") + @Parameter(names = {"-i", "--input"}, + description = "Block file or consecutive chunk directory to verify or replay.") private String input; @Parameter(names = {"-d", "--output-directory"}, @@ -197,8 +279,8 @@ private void validate() { if (input == null || input.trim().isEmpty()) { throw new ParameterException("--input is required"); } - if (!Files.isRegularFile(Paths.get(input))) { - throw new ParameterException("Block file does not exist: " + input); + if (!Files.isRegularFile(Paths.get(input)) && !Files.isDirectory(Paths.get(input))) { + throw new ParameterException("Block input does not exist: " + input); } if (config == null || config.trim().isEmpty()) { throw new ParameterException("--config must not be empty"); @@ -223,9 +305,10 @@ static final class ReplayResult { private final long warmup; private final long measured; private final long measuredNanos; + private final byte[] lastBlockId; private ReplayResult(boolean applied, long start, long end, long processed, - long warmup, long measured, long measuredNanos) { + long warmup, long measured, long measuredNanos, byte[] lastBlockId) { this.applied = applied; this.start = start; this.end = end; @@ -233,6 +316,8 @@ private ReplayResult(boolean applied, long start, long end, long processed, this.warmup = warmup; this.measured = measured; this.measuredNanos = measuredNanos; + this.lastBlockId = lastBlockId == null ? null + : Arrays.copyOf(lastBlockId, lastBlockId.length); } String format() { @@ -246,4 +331,16 @@ String format() { elapsedMs, blocksPerSecond); } } + + private static final class InputFile { + private final Path path; + private final long start; + private final long end; + + private InputFile(Path path, long start, long end) { + this.path = path; + this.start = start; + this.end = end; + } + } } diff --git a/framework/src/test/java/org/tron/program/BlockReplayTest.java b/framework/src/test/java/org/tron/program/BlockReplayTest.java index b690525b426..1d1f4e2713a 100644 --- a/framework/src/test/java/org/tron/program/BlockReplayTest.java +++ b/framework/src/test/java/org/tron/program/BlockReplayTest.java @@ -62,6 +62,35 @@ public void shouldVerifyThroughCommandLine() throws Exception { assertTrue(output.toString().contains("processed=2")); } + @Test + public void shouldVerifyConsecutiveDirectoryWithGlobalLimitAndWarmup() throws Exception { + BlockId parent = new BlockId(Sha256Hash.ZERO_HASH, 9); + BlockCapsule[] blocks = blocks(parent, 10, 4); + Path directory = temporaryFolder.newFolder("chunks").toPath(); + write(directory.resolve("second.dat"), new BlockCapsule[] {blocks[2], blocks[3]}); + write(directory.resolve("first.dat"), new BlockCapsule[] {blocks[0], blocks[1]}); + + BlockReplay.ReplayResult result = BlockReplay.replayInput(directory, null, null, 1, 3, false); + + assertTrue(result.format().contains("range=[10,12]")); + assertTrue(result.format().contains("processed=3")); + assertTrue(result.format().contains("warmup=1")); + assertTrue(result.format().contains("measured=2")); + } + + @Test + public void shouldRejectParentMismatchBetweenChunks() throws Exception { + BlockId parent = new BlockId(Sha256Hash.ZERO_HASH, 9); + BlockCapsule first = blocks(parent, 10, 1)[0]; + BlockCapsule second = blocks(new BlockId(Sha256Hash.ZERO_HASH, 10), 11, 1)[0]; + Path directory = temporaryFolder.newFolder("parent-mismatch").toPath(); + write(directory.resolve("10.dat"), new BlockCapsule[] {first}); + write(directory.resolve("11.dat"), new BlockCapsule[] {second}); + + assertThrows(IllegalArgumentException.class, + () -> BlockReplay.replayInput(directory, null, null, 0, Long.MAX_VALUE, false)); + } + @Test public void shouldApplyBlocksThroughSyncPath() throws Exception { BlockId parent = new BlockId(Sha256Hash.ZERO_HASH, 9); @@ -125,6 +154,10 @@ public void shouldRejectD0WithDifferentHead() throws Exception { private Path write(BlockCapsule[] blocks) throws Exception { Path input = temporaryFolder.getRoot().toPath().resolve("blocks.dat"); + return write(input, blocks); + } + + private Path write(Path input, BlockCapsule[] blocks) throws Exception { BlockFile.write(input, blocks[0].getNum(), blocks[blocks.length - 1].getNum(), false, height -> { BlockCapsule block = blocks[(int) (height - blocks[0].getNum())]; From 239fead502665746aefa9eb4988dc7524108565a Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 19 Aug 2026 22:52:23 +0800 Subject: [PATCH 010/161] feat(chainbase): persist archive recovery state Persist bounded restart checkpoints, truncation recovery, H/C/D/R progress authorities, and immutable serving generations. Pin committed history resources and fail closed on identity or durability drift. --- .../db2/archive/ArchiveGenerationCapsule.java | 57 ++ .../db2/archive/ArchiveHistoryTruncator.java | 69 +++ .../db2/archive/ArchiveHistoryWriter.java | 174 ++++-- .../archive/ArchiveParticipantBatchFile.java | 186 +++++++ .../db2/archive/ArchiveProgressEnvelope.java | 116 ++++ .../archive/ArchiveProgressEnvelopeCodec.java | 165 ++++++ .../core/db2/archive/ArchiveProgressFile.java | 78 +++ .../core/db2/archive/ArchiveReadSnapshot.java | 89 ++- .../archive/ArchiveReaderHeadPublisher.java | 61 +++ .../db2/archive/ArchiveRecoveryExecutor.java | 117 ++++ .../db2/archive/ArchiveRecoveryPlanner.java | 181 +++++++ .../db2/archive/ArchiveRecoveryScanner.java | 114 ++++ .../db2/archive/ArchiveRestartCheckpoint.java | 197 +++++++ .../db2/archive/ArchiveTruncationIntent.java | 228 ++++++++ .../archive/ArchiveTruncationRecovery.java | 96 ++++ .../db2/archive/HistoricalRangeOverlay.java | 2 +- .../core/db2/archive/HistoryCommitStore.java | 66 ++- .../core/db2/archive/HistoryIndexStore.java | 49 +- .../core/db2/archive/HistorySegmentStore.java | 58 +- .../PersistentCommittedHistoryReader.java | 245 +++++++++ .../PersistentServingKeyIndexCatalog.java | 344 ++++++++++++ .../PersistentServingKeyIndexGeneration.java | 511 ++++++++++++++++++ .../core/db2/archive/ServingKeyIndex.java | 32 ++ .../archive/ServingKeyIndexGeneration.java | 2 +- .../archive/ArchiveHistoryTruncatorTest.java | 143 +++++ .../db2/archive/ArchiveHistoryWriterTest.java | 168 ++++++ .../ArchiveParticipantBatchFileTest.java | 225 ++++++++ .../archive/ArchiveProgressEnvelopeTest.java | 139 +++++ .../archive/ArchiveRecoveryExecutorTest.java | 162 ++++++ .../archive/ArchiveRecoveryPlannerTest.java | 107 ++++ .../archive/ArchiveRecoveryScannerTest.java | 224 ++++++++ .../ArchiveTruncationRecoveryTest.java | 180 ++++++ ...rsistentServingKeyIndexGenerationTest.java | 496 +++++++++++++++++ .../SnapshotOldValueCollectorTest.java | 6 +- 34 files changed, 5025 insertions(+), 62 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveGenerationCapsule.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryTruncator.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantBatchFile.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressFile.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryPlanner.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRestartCheckpoint.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndex.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryTruncatorTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantBatchFileTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryExecutorTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryPlannerTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryScannerTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveGenerationCapsule.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveGenerationCapsule.java new file mode 100644 index 00000000000..02fbea822df --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveGenerationCapsule.java @@ -0,0 +1,57 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Objects; + +/** Acquires one reader-visible serving/history/latest resource capsule from durable R. */ +public final class ArchiveGenerationCapsule { + + private final PersistentServingKeyIndexCatalog catalog; + private final Path readerVisiblePath; + private final Path archiveDirectory; + private final long maxSegmentSize; + private final ArchiveReadSnapshot.PinnedLatestStateFactory latestFactory; + + public ArchiveGenerationCapsule(PersistentServingKeyIndexCatalog catalog, + Path readerVisiblePath, Path archiveDirectory, long maxSegmentSize, + ArchiveReadSnapshot.PinnedLatestStateFactory latestFactory) { + this.catalog = Objects.requireNonNull(catalog, "catalog"); + this.readerVisiblePath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); + this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + if (maxSegmentSize <= 0) { + throw new IllegalArgumentException("maxSegmentSize must be positive"); + } + this.maxSegmentSize = maxSegmentSize; + this.latestFactory = Objects.requireNonNull(latestFactory, "latestFactory"); + } + + public ArchiveReadSnapshot pin(long targetBlock) throws IOException { + ArchiveProgressEnvelope readerVisible = new ArchiveProgressFile(readerVisiblePath, + new ArchiveProgressEnvelopeCodec()).load(); + return ArchiveReadSnapshot.pin(targetBlock, catalog, readerVisible, archiveDirectory, + maxSegmentSize, serving -> pinLatest(serving)); + } + + private ArchiveReadSnapshot.PinnedLatestState pinLatest( + PersistentServingKeyIndexGeneration serving) throws IOException { + if (!serving.isLatestSourceIdentityBound()) { + throw new ArchivePersistenceException( + "Serving generation is not bound to latest engine source identities"); + } + ArchiveReadSnapshot.PinnedLatestState latest = latestFactory.pin(serving); + if (!java.util.Arrays.equals(serving.getLatestSourceIdentityDigest(), + latest.getSourceIdentityDigest())) { + try { + latest.close(); + } catch (IOException closeFailure) { + ArchivePersistenceException mismatch = new ArchivePersistenceException( + "Latest engine source identity digest mismatch"); + mismatch.addSuppressed(closeFailure); + throw mismatch; + } + throw new ArchivePersistenceException("Latest engine source identity digest mismatch"); + } + return latest; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryTruncator.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryTruncator.java new file mode 100644 index 00000000000..de0e2438a4d --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryTruncator.java @@ -0,0 +1,69 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +/** Shrinks commit authority before removing stale authoritative-index and body suffixes. */ +public final class ArchiveHistoryTruncator { + + private final HistoryCommitStore commits; + private final HistoryIndexStore index; + private final HistorySegmentStore bodies; + private final FaultHook faultHook; + + public ArchiveHistoryTruncator(HistoryCommitStore commits, HistoryIndexStore index, + HistorySegmentStore bodies) { + this(commits, index, bodies, stage -> { }); + } + + ArchiveHistoryTruncator(HistoryCommitStore commits, HistoryIndexStore index, + HistorySegmentStore bodies, FaultHook faultHook) { + this.commits = Objects.requireNonNull(commits, "commits"); + this.index = Objects.requireNonNull(index, "index"); + this.bodies = Objects.requireNonNull(bodies, "bodies"); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + public void truncateAfter(long lastEpoch) throws IOException { + HistoryCommitMarker target = commits.get(lastEpoch); + if (target == null) { + throw new ArchivePersistenceException( + "Archive truncation target is outside committed history: " + lastEpoch); + } + HistoryIndexRecord targetIndex = index.read(target.getIndexLocation()); + BlockReverseDiff targetBody = bodies.read(target.getHistoryLocation()); + if (!target.getMeta().equals(targetIndex.getMeta()) + || !target.getMeta().equals(targetBody.getMeta()) + || !sameLocation(target.getHistoryLocation(), targetIndex.getHistoryLocation())) { + throw new ArchivePersistenceException( + "Archive truncation target identity does not match index and body"); + } + + commits.truncateAfter(lastEpoch); + faultHook.afterDurableStage(Stage.COMMIT_AUTHORITY); + index.truncateAfter(target.getIndexLocation(), commits.size()); + faultHook.afterDurableStage(Stage.AUTHORITATIVE_INDEX); + bodies.truncateAfter(target.getHistoryLocation(), commits.size()); + faultHook.afterDurableStage(Stage.HISTORY_BODY); + } + + private static boolean sameLocation(HistoryLocation expected, HistoryLocation actual) { + return expected.getSegmentId() == actual.getSegmentId() + && expected.getOffset() == actual.getOffset() + && expected.getRecordLength() == actual.getRecordLength() + && expected.getBodyChecksum() == actual.getBodyChecksum() + && Arrays.equals(expected.getBodyDigest(), actual.getBodyDigest()); + } + + public enum Stage { + COMMIT_AUTHORITY, + AUTHORITATIVE_INDEX, + HISTORY_BODY + } + + @FunctionalInterface + interface FaultHook { + void afterDurableStage(Stage stage) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java index cda4bb5fc3c..41a9804aadf 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java @@ -3,23 +3,31 @@ import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; import java.util.List; +import java.util.NoSuchElementException; import java.util.Set; import java.util.UUID; /** - * Ordered history body/index/marker writer. A marker is the only reader-visible commit boundary. + * Ordered history body/index/marker writer. A marker is the durable history boundary H; reader + * visibility R is a separate recovery authority and is not yet integrated into this prototype. */ public final class ArchiveHistoryWriter implements DurableBlockReverseDiffSink, Closeable { + static final int MAX_RESTART_TAIL_RECORDS = 1024; + private final HistorySegmentStore bodies; private final HistoryIndexStore index; private final HistoryCommitStore commits; private final AccountChangeIndex accountIndex; private final ArchiveBaseManifest manifest; + private final Path archiveDirectory; + private final HistoryCommitMarkerCodec commitCodec; private final List participatingDatabases; private final DurabilityHook hook; @@ -30,20 +38,31 @@ public ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, Set participatingDatabases, DurabilityHook hook) throws IOException { + this.archiveDirectory = archiveDirectory; + this.commitCodec = new HistoryCommitMarkerCodec(); this.participatingDatabases = new ArrayList<>(participatingDatabases); this.participatingDatabases.sort(String::compareTo); this.manifest = new ArchiveBaseManifest(archiveDirectory, this.participatingDatabases); + new ArchiveTruncationRecovery(archiveDirectory, maxSegmentSize).recover(); + ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archiveDirectory, + commitCodec); this.bodies = new HistorySegmentStore(archiveDirectory, new BlockHistoryCodec(), - maxSegmentSize); - this.index = new HistoryIndexStore(archiveDirectory, new HistoryIndexCodec()); - this.commits = new HistoryCommitStore(archiveDirectory, new HistoryCommitMarkerCodec()); + maxSegmentSize, checkpoint); + this.index = new HistoryIndexStore(archiveDirectory, new HistoryIndexCodec(), checkpoint); + this.commits = new HistoryCommitStore(archiveDirectory, commitCodec, checkpoint); this.hook = hook; recoverPreparedSuffix(); + persistRestartCheckpoint(); if (commits.head() != null) { manifest.ensureBase(commits.get(commits.firstEpoch()).getMeta()); } this.accountIndex = new AccountChangeIndex(archiveDirectory.resolve("account-change-index")); - catchUpAccountIndex(); + try { + catchUpAccountIndex(); + } catch (IOException | RuntimeException failure) { + closeAfterFailedConstruction(failure); + throw failure; + } } @Override @@ -69,32 +88,10 @@ public synchronized void acceptAll(List diffs) { previous = diff.getMeta(); } try { - List bodyLocations = new ArrayList<>(diffs.size()); - List indexLocations = new ArrayList<>(diffs.size()); - for (BlockReverseDiff diff : diffs) { - hook.before(Stage.APPEND_BODY, diff.getMeta()); - HistoryLocation bodyLocation = bodies.append(diff); - bodyLocations.add(bodyLocation); - hook.before(Stage.APPEND_INDEX, diff.getMeta()); - indexLocations.add(index.append(HistoryIndexRecord.from(diff, bodyLocation))); - } - BlockSnapshotMeta lastMeta = diffs.get(diffs.size() - 1).getMeta(); - hook.before(Stage.SYNC_BODY, lastMeta); - bodies.sync(); - hook.before(Stage.SYNC_INDEX, lastMeta); - index.sync(); - HistoryCommitMarker head = commits.head(); - long previousEpoch = head == null ? diffs.get(0).getMeta().getEpoch() - 1 - : head.getMeta().getEpoch(); - List markers = new ArrayList<>(diffs.size()); - for (int i = 0; i < diffs.size(); i++) { - BlockReverseDiff diff = diffs.get(i); - hook.before(Stage.COMMIT_MARKER, diff.getMeta()); - markers.add(new HistoryCommitMarker(diff.getMeta(), previousEpoch, - bodyLocations.get(i), indexLocations.get(i), batchId(), participatingDatabases)); - previousEpoch = diff.getMeta().getEpoch(); + for (int start = 0; start < diffs.size(); start += MAX_RESTART_TAIL_RECORDS) { + int end = Math.min(diffs.size(), start + MAX_RESTART_TAIL_RECORDS); + persistChunk(diffs.subList(start, end)); } - commits.commitAll(markers); accountIndex.apply(diffs); } catch (IOException | RuntimeException e) { handleWriteFailure(diffs.get(diffs.size() - 1).getMeta(), e); @@ -110,9 +107,11 @@ public synchronized void revert(BlockSnapshotMeta meta) { HistoryCommitMarker previous = commits.get(meta.getEpoch() - 1); accountIndex.revert(reverted, previous == null ? null : previous.getMeta()); commits.removeHead(meta); + persistRestartCheckpoint(); previous = commits.head(); - index.truncateAfter(previous == null ? null : previous.getIndexLocation()); - bodies.truncateAfter(previous == null ? null : previous.getHistoryLocation()); + index.truncateAfter(previous == null ? null : previous.getIndexLocation(), commits.size()); + bodies.truncateAfter(previous == null ? null : previous.getHistoryLocation(), + commits.size()); return; } @@ -196,6 +195,88 @@ public synchronized BlockSnapshotMeta committedHeadMeta() { return marker == null ? null : marker.getMeta(); } + /** Builds one immutable persistent serving generation from the current committed prefix H. */ + public synchronized PersistentServingKeyIndexGeneration buildServingGeneration( + Path shadowDirectory, String generationId) throws IOException { + return buildServingGeneration(shadowDirectory, generationId, new byte[32]); + } + + public synchronized PersistentServingKeyIndexGeneration buildServingGeneration( + Path shadowDirectory, String generationId, byte[] latestSourceIdentityDigest) + throws IOException { + HistoryCommitMarker first = commits.head() == null ? null : commits.get(commits.firstEpoch()); + if (first == null) { + throw new IllegalStateException("Cannot build a serving generation from empty history"); + } + long firstEpoch = commits.firstEpoch(); + long lastEpoch = commits.head().getMeta().getEpoch(); + Iterable committed = () -> new Iterator() { + private long nextEpoch = firstEpoch; + + @Override + public boolean hasNext() { + return nextEpoch <= lastEpoch; + } + + @Override + public HistoryCommitMarker next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return commits.get(nextEpoch++); + } + }; + return PersistentServingKeyIndexGeneration.build(shadowDirectory, generationId, + firstEpoch - 1, first.getMeta().getParentHash(), committed, index::read, + participatingDatabases, latestSourceIdentityDigest); + } + + long getStartupScannedRecords() { + return bodies.getStartupScannedRecords() + index.getStartupScannedRecords() + + commits.getStartupScannedRecords(); + } + + private void persistRestartCheckpoint() throws IOException { + HistoryCommitMarker head = commits.head(); + if (head != null) { + ArchiveRestartCheckpoint.persist(archiveDirectory, commits.firstEpoch(), commits.size(), + commits.getRecordLength(), head, commitCodec); + } else { + Files.deleteIfExists(archiveDirectory.resolve("restart.checkpoint")); + HistorySegmentStore.syncDirectory(archiveDirectory); + } + } + + private void persistChunk(List diffs) throws IOException { + List bodyLocations = new ArrayList<>(diffs.size()); + List indexLocations = new ArrayList<>(diffs.size()); + for (BlockReverseDiff diff : diffs) { + hook.before(Stage.APPEND_BODY, diff.getMeta()); + HistoryLocation bodyLocation = bodies.append(diff); + bodyLocations.add(bodyLocation); + hook.before(Stage.APPEND_INDEX, diff.getMeta()); + indexLocations.add(index.append(HistoryIndexRecord.from(diff, bodyLocation))); + } + BlockSnapshotMeta lastMeta = diffs.get(diffs.size() - 1).getMeta(); + hook.before(Stage.SYNC_BODY, lastMeta); + bodies.sync(); + hook.before(Stage.SYNC_INDEX, lastMeta); + index.sync(); + HistoryCommitMarker head = commits.head(); + long previousEpoch = head == null ? diffs.get(0).getMeta().getEpoch() - 1 + : head.getMeta().getEpoch(); + List markers = new ArrayList<>(diffs.size()); + for (int i = 0; i < diffs.size(); i++) { + BlockReverseDiff diff = diffs.get(i); + hook.before(Stage.COMMIT_MARKER, diff.getMeta()); + markers.add(new HistoryCommitMarker(diff.getMeta(), previousEpoch, + bodyLocations.get(i), indexLocations.get(i), batchId(), participatingDatabases)); + previousEpoch = diff.getMeta().getEpoch(); + } + commits.commitAll(markers); + persistRestartCheckpoint(); + } + private void validateNext(BlockSnapshotMeta previous, BlockSnapshotMeta meta) { if (previous == null) { return; @@ -235,13 +316,11 @@ private void recoverPreparedSuffix() throws IOException { if (bodyScan.getRecordCount() < committedCount) { throw new ArchivePersistenceException("Committed history body is corrupt"); } - bodies.truncateInvalidTail(); } if (indexScan.getInvalidTailOffset() != null) { if (indexScan.getRecordCount() < committedCount) { throw new ArchivePersistenceException("Committed history index is corrupt"); } - index.truncateInvalidTail(); } HistoryCommitMarker head = commits.head(); if (head != null) { @@ -252,8 +331,8 @@ private void recoverPreparedSuffix() throws IOException { throw new ArchivePersistenceException("Commit head does not match history body metadata"); } } - index.truncateAfter(head == null ? null : head.getIndexLocation()); - bodies.truncateAfter(head == null ? null : head.getHistoryLocation()); + index.truncateAfter(head == null ? null : head.getIndexLocation(), commits.size()); + bodies.truncateAfter(head == null ? null : head.getHistoryLocation(), commits.size()); } private void catchUpAccountIndex() throws IOException { @@ -287,6 +366,29 @@ private void catchUpAccountIndex() throws IOException { } } + private void closeAfterFailedConstruction(Exception failure) { + try { + accountIndex.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + try { + index.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + try { + bodies.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + try { + commits.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + private static OldValue findOldValue(BlockReverseDiff diff, String dbName, byte[] rawKey) { for (BlockReverseDiff.DbGroup group : diff.getGroups()) { if (!dbName.equals(group.getDbName())) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantBatchFile.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantBatchFile.java new file mode 100644 index 00000000000..f759c889541 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantBatchFile.java @@ -0,0 +1,186 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; + +/** Atomic-file prototype containing participant business bytes and D[i] in one durable unit. */ +public final class ArchiveParticipantBatchFile { + + private static final int MAGIC = 0x54414254; // TABT + private static final short VERSION = 1; + private static final int HEADER_LENGTH = 20; + private static final int MAX_BUSINESS_LENGTH = 64 * 1024 * 1024; + + private final Path path; + private final Path temporary; + private final String participant; + private final List participants; + private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); + private final FaultHook faultHook; + + public ArchiveParticipantBatchFile(Path path, String participant, List participants) { + this(path, participant, participants, temporary -> { }); + } + + ArchiveParticipantBatchFile(Path path, String participant, List participants, + FaultHook faultHook) { + this.path = Objects.requireNonNull(path, "path"); + this.temporary = path.resolveSibling(path.getFileName() + ".tmp"); + this.participants = validateParticipants(participants); + if (participant == null || participant.isEmpty() || !this.participants.contains(participant)) { + throw new IllegalArgumentException("Archive batch participant is invalid"); + } + this.participant = participant; + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + public void store(byte[] businessPayload, ArchiveProgressEnvelope progress) throws IOException { + byte[] encoded = encode(businessPayload, progress); + Path directory = Objects.requireNonNull(path.getParent(), "participant batch directory"); + Files.createDirectories(directory); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(encoded); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + faultHook.afterTemporaryForce(temporary); + try { + Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new ArchivePersistenceException( + "Archive participant filesystem does not support atomic replacement", unsupported); + } + HistorySegmentStore.syncDirectory(directory); + } + + public Snapshot load() throws IOException { + if (!Files.exists(path)) { + throw new ArchivePersistenceException("Archive participant batch is missing: " + path); + } + try { + return decode(Files.readAllBytes(path)); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Archive participant batch is corrupt", invalid); + } + } + + Path getTemporaryPath() { + return temporary; + } + + private byte[] encode(byte[] businessPayload, ArchiveProgressEnvelope progress) { + byte[] business = Arrays.copyOf(Objects.requireNonNull(businessPayload, "businessPayload"), + businessPayload.length); + if (business.length > MAX_BUSINESS_LENGTH) { + throw new IllegalArgumentException("Archive participant business payload is too large"); + } + requireProgress(progress); + byte[] encodedProgress = progressCodec.encode(progress); + int length = HEADER_LENGTH + business.length + encodedProgress.length + Integer.BYTES; + ByteBuffer buffer = ByteBuffer.allocate(length); + buffer.putInt(MAGIC).putShort(VERSION).putShort((short) 0).putInt(length) + .putInt(business.length).putInt(encodedProgress.length).put(business).put(encodedProgress); + byte[] payload = Arrays.copyOf(buffer.array(), length - Integer.BYTES); + buffer.putInt(Hashing.crc32c().hashBytes(payload).asInt()); + return buffer.array(); + } + + private Snapshot decode(byte[] encoded) { + if (encoded == null || encoded.length < HEADER_LENGTH + Integer.BYTES) { + throw new IllegalArgumentException("Archive participant batch length is invalid"); + } + ByteBuffer buffer = ByteBuffer.wrap(encoded); + if (buffer.getInt() != MAGIC || buffer.getShort() != VERSION || buffer.getShort() != 0 + || buffer.getInt() != encoded.length) { + throw new IllegalArgumentException("Unsupported archive participant batch header"); + } + int businessLength = buffer.getInt(); + int progressLength = buffer.getInt(); + long expectedLength = HEADER_LENGTH + (long) businessLength + progressLength + Integer.BYTES; + if (businessLength < 0 || businessLength > MAX_BUSINESS_LENGTH + || progressLength <= 0 || expectedLength != encoded.length) { + throw new IllegalArgumentException("Archive participant batch payload length is invalid"); + } + int expectedChecksum = ByteBuffer.wrap(encoded, encoded.length - Integer.BYTES, + Integer.BYTES).getInt(); + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + if (expectedChecksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new IllegalArgumentException("Archive participant batch checksum mismatch"); + } + byte[] business = new byte[businessLength]; + buffer.get(business); + byte[] encodedProgress = new byte[progressLength]; + buffer.get(encodedProgress); + ArchiveProgressEnvelope progress = progressCodec.decode(encodedProgress); + requireProgress(progress); + return new Snapshot(business, progress); + } + + private void requireProgress(ArchiveProgressEnvelope progress) { + Objects.requireNonNull(progress, "progress"); + if (progress.getKind() != Kind.PARTICIPANT_PROGRESS + || !participant.equals(progress.getParticipant()) + || !participants.equals(progress.getParticipants())) { + throw new IllegalArgumentException("Archive participant batch progress identity mismatch"); + } + } + + private static List validateParticipants(List participants) { + Objects.requireNonNull(participants, "participants"); + if (participants.isEmpty()) { + throw new IllegalArgumentException("Archive participant set must not be empty"); + } + List copy = new ArrayList<>(participants.size()); + String previous = null; + for (String participant : participants) { + if (participant == null || participant.isEmpty() + || previous != null && previous.compareTo(participant) >= 0) { + throw new IllegalArgumentException( + "Archive participants must be non-empty, unique, and sorted"); + } + copy.add(participant); + previous = participant; + } + return Collections.unmodifiableList(copy); + } + + public static final class Snapshot { + private final byte[] businessPayload; + private final ArchiveProgressEnvelope progress; + + private Snapshot(byte[] businessPayload, ArchiveProgressEnvelope progress) { + this.businessPayload = Arrays.copyOf(businessPayload, businessPayload.length); + this.progress = progress; + } + + public byte[] getBusinessPayload() { + return Arrays.copyOf(businessPayload, businessPayload.length); + } + + public ArchiveProgressEnvelope getProgress() { + return progress; + } + } + + @FunctionalInterface + interface FaultHook { + void afterTemporaryForce(Path temporary) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java new file mode 100644 index 00000000000..d94647c5889 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java @@ -0,0 +1,116 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** Prototype identity envelope for apply checkpoint C or one participant progress D[i]. */ +public final class ArchiveProgressEnvelope { + + public enum Kind { + APPLY_CHECKPOINT, + PARTICIPANT_PROGRESS, + READER_VISIBLE + } + + private final Kind kind; + private final String participant; + private final long epoch; + private final byte[] blockHash; + private final byte[] batchId; + private final byte[] payloadDigest; + private final List participants; + + public ArchiveProgressEnvelope(Kind kind, String participant, long epoch, byte[] blockHash, + byte[] batchId, byte[] payloadDigest, List participants) { + this.kind = Objects.requireNonNull(kind, "kind"); + if (epoch < 0) { + throw new IllegalArgumentException("Archive progress epoch must be non-negative"); + } + this.epoch = epoch; + this.blockHash = exactBytes(blockHash, 32, "blockHash"); + this.batchId = exactBytes(batchId, 16, "batchId"); + this.payloadDigest = exactBytes(payloadDigest, 32, "payloadDigest"); + this.participants = validateParticipants(participants); + if (kind != Kind.PARTICIPANT_PROGRESS) { + if (participant != null) { + throw new IllegalArgumentException("Global archive progress must not name one participant"); + } + this.participant = null; + } else { + if (participant == null || participant.isEmpty() + || !this.participants.contains(participant)) { + throw new IllegalArgumentException("Participant progress identity is invalid"); + } + this.participant = participant; + } + } + + public Kind getKind() { + return kind; + } + + public String getParticipant() { + return participant; + } + + public long getEpoch() { + return epoch; + } + + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + public byte[] getBatchId() { + return Arrays.copyOf(batchId, batchId.length); + } + + public byte[] getPayloadDigest() { + return Arrays.copyOf(payloadDigest, payloadDigest.length); + } + + public List getParticipants() { + return participants; + } + + public void requireIdentity(Kind expectedKind, String expectedParticipant, long expectedEpoch, + byte[] expectedBlockHash, byte[] expectedBatchId, byte[] expectedPayloadDigest, + List expectedParticipants) { + if (kind != expectedKind || !Objects.equals(participant, expectedParticipant) + || epoch != expectedEpoch || !Arrays.equals(blockHash, expectedBlockHash) + || !Arrays.equals(batchId, expectedBatchId) + || !Arrays.equals(payloadDigest, expectedPayloadDigest) + || !participants.equals(expectedParticipants)) { + throw new ArchivePersistenceException("Archive progress identity mismatch"); + } + } + + private static byte[] exactBytes(byte[] value, int length, String name) { + if (value == null || value.length != length) { + throw new IllegalArgumentException(name + " must be exactly " + length + " bytes"); + } + return Arrays.copyOf(value, value.length); + } + + private static List validateParticipants(List participants) { + Objects.requireNonNull(participants, "participants"); + if (participants.isEmpty()) { + throw new IllegalArgumentException("Archive participant set must not be empty"); + } + List copy = new ArrayList<>(participants.size()); + String previous = null; + for (String participant : participants) { + if (participant == null || participant.isEmpty() + || previous != null && previous.compareTo(participant) >= 0) { + throw new IllegalArgumentException( + "Archive participants must be non-empty, unique, and sorted"); + } + copy.add(participant); + previous = participant; + } + return Collections.unmodifiableList(copy); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java new file mode 100644 index 00000000000..d6123a0555a --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java @@ -0,0 +1,165 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; + +/** Checksummed prototype codec for archive C, D[i], and R progress identities. */ +public final class ArchiveProgressEnvelopeCodec { + + private static final int MAGIC = 0x54415047; // TAPG + private static final short VERSION = 1; + private static final int HEADER_LENGTH = 12; + private static final int MAX_FIELD_LENGTH = 1024; + private static final int MAX_PARTICIPANTS = 1024; + static final int MAX_ENCODED_LENGTH = 1024 * 1024; + + public byte[] encode(ArchiveProgressEnvelope envelope) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeByte(kindCode(envelope.getKind())); + output.writeByte(0); + output.writeInt(0); + output.writeLong(envelope.getEpoch()); + output.write(envelope.getBlockHash()); + output.write(envelope.getBatchId()); + output.write(envelope.getPayloadDigest()); + writeString(output, envelope.getParticipant() == null ? "" : envelope.getParticipant()); + output.writeInt(envelope.getParticipants().size()); + for (String participant : envelope.getParticipants()) { + writeString(output, participant); + } + output.flush(); + byte[] payload = bytes.toByteArray(); + int length = payload.length + Integer.BYTES; + if (length > MAX_ENCODED_LENGTH) { + throw new IllegalArgumentException("Archive progress envelope is too large"); + } + ByteBuffer.wrap(payload).putInt(8, length); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(payload); + output.writeInt(crc32c(payload)); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected progress envelope encoding failure", impossible); + } + } + + public ArchiveProgressEnvelope decode(byte[] encoded) { + if (encoded == null || encoded.length < HEADER_LENGTH + Integer.BYTES + || encoded.length > MAX_ENCODED_LENGTH) { + throw new IllegalArgumentException("Archive progress envelope length is invalid"); + } + int expectedChecksum = ByteBuffer.wrap(encoded, encoded.length - Integer.BYTES, + Integer.BYTES).getInt(); + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + if (expectedChecksum != crc32c(payload)) { + throw new IllegalArgumentException("Archive progress envelope checksum mismatch"); + } + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + if (input.readInt() != MAGIC || input.readShort() != VERSION) { + throw new IllegalArgumentException("Unsupported archive progress envelope header"); + } + Kind kind = decodeKind(input.readUnsignedByte()); + if (input.readUnsignedByte() != 0 || input.readInt() != encoded.length) { + throw new IllegalArgumentException("Unsupported archive progress envelope header"); + } + long epoch = input.readLong(); + byte[] blockHash = readExact(input, 32); + byte[] batchId = readExact(input, 16); + byte[] payloadDigest = readExact(input, 32); + String participant = readString(input, true); + int count = input.readInt(); + if (count <= 0 || count > MAX_PARTICIPANTS) { + throw new IllegalArgumentException("Archive progress participant count is invalid"); + } + List participants = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + participants.add(readString(input, false)); + } + if (input.available() != Integer.BYTES) { + throw new IllegalArgumentException("Archive progress envelope payload mismatch"); + } + return new ArchiveProgressEnvelope(kind, participant.isEmpty() ? null : participant, epoch, + blockHash, batchId, payloadDigest, participants); + } catch (EOFException truncated) { + throw new IllegalArgumentException("Archive progress envelope is truncated", truncated); + } catch (IOException invalid) { + throw new IllegalArgumentException("Invalid archive progress envelope", invalid); + } + } + + private static int kindCode(Kind kind) { + switch (kind) { + case APPLY_CHECKPOINT: + return 1; + case PARTICIPANT_PROGRESS: + return 2; + case READER_VISIBLE: + return 3; + default: + throw new IllegalArgumentException("Unknown archive progress envelope kind"); + } + } + + private static Kind decodeKind(int code) { + if (code == 1) { + return Kind.APPLY_CHECKPOINT; + } + if (code == 2) { + return Kind.PARTICIPANT_PROGRESS; + } + if (code == 3) { + return Kind.READER_VISIBLE; + } + throw new IllegalArgumentException("Unknown archive progress envelope kind"); + } + + private static void writeString(DataOutputStream output, String value) throws IOException { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + if (encoded.length > MAX_FIELD_LENGTH) { + throw new IllegalArgumentException("Archive progress string is too large"); + } + output.writeInt(encoded.length); + output.write(encoded); + } + + private static String readString(DataInputStream input, boolean allowEmpty) throws IOException { + int length = input.readInt(); + if (length < 0 || length > MAX_FIELD_LENGTH || !allowEmpty && length == 0) { + throw new IllegalArgumentException("Archive progress string length is invalid"); + } + byte[] encoded = readExact(input, length); + String decoded = new String(encoded, StandardCharsets.UTF_8); + if (!Arrays.equals(encoded, decoded.getBytes(StandardCharsets.UTF_8))) { + throw new IllegalArgumentException("Archive progress string is not valid UTF-8"); + } + return decoded; + } + + private static byte[] readExact(DataInputStream input, int length) throws IOException { + byte[] value = new byte[length]; + input.readFully(value); + return value; + } + + private static int crc32c(byte[] value) { + return Hashing.crc32c().hashBytes(value).asInt(); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressFile.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressFile.java new file mode 100644 index 00000000000..ed8e3d40567 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressFile.java @@ -0,0 +1,78 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Objects; + +/** Atomic-file prototype for one C or D[i] envelope. */ +final class ArchiveProgressFile { + + private final Path path; + private final Path temporary; + private final ArchiveProgressEnvelopeCodec codec; + private final FaultHook faultHook; + + ArchiveProgressFile(Path path, ArchiveProgressEnvelopeCodec codec) { + this(path, codec, temporary -> { }); + } + + ArchiveProgressFile(Path path, ArchiveProgressEnvelopeCodec codec, FaultHook faultHook) { + this.path = Objects.requireNonNull(path, "path"); + this.temporary = path.resolveSibling(path.getFileName() + ".tmp"); + this.codec = Objects.requireNonNull(codec, "codec"); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + ArchiveProgressEnvelope load() throws IOException { + if (!Files.exists(path)) { + throw new ArchivePersistenceException("Archive progress envelope is missing: " + path); + } + long size = Files.size(path); + if (size <= 0 || size > ArchiveProgressEnvelopeCodec.MAX_ENCODED_LENGTH) { + throw new ArchivePersistenceException("Archive progress envelope file length is invalid"); + } + try { + return codec.decode(Files.readAllBytes(path)); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Archive progress envelope is corrupt", invalid); + } + } + + void store(ArchiveProgressEnvelope envelope) throws IOException { + byte[] encoded = codec.encode(envelope); + Path directory = Objects.requireNonNull(path.getParent(), "progress directory"); + Files.createDirectories(directory); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(encoded); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + faultHook.afterTemporaryForce(temporary); + try { + Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new ArchivePersistenceException( + "Archive progress filesystem does not support atomic replacement", unsupported); + } + HistorySegmentStore.syncDirectory(directory); + } + + Path getTemporaryPath() { + return temporary; + } + + @FunctionalInterface + interface FaultHook { + void afterTemporaryForce(Path temporary) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java index e00af49f52d..64013575211 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java @@ -16,13 +16,13 @@ public final class ArchiveReadSnapshot implements Closeable { private final long targetBlock; private final long pinnedBlock; private final byte[] pinnedHash; - private final ServingKeyIndexGeneration serving; + private final ServingKeyIndex serving; private final PinnedLatestState latest; private final PinnedHistory history; private boolean closed; private ArchiveReadSnapshot(long targetBlock, long pinnedBlock, byte[] pinnedHash, - ServingKeyIndexGeneration serving, PinnedLatestState latest, PinnedHistory history) { + ServingKeyIndex serving, PinnedLatestState latest, PinnedHistory history) { if (targetBlock > pinnedBlock) { throw new IllegalArgumentException("Target block must not exceed pinned block"); } @@ -37,17 +37,59 @@ private ArchiveReadSnapshot(long targetBlock, long pinnedBlock, byte[] pinnedHas /** Takes ownership of already pinned resources, including on identity-validation failure. */ public static ArchiveReadSnapshot pin(long targetBlock, long pinnedBlock, byte[] pinnedHash, - ServingKeyIndexGeneration serving, PinnedLatestState latest, PinnedHistory history) + ServingKeyIndex serving, PinnedLatestState latest, PinnedHistory history) throws IOException { try { return new ArchiveReadSnapshot(targetBlock, pinnedBlock, pinnedHash, serving, latest, history); } catch (RuntimeException failure) { - closeAfterFailedPin(history, latest, failure); + closeAfterFailedPin(serving, history, latest, failure); throw failure; } } + /** Pins catalog, authoritative history, and latest-engine resources as one request unit. */ + public static ArchiveReadSnapshot pin(long targetBlock, + PersistentServingKeyIndexCatalog catalog, ArchiveProgressEnvelope readerVisible, + PinnedHistoryFactory historyFactory, PinnedLatestStateFactory latestFactory) + throws IOException { + Objects.requireNonNull(catalog, "catalog"); + Objects.requireNonNull(historyFactory, "historyFactory"); + Objects.requireNonNull(latestFactory, "latestFactory"); + PersistentServingKeyIndexGeneration serving = catalog.pin(readerVisible); + PinnedHistory history; + try { + history = historyFactory.pin(serving); + } catch (IOException | RuntimeException failure) { + serving.close(); + throw failure; + } + PinnedLatestState latest; + try { + latest = latestFactory.pin(serving); + } catch (IOException | RuntimeException failure) { + try { + history.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + serving.close(); + throw failure; + } + return pin(targetBlock, serving.getIndexedThrough(), serving.getHeadHash(), serving, latest, + history); + } + + /** Pins persistent commit/index/segment handles plus one caller-owned latest engine snapshot. */ + public static ArchiveReadSnapshot pin(long targetBlock, + PersistentServingKeyIndexCatalog catalog, ArchiveProgressEnvelope readerVisible, + java.nio.file.Path archiveDirectory, long maxSegmentSize, + PinnedLatestStateFactory latestFactory) throws IOException { + return pin(targetBlock, catalog, readerVisible, + serving -> PersistentCommittedHistoryReader.open( + archiveDirectory, maxSegmentSize, serving), latestFactory); + } + public synchronized OldValue get(String dbName, byte[] physicalRawKey) throws IOException { ensureOpen(); Objects.requireNonNull(dbName, "dbName"); @@ -108,6 +150,15 @@ public synchronized void close() throws IOException { failure.addSuppressed(e); } } + try { + serving.close(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } if (failure != null) { throw failure; } @@ -142,8 +193,8 @@ private static byte[] copyHash(byte[] hash, String name) { return Arrays.copyOf(hash, hash.length); } - private static void closeAfterFailedPin(PinnedHistory history, PinnedLatestState latest, - RuntimeException failure) throws IOException { + private static void closeAfterFailedPin(ServingKeyIndex serving, PinnedHistory history, + PinnedLatestState latest, RuntimeException failure) throws IOException { IOException closeFailure = null; if (history != null) { try { @@ -163,6 +214,17 @@ private static void closeAfterFailedPin(PinnedHistory history, PinnedLatestState } } } + if (serving != null) { + try { + serving.close(); + } catch (IOException e) { + if (closeFailure == null) { + closeFailure = e; + } else { + closeFailure.addSuppressed(e); + } + } + } if (closeFailure != null) { failure.addSuppressed(closeFailure); } @@ -173,6 +235,11 @@ public interface PinnedLatestState extends Closeable { byte[] getBlockHash(); + /** Empty means the legacy/in-memory prototype is not bound to engine source identities. */ + default byte[] getSourceIdentityDigest() { + return new byte[0]; + } + OldValue get(String dbName, byte[] physicalRawKey) throws IOException; List range(String dbName, byte[] lowerInclusive, byte[] upperExclusive) @@ -191,4 +258,14 @@ public interface PinnedHistory extends Closeable { OldValue read(String dbName, byte[] physicalRawKey, long firstChangeBlock) throws IOException; } + + @FunctionalInterface + public interface PinnedHistoryFactory { + PinnedHistory pin(PersistentServingKeyIndexGeneration serving) throws IOException; + } + + @FunctionalInterface + public interface PinnedLatestStateFactory { + PinnedLatestState pin(PersistentServingKeyIndexGeneration serving) throws IOException; + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java new file mode 100644 index 00000000000..d986f04f6a6 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java @@ -0,0 +1,61 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; + +/** Atomically publishes a reader-visible R identity derived from committed history. */ +public final class ArchiveReaderHeadPublisher { + + private final HistoryCommitStore history; + private final ArchiveProgressFile progressFile; + private final List participants; + + public ArchiveReaderHeadPublisher(HistoryCommitStore history, Path path, + List participants) { + this(history, path, participants, temporary -> { }); + } + + ArchiveReaderHeadPublisher(HistoryCommitStore history, Path path, List participants, + ArchiveProgressFile.FaultHook faultHook) { + this.history = Objects.requireNonNull(history, "history"); + this.progressFile = new ArchiveProgressFile(Objects.requireNonNull(path, "path"), + new ArchiveProgressEnvelopeCodec(), Objects.requireNonNull(faultHook, "faultHook")); + this.participants = validateParticipants(participants); + } + + public void publish(long epoch) throws IOException { + HistoryCommitMarker marker = history.get(epoch); + if (marker == null || marker.getMeta().getEpoch() != epoch + || !marker.getDatabases().equals(participants)) { + throw new ArchivePersistenceException( + "Missing or mismatched committed reader identity at epoch " + epoch); + } + progressFile.store(new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, epoch, + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), participants)); + } + + private static List validateParticipants(List participants) { + Objects.requireNonNull(participants, "participants"); + if (participants.isEmpty()) { + throw new IllegalArgumentException("Archive participant set must not be empty"); + } + List copy = new ArrayList<>(participants.size()); + String previous = null; + for (String participant : participants) { + if (participant == null || participant.isEmpty() + || previous != null && previous.compareTo(participant) >= 0) { + throw new IllegalArgumentException( + "Archive participants must be non-empty, unique, and sorted"); + } + copy.add(participant); + previous = participant; + } + return Collections.unmodifiableList(copy); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java new file mode 100644 index 00000000000..fee9c953864 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java @@ -0,0 +1,117 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryAction; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; + +/** Executes a fresh H/C/D[i]/R plan against explicitly durable storage boundaries. */ +public final class ArchiveRecoveryExecutor { + + private final RecoveryStorage storage; + private final FaultHook faultHook; + + public ArchiveRecoveryExecutor(RecoveryStorage storage) { + this(storage, action -> { }); + } + + ArchiveRecoveryExecutor(RecoveryStorage storage, FaultHook faultHook) { + this.storage = Objects.requireNonNull(storage, "storage"); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + public RecoveryPlan recover() { + try { + RecoverySnapshot snapshot = Objects.requireNonNull(storage.scan(), "recovery snapshot"); + RecoveryPlan plan = ArchiveRecoveryPlanner.plan(snapshot.getHistoryHead(), + snapshot.getCheckpointHead(), snapshot.getParticipantHeads(), + snapshot.getReaderVisibleHead()); + for (RecoveryAction action : plan.getActions()) { + execute(action); + faultHook.afterDurableAction(action); + } + return plan; + } catch (IOException failure) { + throw new ArchivePersistenceException("Archive recovery action failed", failure); + } + } + + private void execute(RecoveryAction action) throws IOException { + ActionType type = action.getType(); + switch (type) { + case TRUNCATE_HISTORY: + storage.truncateHistoryAndSync(action.getLastEpoch()); + return; + case REPLAY_PARTICIPANT: + storage.replayParticipantAndSyncProgress(action.getParticipant(), + action.getFirstEpoch(), action.getLastEpoch()); + return; + case PUBLISH_READER_HEAD: + storage.publishReaderHeadAndSync(action.getLastEpoch()); + return; + default: + throw new ArchivePersistenceException("Unsupported archive recovery action: " + type); + } + } + + /** + * Durable recovery boundary supplied by the archive history, checkpoint and participant engines. + * + *

{@link #replayParticipantAndSyncProgress} must atomically persist the participant's business + * mutations and D[i] progress in one sync engine batch. Returning before both are durable violates + * the recovery contract. + */ + public interface RecoveryStorage { + RecoverySnapshot scan() throws IOException; + + void truncateHistoryAndSync(long historyHead) throws IOException; + + void replayParticipantAndSyncProgress(String participant, long firstEpoch, long lastEpoch) + throws IOException; + + void publishReaderHeadAndSync(long readerVisibleHead) throws IOException; + } + + /** Immutable result of one fresh durable H/C/D[i]/R scan. */ + public static final class RecoverySnapshot { + private final long historyHead; + private final long checkpointHead; + private final SortedMap participantHeads; + private final long readerVisibleHead; + + public RecoverySnapshot(long historyHead, long checkpointHead, + Map participantHeads, long readerVisibleHead) { + this.historyHead = historyHead; + this.checkpointHead = checkpointHead; + this.participantHeads = Collections.unmodifiableSortedMap( + new TreeMap<>(Objects.requireNonNull(participantHeads, "participantHeads"))); + this.readerVisibleHead = readerVisibleHead; + } + + public long getHistoryHead() { + return historyHead; + } + + public long getCheckpointHead() { + return checkpointHead; + } + + public SortedMap getParticipantHeads() { + return participantHeads; + } + + public long getReaderVisibleHead() { + return readerVisibleHead; + } + } + + @FunctionalInterface + interface FaultHook { + void afterDurableAction(RecoveryAction action) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryPlanner.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryPlanner.java new file mode 100644 index 00000000000..8763ceedecc --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryPlanner.java @@ -0,0 +1,181 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; + +/** Deterministic fail-closed planner for one durable H/C/D[i]/R recovery snapshot. */ +public final class ArchiveRecoveryPlanner { + + static final long MAX_REPLAY_EPOCHS_PER_ACTION = 1024; + + private ArchiveRecoveryPlanner() { + } + + public static RecoveryPlan plan(long historyHead, long checkpointHead, + Map participantHeads, long readerVisibleHead) { + if (historyHead < 0 || checkpointHead < 0 || readerVisibleHead < 0) { + throw new ArchivePersistenceException("Archive recovery heads must be non-negative"); + } + if (checkpointHead > historyHead) { + throw new ArchivePersistenceException("Archive checkpoint is ahead of history"); + } + SortedMap sortedHeads = validateParticipants(participantHeads, checkpointHead); + long safeHead = Math.min(historyHead, checkpointHead); + for (long participantHead : sortedHeads.values()) { + safeHead = Math.min(safeHead, participantHead); + } + if (readerVisibleHead > safeHead) { + throw new ArchivePersistenceException("Reader-visible archive head is unsafe"); + } + + List actions = new ArrayList<>(); + if (historyHead > checkpointHead) { + actions.add(RecoveryAction.truncateHistory(checkpointHead)); + } + sortedHeads.forEach((participant, appliedHead) -> addReplayActions(actions, participant, + appliedHead, checkpointHead)); + if (readerVisibleHead < checkpointHead) { + actions.add(RecoveryAction.publishReaderHead(checkpointHead)); + } + return new RecoveryPlan(historyHead, checkpointHead, sortedHeads, readerVisibleHead, + safeHead, actions); + } + + private static SortedMap validateParticipants(Map participantHeads, + long checkpointHead) { + Objects.requireNonNull(participantHeads, "participantHeads"); + if (participantHeads.isEmpty()) { + throw new ArchivePersistenceException("Archive recovery participant set is empty"); + } + SortedMap sorted = new TreeMap<>(); + participantHeads.forEach((participant, head) -> { + if (participant == null || participant.isEmpty() || head == null || head < 0) { + throw new ArchivePersistenceException("Archive participant progress is invalid"); + } + if (head > checkpointHead) { + throw new ArchivePersistenceException( + "Archive participant is ahead of the checkpoint: " + participant); + } + sorted.put(participant, head); + }); + return sorted; + } + + private static void addReplayActions(List actions, String participant, + long appliedHead, long checkpointHead) { + if (appliedHead == checkpointHead) { + return; + } + long first = appliedHead + 1; + while (first <= checkpointHead) { + long remaining = checkpointHead - first; + long last = remaining >= MAX_REPLAY_EPOCHS_PER_ACTION + ? first + MAX_REPLAY_EPOCHS_PER_ACTION - 1 : checkpointHead; + actions.add(RecoveryAction.replayParticipant(participant, first, last)); + if (last == checkpointHead) { + break; + } + first = last + 1; + } + } + + public enum ActionType { + TRUNCATE_HISTORY, + REPLAY_PARTICIPANT, + PUBLISH_READER_HEAD + } + + public static final class RecoveryAction { + private final ActionType type; + private final String participant; + private final long firstEpoch; + private final long lastEpoch; + + private RecoveryAction(ActionType type, String participant, long firstEpoch, + long lastEpoch) { + this.type = type; + this.participant = participant; + this.firstEpoch = firstEpoch; + this.lastEpoch = lastEpoch; + } + + private static RecoveryAction truncateHistory(long head) { + return new RecoveryAction(ActionType.TRUNCATE_HISTORY, null, head, head); + } + + private static RecoveryAction replayParticipant(String participant, long firstEpoch, + long lastEpoch) { + return new RecoveryAction(ActionType.REPLAY_PARTICIPANT, participant, firstEpoch, + lastEpoch); + } + + private static RecoveryAction publishReaderHead(long head) { + return new RecoveryAction(ActionType.PUBLISH_READER_HEAD, null, head, head); + } + + public ActionType getType() { + return type; + } + + public String getParticipant() { + return participant; + } + + public long getFirstEpoch() { + return firstEpoch; + } + + public long getLastEpoch() { + return lastEpoch; + } + } + + public static final class RecoveryPlan { + private final long historyHead; + private final long checkpointHead; + private final SortedMap participantHeads; + private final long readerVisibleHead; + private final long safeHeadBeforeRecovery; + private final List actions; + + private RecoveryPlan(long historyHead, long checkpointHead, + SortedMap participantHeads, long readerVisibleHead, + long safeHeadBeforeRecovery, List actions) { + this.historyHead = historyHead; + this.checkpointHead = checkpointHead; + this.participantHeads = Collections.unmodifiableSortedMap(new TreeMap<>(participantHeads)); + this.readerVisibleHead = readerVisibleHead; + this.safeHeadBeforeRecovery = safeHeadBeforeRecovery; + this.actions = Collections.unmodifiableList(new ArrayList<>(actions)); + } + + public long getHistoryHead() { + return historyHead; + } + + public long getCheckpointHead() { + return checkpointHead; + } + + public SortedMap getParticipantHeads() { + return participantHeads; + } + + public long getReaderVisibleHead() { + return readerVisibleHead; + } + + public long getSafeHeadBeforeRecovery() { + return safeHeadBeforeRecovery; + } + + public List getActions() { + return actions; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java new file mode 100644 index 00000000000..00b6443ca0e --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java @@ -0,0 +1,114 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; + +/** Validates durable C and D[i] identities before exposing one recovery snapshot. */ +public final class ArchiveRecoveryScanner { + + private final HistoryIdentitySource history; + private final ProgressIdentitySource progress; + private final List participants; + + public ArchiveRecoveryScanner(HistoryIdentitySource history, ProgressIdentitySource progress, + List participants) { + this.history = Objects.requireNonNull(history, "history"); + this.progress = Objects.requireNonNull(progress, "progress"); + this.participants = validateParticipants(participants); + } + + public RecoverySnapshot scan() throws IOException { + ArchiveProgressEnvelope checkpoint = progress.loadCheckpoint(); + if (checkpoint == null) { + throw new ArchivePersistenceException("Missing archive apply checkpoint"); + } + Map loadedProgress = progress.loadParticipantProgress(); + if (loadedProgress == null) { + throw new ArchivePersistenceException("Missing archive participant progress set"); + } + SortedMap participantProgress = + new TreeMap<>(loadedProgress); + if (!new ArrayList<>(participantProgress.keySet()).equals(participants)) { + throw new ArchivePersistenceException("Archive participant progress set mismatch"); + } + + validateEnvelope(checkpoint, Kind.APPLY_CHECKPOINT, null); + SortedMap participantHeads = new TreeMap<>(); + for (String participant : participants) { + ArchiveProgressEnvelope envelope = participantProgress.get(participant); + if (envelope == null) { + throw new ArchivePersistenceException( + "Missing archive participant progress: " + participant); + } + validateEnvelope(envelope, Kind.PARTICIPANT_PROGRESS, participant); + participantHeads.put(participant, envelope.getEpoch()); + } + ArchiveProgressEnvelope readerVisible = progress.loadReaderVisible(); + if (readerVisible == null) { + throw new ArchivePersistenceException("Missing archive reader-visible progress"); + } + validateEnvelope(readerVisible, Kind.READER_VISIBLE, null); + return new RecoverySnapshot(history.committedHeadEpoch(), checkpoint.getEpoch(), + participantHeads, readerVisible.getEpoch()); + } + + private void validateEnvelope(ArchiveProgressEnvelope envelope, Kind kind, String participant) + throws IOException { + HistoryCommitMarker marker = history.committedMarker(envelope.getEpoch()); + if (marker == null) { + throw new ArchivePersistenceException( + "Missing committed history identity at epoch " + envelope.getEpoch()); + } + if (marker.getMeta().getEpoch() != envelope.getEpoch() + || !marker.getDatabases().equals(participants)) { + throw new ArchivePersistenceException( + "Committed history identity mismatch at epoch " + envelope.getEpoch()); + } + envelope.requireIdentity(kind, participant, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), participants); + } + + private static List validateParticipants(List participants) { + Objects.requireNonNull(participants, "participants"); + if (participants.isEmpty()) { + throw new IllegalArgumentException("Archive participant set must not be empty"); + } + List copy = new ArrayList<>(participants.size()); + String previous = null; + for (String participant : participants) { + if (participant == null || participant.isEmpty() + || previous != null && previous.compareTo(participant) >= 0) { + throw new IllegalArgumentException( + "Archive participants must be non-empty, unique, and sorted"); + } + copy.add(participant); + previous = participant; + } + return Collections.unmodifiableList(copy); + } + + /** Committed history identity lookup. A missing epoch returns {@code null}. */ + public interface HistoryIdentitySource { + long committedHeadEpoch() throws IOException; + + HistoryCommitMarker committedMarker(long epoch) throws IOException; + } + + /** Durable apply checkpoint, participant progress and reader-visible head lookup. */ + public interface ProgressIdentitySource { + ArchiveProgressEnvelope loadCheckpoint() throws IOException; + + Map loadParticipantProgress() throws IOException; + + ArchiveProgressEnvelope loadReaderVisible() throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRestartCheckpoint.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRestartCheckpoint.java new file mode 100644 index 00000000000..cb0f71a96c6 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRestartCheckpoint.java @@ -0,0 +1,197 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; + +/** Atomic restart anchor for a previously validated committed history prefix. */ +final class ArchiveRestartCheckpoint { + + private static final int MAGIC = 0x54415246; // TARF + private static final short VERSION = 1; + private static final int HEADER_LENGTH = 36; + private static final int MAX_LENGTH = 2 * 1024 * 1024; + private static final String FILE_NAME = "restart.checkpoint"; + private static final String TEMP_FILE_NAME = "restart.checkpoint.tmp"; + + private final long firstEpoch; + private final long recordCount; + private final int commitRecordLength; + private final HistoryCommitMarker marker; + private final byte[] encodedMarker; + + private ArchiveRestartCheckpoint(long firstEpoch, long recordCount, int commitRecordLength, + HistoryCommitMarker marker, byte[] encodedMarker) { + this.firstEpoch = firstEpoch; + this.recordCount = recordCount; + this.commitRecordLength = commitRecordLength; + this.marker = marker; + this.encodedMarker = Arrays.copyOf(encodedMarker, encodedMarker.length); + } + + static ArchiveRestartCheckpoint load(Path archiveDirectory, + HistoryCommitMarkerCodec markerCodec) throws IOException { + Path path = archiveDirectory.resolve(FILE_NAME); + if (!Files.exists(path)) { + return null; + } + long size = Files.size(path); + if (size < HEADER_LENGTH + Integer.BYTES || size > MAX_LENGTH) { + throw new ArchivePersistenceException("Archive restart checkpoint length is invalid"); + } + byte[] encoded = Files.readAllBytes(path); + try { + return decode(encoded, markerCodec); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Archive restart checkpoint is corrupt", invalid); + } + } + + static ArchiveRestartCheckpoint persist(Path archiveDirectory, long firstEpoch, + long recordCount, int commitRecordLength, HistoryCommitMarker marker, + HistoryCommitMarkerCodec markerCodec) throws IOException { + if (marker == null || recordCount <= 0 || commitRecordLength <= 0 + || firstEpoch + recordCount - 1 != marker.getMeta().getEpoch()) { + throw new IllegalArgumentException("Invalid archive restart checkpoint state"); + } + byte[] markerBytes = markerCodec.encode(marker); + if (markerBytes.length != commitRecordLength) { + throw new IllegalArgumentException("Checkpoint commit record length mismatch"); + } + byte[] encoded = encode(firstEpoch, recordCount, commitRecordLength, markerBytes); + Files.createDirectories(archiveDirectory); + Path temporary = archiveDirectory.resolve(TEMP_FILE_NAME); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + writeFully(channel, ByteBuffer.wrap(encoded)); + channel.force(true); + } + Path target = archiveDirectory.resolve(FILE_NAME); + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new ArchivePersistenceException( + "Archive filesystem does not support atomic restart checkpoint replacement", + unsupported); + } + HistorySegmentStore.syncDirectory(archiveDirectory); + return new ArchiveRestartCheckpoint(firstEpoch, recordCount, commitRecordLength, marker, + markerBytes); + } + + long getFirstEpoch() { + return firstEpoch; + } + + long getRecordCount() { + return recordCount; + } + + int getCommitRecordLength() { + return commitRecordLength; + } + + HistoryCommitMarker getMarker() { + return marker; + } + + byte[] getEncodedMarker() { + return Arrays.copyOf(encodedMarker, encodedMarker.length); + } + + private static byte[] encode(long firstEpoch, long recordCount, int commitRecordLength, + byte[] markerBytes) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(0); + output.writeLong(firstEpoch); + output.writeLong(recordCount); + output.writeInt(commitRecordLength); + output.writeInt(markerBytes.length); + output.write(markerBytes); + output.flush(); + byte[] withoutChecksum = bytes.toByteArray(); + int length = withoutChecksum.length + Integer.BYTES; + if (length > MAX_LENGTH) { + throw new IllegalArgumentException("Archive restart checkpoint is too large"); + } + ByteBuffer.wrap(withoutChecksum).putInt(8, length); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(withoutChecksum); + output.writeInt(crc32c(withoutChecksum)); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected checkpoint encoding failure", impossible); + } + } + + private static ArchiveRestartCheckpoint decode(byte[] encoded, + HistoryCommitMarkerCodec markerCodec) { + if (encoded == null || encoded.length < HEADER_LENGTH + Integer.BYTES + || encoded.length > MAX_LENGTH) { + throw new IllegalArgumentException("Restart checkpoint length is invalid"); + } + int checksum = ByteBuffer.wrap(encoded, encoded.length - Integer.BYTES, + Integer.BYTES).getInt(); + byte[] withoutChecksum = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + if (checksum != crc32c(withoutChecksum)) { + throw new IllegalArgumentException("Restart checkpoint checksum mismatch"); + } + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != encoded.length) { + throw new IllegalArgumentException("Unsupported restart checkpoint header"); + } + long firstEpoch = input.readLong(); + long recordCount = input.readLong(); + int recordLength = input.readInt(); + int markerLength = input.readInt(); + if (firstEpoch < 0 || recordCount <= 0 || recordLength <= 0 + || markerLength != recordLength || markerLength > input.available() - Integer.BYTES) { + throw new IllegalArgumentException("Invalid restart checkpoint fields"); + } + byte[] markerBytes = new byte[markerLength]; + input.readFully(markerBytes); + if (input.available() != Integer.BYTES) { + throw new IllegalArgumentException("Restart checkpoint payload mismatch"); + } + HistoryCommitMarker marker = markerCodec.decode(markerBytes); + if (firstEpoch + recordCount - 1 != marker.getMeta().getEpoch()) { + throw new IllegalArgumentException("Restart checkpoint ordinal mismatch"); + } + return new ArchiveRestartCheckpoint(firstEpoch, recordCount, recordLength, marker, + markerBytes); + } catch (IOException invalid) { + throw new IllegalArgumentException("Restart checkpoint is truncated", invalid); + } + } + + private static int crc32c(byte[] bytes) { + return Hashing.crc32c().hashBytes(bytes).asInt(); + } + + private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java new file mode 100644 index 00000000000..b7120697af1 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java @@ -0,0 +1,228 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; + +/** Atomic startup intent identifying one validated history truncation target. */ +final class ArchiveTruncationIntent { + + private static final int MAGIC = 0x54415449; // TATI + private static final short VERSION = 1; + private static final String FILE_NAME = "truncation.intent"; + private static final String TEMP_FILE_NAME = "truncation.intent.tmp"; + private static final int HEADER_LENGTH = 36; + private static final int MAX_LENGTH = 2 * 1024 * 1024; + + private final long firstEpoch; + private final long recordCount; + private final int recordLength; + private final HistoryCommitMarker marker; + private final byte[] encodedMarker; + + private ArchiveTruncationIntent(long firstEpoch, long recordCount, int recordLength, + HistoryCommitMarker marker, byte[] encodedMarker) { + this.firstEpoch = firstEpoch; + this.recordCount = recordCount; + this.recordLength = recordLength; + this.marker = marker; + this.encodedMarker = Arrays.copyOf(encodedMarker, encodedMarker.length); + } + + static ArchiveTruncationIntent prepare(Path archiveDirectory, HistoryCommitStore commits, + HistoryIndexStore index, HistorySegmentStore bodies, long targetEpoch, + HistoryCommitMarkerCodec markerCodec) throws IOException { + return prepare(archiveDirectory, commits, index, bodies, targetEpoch, markerCodec, + temporary -> { }); + } + + static ArchiveTruncationIntent prepare(Path archiveDirectory, HistoryCommitStore commits, + HistoryIndexStore index, HistorySegmentStore bodies, long targetEpoch, + HistoryCommitMarkerCodec markerCodec, FaultHook faultHook) throws IOException { + HistoryCommitMarker marker = commits.get(targetEpoch); + if (marker == null) { + throw new ArchivePersistenceException( + "Archive truncation intent target is outside committed history"); + } + HistoryIndexRecord indexRecord = index.read(marker.getIndexLocation()); + BlockReverseDiff body = bodies.read(marker.getHistoryLocation()); + if (!marker.getMeta().equals(indexRecord.getMeta()) + || !marker.getMeta().equals(body.getMeta()) + || !sameLocation(marker.getHistoryLocation(), indexRecord.getHistoryLocation())) { + throw new ArchivePersistenceException("Archive truncation intent target is inconsistent"); + } + long count = targetEpoch - commits.firstEpoch() + 1; + byte[] markerBytes = markerCodec.encode(marker); + ArchiveTruncationIntent intent = new ArchiveTruncationIntent(commits.firstEpoch(), count, + commits.getRecordLength(), marker, markerBytes); + intent.persist(archiveDirectory, faultHook); + return intent; + } + + static ArchiveTruncationIntent load(Path archiveDirectory, + HistoryCommitMarkerCodec markerCodec) throws IOException { + Path path = archiveDirectory.resolve(FILE_NAME); + if (!Files.exists(path)) { + return null; + } + try { + return decode(Files.readAllBytes(path), markerCodec); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Archive truncation intent is corrupt", invalid); + } + } + + void clear(Path archiveDirectory) throws IOException { + Files.deleteIfExists(archiveDirectory.resolve(FILE_NAME)); + HistorySegmentStore.syncDirectory(archiveDirectory); + } + + ArchiveRestartCheckpoint persistCheckpoint(Path archiveDirectory, + HistoryCommitMarkerCodec markerCodec) throws IOException { + return ArchiveRestartCheckpoint.persist(archiveDirectory, firstEpoch, recordCount, + recordLength, marker, markerCodec); + } + + long commitEndOffset() { + return recordCount * (long) recordLength; + } + + long markerOffset() { + return (recordCount - 1) * (long) recordLength; + } + + long getRecordCount() { + return recordCount; + } + + HistoryCommitMarker getMarker() { + return marker; + } + + byte[] getEncodedMarker() { + return Arrays.copyOf(encodedMarker, encodedMarker.length); + } + + private void persist(Path archiveDirectory, FaultHook faultHook) throws IOException { + byte[] encoded = encode(); + Files.createDirectories(archiveDirectory); + Path temporary = archiveDirectory.resolve(TEMP_FILE_NAME); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + writeFully(channel, ByteBuffer.wrap(encoded)); + channel.force(true); + } + faultHook.afterTemporaryForce(temporary); + try { + Files.move(temporary, archiveDirectory.resolve(FILE_NAME), + StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new ArchivePersistenceException( + "Archive filesystem does not support atomic truncation intent", unsupported); + } + HistorySegmentStore.syncDirectory(archiveDirectory); + } + + private byte[] encode() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(0); + output.writeLong(firstEpoch); + output.writeLong(recordCount); + output.writeInt(recordLength); + output.writeInt(encodedMarker.length); + output.write(encodedMarker); + output.flush(); + byte[] payload = bytes.toByteArray(); + int length = payload.length + Integer.BYTES; + if (length > MAX_LENGTH) { + throw new IllegalArgumentException("Archive truncation intent is too large"); + } + ByteBuffer.wrap(payload).putInt(8, length); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(payload); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected truncation intent encoding failure", impossible); + } + } + + private static ArchiveTruncationIntent decode(byte[] encoded, + HistoryCommitMarkerCodec markerCodec) { + if (encoded == null || encoded.length < HEADER_LENGTH + Integer.BYTES + || encoded.length > MAX_LENGTH) { + throw new IllegalArgumentException("Archive truncation intent length is invalid"); + } + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(encoded, encoded.length - Integer.BYTES, + Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new IllegalArgumentException("Archive truncation intent checksum mismatch"); + } + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != encoded.length) { + throw new IllegalArgumentException("Unsupported archive truncation intent header"); + } + long firstEpoch = input.readLong(); + long recordCount = input.readLong(); + int recordLength = input.readInt(); + int markerLength = input.readInt(); + if (firstEpoch < 0 || recordCount <= 0 || recordLength <= 0 + || markerLength != recordLength || markerLength > input.available() - Integer.BYTES) { + throw new IllegalArgumentException("Invalid archive truncation intent fields"); + } + byte[] markerBytes = new byte[markerLength]; + input.readFully(markerBytes); + if (input.available() != Integer.BYTES) { + throw new IllegalArgumentException("Archive truncation intent payload mismatch"); + } + HistoryCommitMarker marker = markerCodec.decode(markerBytes); + if (firstEpoch + recordCount - 1 != marker.getMeta().getEpoch()) { + throw new IllegalArgumentException("Archive truncation intent ordinal mismatch"); + } + return new ArchiveTruncationIntent(firstEpoch, recordCount, recordLength, marker, + markerBytes); + } catch (IOException invalid) { + throw new IllegalArgumentException("Archive truncation intent is truncated", invalid); + } + } + + private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } + + private static boolean sameLocation(HistoryLocation expected, HistoryLocation actual) { + return expected.getSegmentId() == actual.getSegmentId() + && expected.getOffset() == actual.getOffset() + && expected.getRecordLength() == actual.getRecordLength() + && expected.getBodyChecksum() == actual.getBodyChecksum() + && Arrays.equals(expected.getBodyDigest(), actual.getBodyDigest()); + } + + @FunctionalInterface + interface FaultHook { + void afterTemporaryForce(Path temporary) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java new file mode 100644 index 00000000000..1f25d2b976f --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java @@ -0,0 +1,96 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Objects; + +/** Completes an atomic truncation intent before normal checkpoint-based startup. */ +public final class ArchiveTruncationRecovery { + + private final Path archiveDirectory; + private final long maxSegmentSize; + private final HistoryCommitMarkerCodec markerCodec = new HistoryCommitMarkerCodec(); + private final FaultHook faultHook; + + public ArchiveTruncationRecovery(Path archiveDirectory, long maxSegmentSize) { + this(archiveDirectory, maxSegmentSize, stage -> { }); + } + + ArchiveTruncationRecovery(Path archiveDirectory, long maxSegmentSize, FaultHook faultHook) { + this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + if (maxSegmentSize <= 0) { + throw new IllegalArgumentException("maxSegmentSize must be positive"); + } + this.maxSegmentSize = maxSegmentSize; + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + public boolean recover() throws IOException { + ArchiveTruncationIntent intent = ArchiveTruncationIntent.load(archiveDirectory, markerCodec); + if (intent == null) { + return false; + } + shrinkCommitLog(intent); + faultHook.afterDurableStage(Stage.COMMIT_SHRUNK); + ArchiveRestartCheckpoint checkpoint = intent.persistCheckpoint(archiveDirectory, markerCodec); + faultHook.afterDurableStage(Stage.CHECKPOINT_PUBLISHED); + + try (HistoryIndexStore index = new HistoryIndexStore( + archiveDirectory, new HistoryIndexCodec(), checkpoint)) { + index.truncateAfter(intent.getMarker().getIndexLocation(), intent.getRecordCount()); + } + faultHook.afterDurableStage(Stage.INDEX_TRUNCATED); + try (HistorySegmentStore bodies = new HistorySegmentStore(archiveDirectory, + new BlockHistoryCodec(), maxSegmentSize, checkpoint)) { + bodies.truncateAfter(intent.getMarker().getHistoryLocation(), intent.getRecordCount()); + } + faultHook.afterDurableStage(Stage.BODY_TRUNCATED); + intent.clear(archiveDirectory); + return true; + } + + private void shrinkCommitLog(ArchiveTruncationIntent intent) throws IOException { + Path path = archiveDirectory.resolve("commits/commit.log"); + if (!Files.exists(path)) { + throw new ArchivePersistenceException("Committed history log is missing during truncation"); + } + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ, + StandardOpenOption.WRITE)) { + if (channel.size() < intent.commitEndOffset()) { + throw new ArchivePersistenceException( + "Committed history log is shorter than truncation intent"); + } + ByteBuffer marker = ByteBuffer.allocate(intent.getEncodedMarker().length); + channel.position(intent.markerOffset()); + while (marker.hasRemaining()) { + if (channel.read(marker) < 0) { + throw new ArchivePersistenceException( + "Committed history target is truncated during recovery"); + } + } + if (!Arrays.equals(marker.array(), intent.getEncodedMarker())) { + throw new ArchivePersistenceException( + "Committed history target does not match truncation intent"); + } + channel.truncate(intent.commitEndOffset()); + channel.force(true); + } + } + + public enum Stage { + COMMIT_SHRUNK, + CHECKPOINT_PUBLISHED, + INDEX_TRUNCATED, + BODY_TRUNCATED + } + + @FunctionalInterface + interface FaultHook { + void afterDurableStage(Stage stage) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java index eb880d3ad46..ea7bc8a8ef3 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java @@ -21,7 +21,7 @@ private HistoricalRangeOverlay() { } public static List materialize(String dbName, long targetBlock, long upperBound, - KeyRange range, List pinnedLatest, ServingKeyIndexGeneration index, + KeyRange range, List pinnedLatest, ServingKeyIndex index, HistoricalValueReader history, Limits limits) throws IOException { Objects.requireNonNull(dbName, "dbName"); Objects.requireNonNull(range, "range"); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java index 8c8ae56a02c..daf419edb07 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java @@ -32,14 +32,25 @@ public final class HistoryCommitStore implements Closeable { private long firstEpoch = -1; private long recordCount; private int recordLength; + private long startupScannedRecords; public HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec) throws IOException { - this(archiveDirectory, codec, ignored -> { }); + this(archiveDirectory, codec, null, ignored -> { }); } HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec, DirectorySync postForceHook) throws IOException { + this(archiveDirectory, codec, null, postForceHook); + } + + HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec, + ArchiveRestartCheckpoint checkpoint) throws IOException { + this(archiveDirectory, codec, checkpoint, ignored -> { }); + } + + HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec, + ArchiveRestartCheckpoint checkpoint, DirectorySync postForceHook) throws IOException { this.directory = archiveDirectory.resolve("commits"); this.logPath = directory.resolve(FILE_NAME); this.codec = codec; @@ -51,7 +62,7 @@ public HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec) if (created) { HistorySegmentStore.syncDirectory(directory); } - scanAndRepairTruncatedTail(); + scanAndRepairTruncatedTail(checkpoint); channel.position(channel.size()); } @@ -124,6 +135,27 @@ public synchronized void removeHead(BlockSnapshotMeta expected) throws IOExcepti channel.position(channel.size()); } + /** Durably removes every committed marker after {@code lastEpoch}. */ + public synchronized void truncateAfter(long lastEpoch) throws IOException { + if (uncertainFrom >= 0) { + throw new IllegalStateException("Cannot truncate commit records with uncertain durability"); + } + HistoryCommitMarker last = get(lastEpoch); + if (last == null) { + throw new IllegalArgumentException("Commit truncation target is outside the committed prefix"); + } + long newCount = lastEpoch - firstEpoch + 1; + if (newCount == recordCount) { + return; + } + channel.truncate(newCount * (long) recordLength); + channel.force(true); + postForceHook.sync(directory); + recordCount = newCount; + head = last; + channel.position(channel.size()); + } + public synchronized HistoryCommitMarker head() { return head; } @@ -173,12 +205,39 @@ Path getLogPath() { return logPath; } - private void scanAndRepairTruncatedTail() throws IOException { + int getRecordLength() { + return recordLength; + } + + long getStartupScannedRecords() { + return startupScannedRecords; + } + + private void scanAndRepairTruncatedTail(ArchiveRestartCheckpoint checkpoint) + throws IOException { long offset = 0; HistoryCommitMarker previous = null; int expectedLength = 0; long count = 0; long size = channel.size(); + if (checkpoint != null) { + expectedLength = checkpoint.getCommitRecordLength(); + count = checkpoint.getRecordCount(); + firstEpoch = checkpoint.getFirstEpoch(); + long checkpointOffset = (count - 1) * (long) expectedLength; + if (checkpointOffset < 0 || checkpointOffset + expectedLength > size) { + throw new ArchivePersistenceException( + "Restart checkpoint is outside the committed history log"); + } + byte[] checkpointRecord = read(checkpointOffset, expectedLength); + startupScannedRecords++; + if (!java.util.Arrays.equals(checkpointRecord, checkpoint.getEncodedMarker())) { + throw new ArchivePersistenceException( + "Restart checkpoint does not match the committed history log"); + } + previous = codec.decode(checkpointRecord); + offset = checkpointOffset + expectedLength; + } while (offset < size) { long remaining = size - offset; if (remaining < HistoryCommitMarkerCodec.HEADER_LENGTH) { @@ -218,6 +277,7 @@ private void scanAndRepairTruncatedTail() throws IOException { } previous = marker; count++; + startupScannedRecords++; offset += length; } recordLength = expectedLength; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java index 3c485849b61..cb173374062 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java @@ -21,8 +21,14 @@ public final class HistoryIndexStore implements Closeable { private final HistoryIndexCodec codec; private final FileChannel channel; private ScanResult scanResult; + private long startupScannedRecords; public HistoryIndexStore(Path archiveDirectory, HistoryIndexCodec codec) throws IOException { + this(archiveDirectory, codec, null); + } + + HistoryIndexStore(Path archiveDirectory, HistoryIndexCodec codec, + ArchiveRestartCheckpoint checkpoint) throws IOException { this.archiveDirectory = archiveDirectory; this.indexPath = archiveDirectory.resolve("state_history.idx"); this.codec = codec; @@ -33,7 +39,7 @@ public HistoryIndexStore(Path archiveDirectory, HistoryIndexCodec codec) throws if (created) { HistorySegmentStore.syncDirectory(archiveDirectory); } - scanResult = scan(); + scanResult = scan(checkpoint); channel.position(channel.size()); } @@ -79,7 +85,7 @@ public synchronized ScanResult getScanResult() { } public synchronized ScanResult rescan() throws IOException { - scanResult = scan(); + scanResult = scan(null); return scanResult; } @@ -89,25 +95,57 @@ public synchronized void truncateInvalidTail() throws IOException { } channel.truncate(scanResult.getInvalidTailOffset()); channel.force(true); - scanResult = scan(); + scanResult = scan(null); channel.position(channel.size()); } public synchronized void truncateAfter(HistoryIndexLocation last) throws IOException { + truncateAfter(last, -1); + } + + synchronized void truncateAfter(HistoryIndexLocation last, long knownRecordCount) + throws IOException { long length = last == null ? 0 : last.endOffset(); channel.truncate(length); channel.force(true); - scanResult = scan(); + if (last != null && knownRecordCount >= 0) { + HistoryIndexRecord record = read(last); + scanResult = new ScanResult(knownRecordCount, + new ScannedIndexRecord(record, last), null, null); + } else if (last == null && knownRecordCount == 0) { + scanResult = new ScanResult(0, null, null, null); + } else if (scanResult.getHead() == null || last == null + || last.endOffset() != scanResult.getHead().getLocation().endOffset()) { + scanResult = scan(null); + } channel.position(channel.size()); } - private ScanResult scan() throws IOException { + long getStartupScannedRecords() { + return startupScannedRecords; + } + + private ScanResult scan(ArchiveRestartCheckpoint checkpoint) throws IOException { long recordCount = 0; ScannedIndexRecord head = null; Long invalidOffset = null; String invalidReason = null; long offset = 0; BlockSnapshotMeta previous = null; + if (checkpoint != null) { + HistoryCommitMarker marker = checkpoint.getMarker(); + HistoryIndexLocation location = marker.getIndexLocation(); + HistoryIndexRecord record = read(location); + startupScannedRecords++; + if (!marker.getMeta().equals(record.getMeta())) { + throw new ArchivePersistenceException( + "Restart checkpoint does not match the history index"); + } + recordCount = checkpoint.getRecordCount(); + head = new ScannedIndexRecord(record, location); + previous = record.getMeta(); + offset = location.endOffset(); + } while (offset < channel.size()) { long remaining = channel.size() - offset; if (remaining < 12) { @@ -148,6 +186,7 @@ record = codec.decode(encoded); sha256(encoded)); head = new ScannedIndexRecord(record, location); recordCount++; + startupScannedRecords++; previous = record.getMeta(); offset += recordLength; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java index f45dc443252..73cb9c917cd 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java @@ -28,9 +28,15 @@ public final class HistorySegmentStore implements Closeable { private FileChannel appendChannel; private int appendSegmentId; private ScanResult scanResult; + private long startupScannedRecords; public HistorySegmentStore(Path archiveDirectory, BlockHistoryCodec codec, long maxSegmentSize) throws IOException { + this(archiveDirectory, codec, maxSegmentSize, null); + } + + HistorySegmentStore(Path archiveDirectory, BlockHistoryCodec codec, long maxSegmentSize, + ArchiveRestartCheckpoint checkpoint) throws IOException { if (maxSegmentSize <= 0) { throw new IllegalArgumentException("maxSegmentSize must be positive"); } @@ -38,7 +44,7 @@ public HistorySegmentStore(Path archiveDirectory, BlockHistoryCodec codec, long this.codec = codec; this.maxSegmentSize = maxSegmentSize; Files.createDirectories(directory); - scanResult = scan(); + scanResult = scan(checkpoint); openAppendChannel(); } @@ -94,7 +100,7 @@ public synchronized ScanResult getScanResult() { } public synchronized ScanResult rescan() throws IOException { - scanResult = scan(); + scanResult = scan(null); return scanResult; } @@ -106,12 +112,17 @@ public synchronized void truncateInvalidTail() throws IOException { } closeAppendChannel(); truncateFrom(tail.getSegmentId(), tail.getOffset()); - scanResult = scan(); + scanResult = scan(null); openAppendChannel(); } /** Truncates all records after {@code last}; null means remove every body record. */ public synchronized void truncateAfter(HistoryLocation last) throws IOException { + truncateAfter(last, -1); + } + + synchronized void truncateAfter(HistoryLocation last, long knownRecordCount) + throws IOException { closeAppendChannel(); if (last == null) { for (Path segment : listSegments()) { @@ -121,26 +132,58 @@ public synchronized void truncateAfter(HistoryLocation last) throws IOException truncateFrom(last.getSegmentId(), last.endOffset()); } syncDirectory(directory); - scanResult = scan(); + if (last != null && knownRecordCount >= 0) { + BlockReverseDiff diff = read(last); + scanResult = new ScanResult(knownRecordCount, new ScannedRecord(diff, last), null); + } else if (last == null && knownRecordCount == 0) { + scanResult = new ScanResult(0, null, null); + } else if (scanResult.getHead() == null || last == null + || last.endOffset() != scanResult.getHead().getLocation().endOffset() + || last.getSegmentId() != scanResult.getHead().getLocation().getSegmentId()) { + scanResult = scan(null); + } openAppendChannel(); } - private ScanResult scan() throws IOException { + long getStartupScannedRecords() { + return startupScannedRecords; + } + + private ScanResult scan(ArchiveRestartCheckpoint checkpoint) throws IOException { long recordCount = 0; ScannedRecord head = null; InvalidTail invalidTail = null; BlockSnapshotMeta previous = null; List segments = listSegments(); - int expectedSegmentId = segments.isEmpty() ? 0 : parseSegmentId(segments.get(0)); + int startSegmentId = segments.isEmpty() ? 0 : parseSegmentId(segments.get(0)); + long startOffset = 0; + if (checkpoint != null) { + HistoryCommitMarker marker = checkpoint.getMarker(); + BlockReverseDiff diff = read(marker.getHistoryLocation()); + startupScannedRecords++; + if (!marker.getMeta().equals(diff.getMeta())) { + throw new ArchivePersistenceException( + "Restart checkpoint does not match the history body"); + } + recordCount = checkpoint.getRecordCount(); + head = new ScannedRecord(diff, marker.getHistoryLocation()); + previous = diff.getMeta(); + startSegmentId = marker.getHistoryLocation().getSegmentId(); + startOffset = marker.getHistoryLocation().endOffset(); + } + int expectedSegmentId = startSegmentId; for (Path segment : segments) { int segmentId = parseSegmentId(segment); + if (segmentId < startSegmentId) { + continue; + } if (segmentId != expectedSegmentId) { return new ScanResult(recordCount, head, new InvalidTail(segmentId, 0, "non-contiguous segment id")); } expectedSegmentId++; try (FileChannel channel = FileChannel.open(segment, StandardOpenOption.READ)) { - long offset = 0; + long offset = segmentId == startSegmentId ? startOffset : 0; while (offset < channel.size()) { long remaining = channel.size() - offset; if (remaining < BlockHistoryCodec.HEADER_LENGTH) { @@ -176,6 +219,7 @@ private ScanResult scan() throws IOException { HistoryLocation location = location(segmentId, offset, record); head = new ScannedRecord(diff, location); recordCount++; + startupScannedRecords++; previous = diff.getMeta(); offset += recordLength; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java new file mode 100644 index 00000000000..d5a3362f0c5 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java @@ -0,0 +1,245 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.HistoryIndexRecord.KeyGroup; + +/** Request-owned commit/index/segment handles pinned to one persistent serving generation. */ +public final class PersistentCommittedHistoryReader + implements ArchiveReadSnapshot.PinnedHistory { + + private final long indexedFrom; + private final long indexedThrough; + private final byte[] headHash; + private final byte[] sourceDigest; + private final HistorySegmentStore bodies; + private final HistoryIndexStore index; + private final HistoryCommitStore commits; + private boolean closed; + + private PersistentCommittedHistoryReader(Path archiveDirectory, long maxSegmentSize, + PersistentServingKeyIndexGeneration serving) throws IOException { + Objects.requireNonNull(serving, "serving"); + ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archiveDirectory, + new HistoryCommitMarkerCodec()); + if (checkpoint == null) { + throw new ArchivePersistenceException("Archive restart checkpoint is missing"); + } + HistorySegmentStore openedBodies = null; + HistoryIndexStore openedIndex = null; + HistoryCommitStore openedCommits = null; + try { + openedBodies = new HistorySegmentStore(archiveDirectory, new BlockHistoryCodec(), + maxSegmentSize, checkpoint); + openedIndex = new HistoryIndexStore(archiveDirectory, new HistoryIndexCodec(), checkpoint); + openedCommits = new HistoryCommitStore(archiveDirectory, new HistoryCommitMarkerCodec(), + checkpoint); + validatePinnedAuthority(serving, openedBodies, openedIndex, openedCommits); + } catch (IOException | RuntimeException failure) { + closeAfterFailedConstruction(openedBodies, openedIndex, openedCommits, failure); + throw failure; + } + this.bodies = openedBodies; + this.index = openedIndex; + this.commits = openedCommits; + this.indexedFrom = serving.getIndexedFrom(); + this.indexedThrough = serving.getIndexedThrough(); + this.headHash = serving.getHeadHash(); + this.sourceDigest = serving.getAuthoritativePrefixDigest(); + } + + public static PersistentCommittedHistoryReader open(Path archiveDirectory, + long maxSegmentSize, PersistentServingKeyIndexGeneration serving) throws IOException { + return new PersistentCommittedHistoryReader(archiveDirectory, maxSegmentSize, serving); + } + + @Override + public synchronized OldValue read(String dbName, byte[] rawKey, long firstChangeBlock) + throws IOException { + ensureOpen(); + Objects.requireNonNull(dbName, "dbName"); + Objects.requireNonNull(rawKey, "rawKey"); + if (firstChangeBlock <= indexedFrom || firstChangeBlock > indexedThrough) { + throw new ArchivePersistenceException( + "Serving index references history outside its pinned generation"); + } + HistoryCommitMarker marker = commits.get(firstChangeBlock); + if (marker == null) { + throw new ArchivePersistenceException("Serving index references an uncommitted epoch"); + } + HistoryIndexRecord indexRecord = index.read(marker.getIndexLocation()); + validateMarker(marker, indexRecord); + BlockReverseDiff body = bodies.read(marker.getHistoryLocation()); + if (!marker.getMeta().equals(body.getMeta()) || !sameKeys(indexRecord, body)) { + throw new ArchivePersistenceException("Authoritative history index/body key mismatch"); + } + if (!contains(indexRecord, dbName, rawKey)) { + throw new ArchivePersistenceException( + "Serving index key is absent from authoritative history index"); + } + for (DbGroup group : body.getGroups()) { + if (dbName.equals(group.getDbName())) { + for (Entry entry : group.getEntries()) { + if (Arrays.equals(rawKey, entry.getKey())) { + return entry.getOldValue(); + } + } + } + } + throw new ArchivePersistenceException("Authoritative history body is missing the indexed key"); + } + + @Override + public long getIndexedFrom() { + return indexedFrom; + } + + @Override + public long getIndexedThrough() { + return indexedThrough; + } + + @Override + public byte[] getHeadHash() { + return Arrays.copyOf(headHash, headHash.length); + } + + @Override + public byte[] getAuthoritativePrefixDigest() { + return Arrays.copyOf(sourceDigest, sourceDigest.length); + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + IOException failure = null; + try { + index.close(); + } catch (IOException closeFailure) { + failure = closeFailure; + } + try { + bodies.close(); + } catch (IOException closeFailure) { + failure = add(failure, closeFailure); + } + try { + commits.close(); + } catch (IOException closeFailure) { + failure = add(failure, closeFailure); + } + if (failure != null) { + throw failure; + } + } + + private static void validatePinnedAuthority(PersistentServingKeyIndexGeneration serving, + HistorySegmentStore bodies, HistoryIndexStore index, HistoryCommitStore commits) + throws IOException { + HistoryCommitMarker marker = commits.get(serving.getIndexedThrough()); + if (marker == null || !Arrays.equals(marker.getMeta().getBlockHash(), serving.getHeadHash()) + || !marker.getDatabases().equals(serving.getParticipatingDatabases())) { + throw new ArchivePersistenceException( + "Serving generation does not match committed history authority"); + } + HistoryIndexRecord indexRecord = index.read(marker.getIndexLocation()); + validateMarker(marker, indexRecord); + BlockReverseDiff body = bodies.read(marker.getHistoryLocation()); + if (!marker.getMeta().equals(body.getMeta()) || !sameKeys(indexRecord, body)) { + throw new ArchivePersistenceException( + "Serving generation head does not match authoritative history files"); + } + } + + private static void validateMarker(HistoryCommitMarker marker, HistoryIndexRecord record) { + if (!marker.getMeta().equals(record.getMeta()) + || !same(marker.getHistoryLocation(), record.getHistoryLocation())) { + throw new ArchivePersistenceException( + "Commit marker does not match authoritative history index"); + } + } + + private static boolean contains(HistoryIndexRecord record, String dbName, byte[] rawKey) { + for (KeyGroup group : record.getGroups()) { + if (dbName.equals(group.getDbName())) { + for (byte[] key : group.getKeys()) { + if (Arrays.equals(key, rawKey)) { + return true; + } + } + } + } + return false; + } + + private static boolean sameKeys(HistoryIndexRecord record, BlockReverseDiff body) { + List indexed = record.getGroups(); + List stored = body.getGroups(); + if (indexed.size() != stored.size()) { + return false; + } + for (int groupIndex = 0; groupIndex < indexed.size(); groupIndex++) { + KeyGroup indexedGroup = indexed.get(groupIndex); + DbGroup storedGroup = stored.get(groupIndex); + if (!indexedGroup.getDbName().equals(storedGroup.getDbName()) + || indexedGroup.getKeys().size() != storedGroup.getEntries().size()) { + return false; + } + for (int keyIndex = 0; keyIndex < indexedGroup.getKeys().size(); keyIndex++) { + if (!Arrays.equals(indexedGroup.getKeys().get(keyIndex), + storedGroup.getEntries().get(keyIndex).getKey())) { + return false; + } + } + } + return true; + } + + private static boolean same(HistoryLocation left, HistoryLocation right) { + return left.getSegmentId() == right.getSegmentId() + && left.getOffset() == right.getOffset() + && left.getRecordLength() == right.getRecordLength() + && left.getBodyChecksum() == right.getBodyChecksum() + && Arrays.equals(left.getBodyDigest(), right.getBodyDigest()); + } + + private static void closeAfterFailedConstruction(HistorySegmentStore bodies, + HistoryIndexStore index, HistoryCommitStore commits, Exception failure) { + close(index, failure); + close(bodies, failure); + close(commits, failure); + } + + private static void close(java.io.Closeable resource, Exception failure) { + if (resource == null) { + return; + } + try { + resource.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + + private static IOException add(IOException current, IOException addition) { + if (current == null) { + return addition; + } + current.addSuppressed(addition); + return current; + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Persistent committed history reader is closed"); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java new file mode 100644 index 00000000000..1162474196a --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java @@ -0,0 +1,344 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +/** Durable generation catalog with request refcounts and safe retired-generation reaping. */ +public final class PersistentServingKeyIndexCatalog implements Closeable { + + private static final int MAGIC = 0x534b4943; // SKIC + private static final short VERSION = 1; + private static final String CURRENT = "current"; + private static final String CURRENT_TEMP = "current.tmp"; + private static final String GENERATIONS = "generations"; + + private final Path root; + private final Path generations; + private final Map references = new HashMap<>(); + private final Set retired = new HashSet<>(); + private final FaultHook faultHook; + private String currentId; + private boolean closed; + + private PersistentServingKeyIndexCatalog(Path root, String currentId, FaultHook faultHook) { + this.root = root; + this.generations = root.resolve(GENERATIONS); + this.currentId = currentId; + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + public static PersistentServingKeyIndexCatalog open(Path root) throws IOException { + return open(root, stage -> { }); + } + + static PersistentServingKeyIndexCatalog open(Path root, FaultHook faultHook) + throws IOException { + Objects.requireNonNull(root, "root"); + String currentId = readCurrent(root); + Path current = root.resolve(GENERATIONS).resolve(currentId); + try (PersistentServingKeyIndexGeneration ignored = + PersistentServingKeyIndexGeneration.open(current)) { + // Opening validates both the immutable descriptor and RocksDB generation. + } + PersistentServingKeyIndexCatalog catalog = + new PersistentServingKeyIndexCatalog(root, currentId, faultHook); + catalog.discoverRetired(); + return catalog; + } + + public static PersistentServingKeyIndexCatalog create(Path root, Path initialShadow, + ArchiveProgressEnvelope readerVisible) throws IOException { + Objects.requireNonNull(root, "root"); + if (Files.exists(root.resolve(CURRENT))) { + throw new IllegalArgumentException("Serving index catalog already exists"); + } + Files.createDirectories(root.resolve(GENERATIONS)); + PersistentServingKeyIndexCatalog catalog = + new PersistentServingKeyIndexCatalog(root, null, stage -> { }); + if (!catalog.publish(null, initialShadow, readerVisible)) { + throw new IllegalStateException("Failed to publish initial serving generation"); + } + return catalog; + } + + /** Pins one immutable RocksDB handle and holds its generation refcount until close. */ + public synchronized PersistentServingKeyIndexGeneration pin( + ArchiveProgressEnvelope readerVisible) throws IOException { + ensureOpen(); + if (currentId == null) { + throw new ArchivePersistenceException("Serving index catalog has no current generation"); + } + String pinnedId = currentId; + references.put(pinnedId, references.getOrDefault(pinnedId, 0) + 1); + PersistentServingKeyIndexGeneration pinned; + try { + pinned = PersistentServingKeyIndexGeneration.open(generations.resolve(pinnedId), + () -> release(pinnedId)); + } catch (IOException | RuntimeException failure) { + release(pinnedId); + throw failure; + } + try { + validateReaderVisibility(pinned, readerVisible); + return pinned; + } catch (RuntimeException failure) { + pinned.close(); + throw failure; + } + } + + /** Atomically publishes a completed shadow generation if {@code expectedId} is still current. */ + public synchronized boolean publish(String expectedId, Path shadow, + ArchiveProgressEnvelope readerVisible) throws IOException { + ensureOpen(); + if (!Objects.equals(expectedId, currentId)) { + return false; + } + String replacementId; + long replacementFrom; + long replacementThrough; + try (PersistentServingKeyIndexGeneration replacement = + PersistentServingKeyIndexGeneration.open(shadow)) { + validateReaderVisibility(replacement, readerVisible); + replacementId = replacement.getGenerationId(); + replacementFrom = replacement.getIndexedFrom(); + replacementThrough = replacement.getIndexedThrough(); + } + validateGenerationId(replacementId); + if (currentId != null) { + try (PersistentServingKeyIndexGeneration current = + PersistentServingKeyIndexGeneration.open(generations.resolve(currentId))) { + if (replacementFrom != current.getIndexedFrom() + || replacementThrough < current.getIndexedThrough()) { + throw new IllegalArgumentException("Serving generation publication regresses coverage"); + } + } + } + Path destination = generations.resolve(replacementId); + if (Files.exists(destination)) { + throw new IllegalArgumentException("Serving generation already exists: " + replacementId); + } + try { + Files.move(shadow, destination, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new ArchivePersistenceException( + "Serving index filesystem does not support atomic generation install", unsupported); + } + HistorySegmentStore.syncDirectory(generations); + faultHook.afterDurableStage(PublicationStage.GENERATION_INSTALLED); + persistCurrent(replacementId); + faultHook.afterDurableStage(PublicationStage.CURRENT_PUBLISHED); + String previous = currentId; + currentId = replacementId; + if (previous != null) { + retired.add(previous); + reapIfUnused(previous); + } + return true; + } + + public synchronized String getCurrentGenerationId() { + ensureOpen(); + return currentId; + } + + public synchronized int getReferenceCount(String generationId) { + return references.getOrDefault(generationId, 0); + } + + public synchronized boolean generationExists(String generationId) { + return Files.isDirectory(generations.resolve(generationId)); + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + if (!references.isEmpty()) { + throw new IllegalStateException("Serving index catalog closed with pinned generations"); + } + closed = true; + for (String generation : new ArrayList<>(retired)) { + reapIfUnused(generation); + } + } + + private synchronized void release(String generationId) { + Integer count = references.get(generationId); + if (count == null || count <= 0) { + throw new IllegalStateException("Serving generation refcount underflow"); + } + if (count == 1) { + references.remove(generationId); + if (retired.contains(generationId)) { + try { + reapIfUnused(generationId); + } catch (IOException failure) { + throw new ArchivePersistenceException("Failed to reap serving generation", failure); + } + } + } else { + references.put(generationId, count - 1); + } + } + + private void discoverRetired() throws IOException { + Files.createDirectories(generations); + try (Stream entries = Files.list(generations)) { + entries.filter(Files::isDirectory) + .map(path -> path.getFileName().toString()) + .filter(id -> !id.equals(currentId)) + .forEach(retired::add); + } + for (String generation : new ArrayList<>(retired)) { + reapIfUnused(generation); + } + } + + private void reapIfUnused(String generationId) throws IOException { + if (generationId.equals(currentId) || references.getOrDefault(generationId, 0) != 0) { + return; + } + Path target = generations.resolve(generationId); + if (Files.exists(target)) { + List paths = new ArrayList<>(); + try (Stream walk = Files.walk(target)) { + walk.sorted(Comparator.reverseOrder()).forEach(paths::add); + } + for (Path path : paths) { + Files.deleteIfExists(path); + } + HistorySegmentStore.syncDirectory(generations); + } + retired.remove(generationId); + } + + private void persistCurrent(String generationId) throws IOException { + byte[] encoded = encodeCurrent(generationId); + Path temporary = root.resolve(CURRENT_TEMP); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(encoded); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + try { + Files.move(temporary, root.resolve(CURRENT), StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new ArchivePersistenceException( + "Serving index filesystem does not support atomic catalog publication", unsupported); + } + HistorySegmentStore.syncDirectory(root); + } + + private static String readCurrent(Path root) throws IOException { + Path current = root.resolve(CURRENT); + if (!Files.isRegularFile(current)) { + throw new ArchivePersistenceException("Serving index current-generation pointer is missing"); + } + byte[] encoded = Files.readAllBytes(current); + if (encoded.length < 16) { + throw new ArchivePersistenceException("Serving index current pointer is corrupt"); + } + byte[] payload = java.util.Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(encoded, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new ArchivePersistenceException("Serving index current pointer checksum mismatch"); + } + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0) { + throw new ArchivePersistenceException("Unsupported serving index current pointer"); + } + String generationId = input.readUTF(); + if (input.available() != Integer.BYTES) { + throw new ArchivePersistenceException("Serving index current pointer payload mismatch"); + } + validateGenerationId(generationId); + return generationId; + } catch (IOException invalid) { + throw new ArchivePersistenceException("Serving index current pointer is truncated", invalid); + } + } + + private static byte[] encodeCurrent(String generationId) { + validateGenerationId(generationId); + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeUTF(generationId); + output.flush(); + byte[] payload = bytes.toByteArray(); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected serving catalog encoding failure", impossible); + } + } + + private static void validateGenerationId(String generationId) { + if (generationId == null || generationId.isEmpty() || generationId.length() > 128 + || generationId.contains("/") || generationId.contains("\\") + || generationId.equals(".") || generationId.equals("..")) { + throw new IllegalArgumentException("Invalid serving generation id"); + } + } + + private static void validateReaderVisibility(PersistentServingKeyIndexGeneration generation, + ArchiveProgressEnvelope readerVisible) { + Objects.requireNonNull(readerVisible, "readerVisible"); + if (readerVisible.getKind() != ArchiveProgressEnvelope.Kind.READER_VISIBLE + || !readerVisible.getParticipants().equals(generation.getParticipatingDatabases()) + || generation.getIndexedThrough() > readerVisible.getEpoch() + || generation.getIndexedThrough() == readerVisible.getEpoch() + && !java.util.Arrays.equals(generation.getHeadHash(), readerVisible.getBlockHash())) { + throw new ArchivePersistenceException( + "Serving generation is outside reader-visible recovery authority"); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Serving index catalog is closed"); + } + } + + enum PublicationStage { + GENERATION_INSTALLED, + CURRENT_PUBLISHED + } + + @FunctionalInterface + interface FaultHook { + void afterDurableStage(PublicationStage stage) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java new file mode 100644 index 00000000000..25e9409a0fa --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java @@ -0,0 +1,511 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.OptionalLong; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; + +/** Persistent immutable exact-key serving generation backed by RocksDB. */ +public final class PersistentServingKeyIndexGeneration implements ServingKeyIndex { + + private static final int MAGIC = 0x534b4947; // SKIG + private static final short VERSION = 2; + private static final short LEGACY_VERSION = 1; + private static final int MAX_MANIFEST_SIZE = 1024 * 1024; + private static final byte DATA_PREFIX = 1; + private static final byte[] PRESENT = new byte[]{1}; + private static final String MANIFEST = "generation.meta"; + private static final String MANIFEST_TEMP = "generation.meta.tmp"; + private static final String DATABASE = "keys"; + + static { + RocksDB.loadLibrary(); + } + + private final Path directory; + private final Descriptor descriptor; + private final Options options; + private final RocksDB database; + private final Runnable release; + private boolean closed; + + private PersistentServingKeyIndexGeneration(Path directory, Descriptor descriptor, + Runnable release) throws IOException { + this.directory = directory; + this.descriptor = descriptor; + this.release = Objects.requireNonNull(release, "release"); + this.options = new Options().setCreateIfMissing(false); + try { + this.database = RocksDB.openReadOnly(options, directory.resolve(DATABASE).toString()); + } catch (RocksDBException failure) { + options.close(); + throw new IOException("Failed to open serving index generation", failure); + } + } + + public static PersistentServingKeyIndexGeneration build(Path directory, String generationId, + long baseEpoch, byte[] baseHash, Iterable committed, + ServingKeyIndexGeneration.AuthoritativeIndexReader reader, + List participatingDatabases) throws IOException { + return build(directory, generationId, baseEpoch, baseHash, committed, reader, + participatingDatabases, new byte[32]); + } + + public static PersistentServingKeyIndexGeneration build(Path directory, String generationId, + long baseEpoch, byte[] baseHash, Iterable committed, + ServingKeyIndexGeneration.AuthoritativeIndexReader reader, + List participatingDatabases, byte[] latestSourceIdentityDigest) throws IOException { + Objects.requireNonNull(directory, "directory"); + Objects.requireNonNull(committed, "committed"); + Objects.requireNonNull(reader, "reader"); + List participants = sortedParticipants(participatingDatabases); + if (generationId == null || generationId.isEmpty() || baseEpoch < 0) { + throw new IllegalArgumentException("Invalid serving generation identity"); + } + requireHash(baseHash, "baseHash"); + requireHash(latestSourceIdentityDigest, "latestSourceIdentityDigest"); + if (Files.exists(directory)) { + throw new IllegalArgumentException("Serving generation directory already exists"); + } + Files.createDirectories(directory); + + MessageDigest sourceDigest = sha256(); + updateLong(sourceDigest, baseEpoch); + sourceDigest.update(baseHash); + updateParticipantDigest(sourceDigest, participants); + long previousEpoch = baseEpoch; + long previousBlock = baseEpoch; + byte[] previousHash = Arrays.copyOf(baseHash, baseHash.length); + long keyChanges = 0; + Options buildOptions = new Options().setCreateIfMissing(true); + WriteOptions writes = new WriteOptions().setSync(false); + try (RocksDB target = RocksDB.open(buildOptions, directory.resolve(DATABASE).toString())) { + for (HistoryCommitMarker marker : committed) { + BlockSnapshotMeta meta = marker.getMeta(); + validateNext(marker, previousEpoch, previousBlock, previousHash, participants); + HistoryIndexRecord record = reader.read(marker.getIndexLocation()); + validateMarker(marker, record, participants); + try (WriteBatch batch = new WriteBatch()) { + for (HistoryIndexRecord.KeyGroup group : record.getGroups()) { + for (byte[] key : group.getKeys()) { + batch.put(dataKey(group.getDbName(), key, meta.getEpoch()), PRESENT); + keyChanges++; + } + } + target.write(writes, batch); + } + updateSourceDigest(sourceDigest, marker); + previousEpoch = meta.getEpoch(); + previousBlock = meta.getBlockNumber(); + previousHash = meta.getBlockHash(); + } + try (WriteOptions sync = new WriteOptions().setSync(true)) { + target.put(sync, new byte[]{0}, new byte[]{1}); + } + } catch (RocksDBException failure) { + throw new IOException("Failed to build serving index generation", failure); + } finally { + writes.close(); + buildOptions.close(); + } + + Descriptor descriptor = new Descriptor(generationId, baseEpoch, previousEpoch, previousHash, + sourceDigest.digest(), latestSourceIdentityDigest, participants, keyChanges); + persistDescriptor(directory, descriptor); + HistorySegmentStore.syncDirectory(directory); + return open(directory); + } + + public static PersistentServingKeyIndexGeneration open(Path directory) throws IOException { + return open(directory, () -> { }); + } + + static PersistentServingKeyIndexGeneration open(Path directory, Runnable release) + throws IOException { + return new PersistentServingKeyIndexGeneration(directory, loadDescriptor(directory), release); + } + + @Override + public synchronized OptionalLong firstChangeAfter(String dbName, byte[] rawKey, + long targetBlock, long upperBound) throws IOException { + ensureOpen(); + Objects.requireNonNull(dbName, "dbName"); + Objects.requireNonNull(rawKey, "rawKey"); + validateCoverage(dbName, targetBlock, upperBound); + if (targetBlock == Long.MAX_VALUE) { + return OptionalLong.empty(); + } + byte[] prefix = dataPrefix(dbName, rawKey); + byte[] seek = ByteBuffer.allocate(prefix.length + Long.BYTES).put(prefix) + .putLong(targetBlock + 1).array(); + try (RocksIterator iterator = database.newIterator()) { + iterator.seek(seek); + if (!iterator.isValid()) { + return OptionalLong.empty(); + } + byte[] found = iterator.key(); + if (found.length != prefix.length + Long.BYTES || !startsWith(found, prefix)) { + return OptionalLong.empty(); + } + long epoch = ByteBuffer.wrap(found, prefix.length, Long.BYTES).getLong(); + return epoch <= upperBound ? OptionalLong.of(epoch) : OptionalLong.empty(); + } + } + + @Override + public List changesInRange(String dbName, + byte[] lowerInclusive, byte[] upperExclusive, long targetBlock, long upperBound, + int maxChangedKeys) { + throw new UnsupportedOperationException( + "Persistent generic range serving is outside the Phase 1 point-query scope"); + } + + @Override + public String getGenerationId() { + return descriptor.generationId; + } + + @Override + public long getIndexedFrom() { + return descriptor.indexedFrom; + } + + @Override + public long getIndexedThrough() { + return descriptor.indexedThrough; + } + + @Override + public byte[] getHeadHash() { + return Arrays.copyOf(descriptor.headHash, descriptor.headHash.length); + } + + @Override + public byte[] getAuthoritativePrefixDigest() { + return Arrays.copyOf(descriptor.sourceDigest, descriptor.sourceDigest.length); + } + + public List getParticipatingDatabases() { + return descriptor.participants; + } + + public long getKeyChangeCount() { + return descriptor.keyChanges; + } + + public byte[] getLatestSourceIdentityDigest() { + return Arrays.copyOf(descriptor.latestSourceIdentityDigest, + descriptor.latestSourceIdentityDigest.length); + } + + public boolean isLatestSourceIdentityBound() { + for (byte value : descriptor.latestSourceIdentityDigest) { + if (value != 0) { + return true; + } + } + return false; + } + + Path getDirectory() { + return directory; + } + + @Override + public synchronized void close() { + if (!closed) { + closed = true; + database.close(); + options.close(); + release.run(); + } + } + + private void validateCoverage(String dbName, long targetBlock, long upperBound) { + if (Collections.binarySearch(descriptor.participants, dbName) < 0) { + throw new IllegalArgumentException("Database is outside serving index coverage: " + dbName); + } + if (targetBlock < descriptor.indexedFrom || targetBlock > upperBound + || upperBound > descriptor.indexedThrough) { + throw new IllegalArgumentException("Query range is outside serving index coverage"); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Serving index generation is closed"); + } + } + + private static byte[] dataKey(String dbName, byte[] rawKey, long epoch) { + if (epoch < 0) { + throw new IllegalArgumentException("Serving index epoch must not be negative"); + } + byte[] prefix = dataPrefix(dbName, rawKey); + return ByteBuffer.allocate(prefix.length + Long.BYTES).put(prefix).putLong(epoch).array(); + } + + private static byte[] dataPrefix(String dbName, byte[] rawKey) { + byte[] database = dbName.getBytes(StandardCharsets.UTF_8); + return ByteBuffer.allocate(1 + Integer.BYTES + database.length + Integer.BYTES + rawKey.length) + .put(DATA_PREFIX).putInt(database.length).put(database).putInt(rawKey.length).put(rawKey) + .array(); + } + + private static boolean startsWith(byte[] value, byte[] prefix) { + if (value.length < prefix.length) { + return false; + } + for (int i = 0; i < prefix.length; i++) { + if (value[i] != prefix[i]) { + return false; + } + } + return true; + } + + private static void validateNext(HistoryCommitMarker marker, long previousEpoch, + long previousBlock, byte[] previousHash, List participants) { + BlockSnapshotMeta meta = marker.getMeta(); + if (marker.getPreviousEpoch() != previousEpoch || meta.getEpoch() != previousEpoch + 1 + || meta.getBlockNumber() != previousBlock + 1 + || !Arrays.equals(meta.getParentHash(), previousHash) + || !participants.equals(marker.getDatabases())) { + throw new IllegalArgumentException("Serving index source commit prefix is inconsistent"); + } + } + + private static void validateMarker(HistoryCommitMarker marker, HistoryIndexRecord record, + List participants) { + if (record == null || !marker.getMeta().equals(record.getMeta()) + || !same(marker.getHistoryLocation(), record.getHistoryLocation())) { + throw new IllegalArgumentException( + "Commit marker does not match authoritative history index record"); + } + for (HistoryIndexRecord.KeyGroup group : record.getGroups()) { + if (Collections.binarySearch(participants, group.getDbName()) < 0) { + throw new IllegalArgumentException("History index contains an unknown database"); + } + } + } + + private static boolean same(HistoryLocation left, HistoryLocation right) { + return left.getSegmentId() == right.getSegmentId() + && left.getOffset() == right.getOffset() + && left.getRecordLength() == right.getRecordLength() + && left.getBodyChecksum() == right.getBodyChecksum() + && Arrays.equals(left.getBodyDigest(), right.getBodyDigest()); + } + + private static List sortedParticipants(List databases) { + List result = new ArrayList<>(Objects.requireNonNull(databases, "databases")); + Collections.sort(result); + if (result.isEmpty()) { + throw new IllegalArgumentException("Serving index participant set must not be empty"); + } + String previous = null; + for (String database : result) { + if (database == null || database.isEmpty() || database.equals(previous)) { + throw new IllegalArgumentException("Serving index participant set is invalid"); + } + previous = database; + } + return Collections.unmodifiableList(result); + } + + private static void updateSourceDigest(MessageDigest digest, HistoryCommitMarker marker) { + updateLong(digest, marker.getMeta().getEpoch()); + updateLong(digest, marker.getMeta().getBlockNumber()); + digest.update(marker.getMeta().getBlockHash()); + digest.update(marker.getMeta().getParentHash()); + updateLong(digest, marker.getIndexLocation().getOffset()); + updateLong(digest, marker.getIndexLocation().getRecordLength()); + digest.update(marker.getIndexLocation().getDigest()); + digest.update(marker.getHistoryLocation().getBodyDigest()); + } + + private static void updateParticipantDigest(MessageDigest digest, List databases) { + updateLong(digest, databases.size()); + for (String database : databases) { + byte[] encoded = database.getBytes(StandardCharsets.UTF_8); + updateLong(digest, encoded.length); + digest.update(encoded); + } + } + + private static void updateLong(MessageDigest digest, long value) { + digest.update(ByteBuffer.allocate(Long.BYTES).putLong(value).array()); + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException impossible) { + throw new IllegalStateException("SHA-256 is unavailable", impossible); + } + } + + private static void requireHash(byte[] hash, String name) { + if (hash == null || hash.length != 32) { + throw new IllegalArgumentException(name + " must be exactly 32 bytes"); + } + } + + private static void persistDescriptor(Path directory, Descriptor descriptor) throws IOException { + byte[] encoded = encodeDescriptor(descriptor); + Path temporary = directory.resolve(MANIFEST_TEMP); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(encoded); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + try { + Files.move(temporary, directory.resolve(MANIFEST), StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new ArchivePersistenceException( + "Serving index filesystem does not support atomic manifests", unsupported); + } + } + + private static Descriptor loadDescriptor(Path directory) throws IOException { + Path manifest = directory.resolve(MANIFEST); + if (!Files.isRegularFile(manifest)) { + throw new ArchivePersistenceException("Serving index generation manifest is missing"); + } + try { + return decodeDescriptor(Files.readAllBytes(manifest)); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Serving index generation manifest is corrupt", + invalid); + } + } + + private static byte[] encodeDescriptor(Descriptor descriptor) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeUTF(descriptor.generationId); + output.writeLong(descriptor.indexedFrom); + output.writeLong(descriptor.indexedThrough); + output.write(descriptor.headHash); + output.write(descriptor.sourceDigest); + output.write(descriptor.latestSourceIdentityDigest); + output.writeLong(descriptor.keyChanges); + output.writeInt(descriptor.participants.size()); + for (String participant : descriptor.participants) { + output.writeUTF(participant); + } + output.flush(); + byte[] payload = bytes.toByteArray(); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected serving manifest encoding failure", impossible); + } + } + + private static Descriptor decodeDescriptor(byte[] encoded) { + if (encoded == null || encoded.length < 96 || encoded.length > MAX_MANIFEST_SIZE) { + throw new IllegalArgumentException("Serving index manifest length is invalid"); + } + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(encoded, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new IllegalArgumentException("Serving index manifest checksum mismatch"); + } + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + if (input.readInt() != MAGIC) { + throw new IllegalArgumentException("Unsupported serving index manifest"); + } + short version = input.readShort(); + if (version != VERSION && version != LEGACY_VERSION || input.readShort() != 0) { + throw new IllegalArgumentException("Unsupported serving index manifest"); + } + String generationId = input.readUTF(); + long from = input.readLong(); + long through = input.readLong(); + byte[] headHash = new byte[32]; + byte[] sourceDigest = new byte[32]; + input.readFully(headHash); + input.readFully(sourceDigest); + byte[] latestSourceIdentityDigest = new byte[32]; + if (version >= VERSION) { + input.readFully(latestSourceIdentityDigest); + } + long keyChanges = input.readLong(); + int count = input.readInt(); + if (generationId.isEmpty() || from < 0 || through < from || keyChanges < 0 + || count <= 0 || count > ArchiveStoreScope.getStateDatabases().size() + 16) { + throw new IllegalArgumentException("Invalid serving index manifest fields"); + } + List participants = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + participants.add(input.readUTF()); + } + if (input.available() != Integer.BYTES) { + throw new IllegalArgumentException("Serving index manifest payload mismatch"); + } + return new Descriptor(generationId, from, through, headHash, sourceDigest, + latestSourceIdentityDigest, sortedParticipants(participants), keyChanges); + } catch (IOException invalid) { + throw new IllegalArgumentException("Serving index manifest is truncated", invalid); + } + } + + private static final class Descriptor { + private final String generationId; + private final long indexedFrom; + private final long indexedThrough; + private final byte[] headHash; + private final byte[] sourceDigest; + private final byte[] latestSourceIdentityDigest; + private final List participants; + private final long keyChanges; + + private Descriptor(String generationId, long indexedFrom, long indexedThrough, + byte[] headHash, byte[] sourceDigest, byte[] latestSourceIdentityDigest, + List participants, long keyChanges) { + this.generationId = generationId; + this.indexedFrom = indexedFrom; + this.indexedThrough = indexedThrough; + this.headHash = Arrays.copyOf(headHash, headHash.length); + this.sourceDigest = Arrays.copyOf(sourceDigest, sourceDigest.length); + this.latestSourceIdentityDigest = Arrays.copyOf(latestSourceIdentityDigest, + latestSourceIdentityDigest.length); + this.participants = participants; + this.keyChanges = keyChanges; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndex.java b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndex.java new file mode 100644 index 00000000000..965ae022ccb --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndex.java @@ -0,0 +1,32 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.OptionalLong; + +/** One immutable serving generation for exact physical-key history lookup. */ +public interface ServingKeyIndex extends Closeable { + + String getGenerationId(); + + long getIndexedFrom(); + + long getIndexedThrough(); + + byte[] getHeadHash(); + + byte[] getAuthoritativePrefixDigest(); + + OptionalLong firstChangeAfter(String dbName, byte[] rawKey, long targetBlock, + long upperBound) throws IOException; + + List changesInRange(String dbName, + byte[] lowerInclusive, byte[] upperExclusive, long targetBlock, long upperBound, + int maxChangedKeys) throws IOException; + + @Override + default void close() throws IOException { + // Pure in-memory generations own no external resource. + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java index d0357504c35..7695434f3db 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java @@ -21,7 +21,7 @@ *

This class deliberately defines no persistent page encoding. A production LSM backend can * preserve this exact-key, committed-prefix and coverage contract after H1 format approval. */ -public final class ServingKeyIndexGeneration { +public final class ServingKeyIndexGeneration implements ServingKeyIndex { private final String generationId; private final long indexedFrom; diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryTruncatorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryTruncatorTest.java new file mode 100644 index 00000000000..1e2d71162da --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryTruncatorTest.java @@ -0,0 +1,143 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveHistoryTruncator.Stage; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +public class ArchiveHistoryTruncatorTest { + + private static final List PARTICIPANTS = Collections.singletonList("account"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void everyStageCrashKeepsCommitAuthoritativeAndSecondRecoveryConverges() + throws Exception { + for (Stage failedStage : Stage.values()) { + Path archive = temporaryFolder.newFolder("truncate-" + failedStage).toPath(); + initialize(archive); + + try (Stores stores = new Stores(archive)) { + ArchiveHistoryTruncator truncator = new ArchiveHistoryTruncator(stores.commits, + stores.index, stores.bodies, stage -> { + if (stage == failedStage) { + throw new IOException("injected after " + stage); + } + }); + assertThrows(IOException.class, () -> truncator.truncateAfter(10)); + } + + try (Stores afterCrash = new Stores(archive)) { + assertEquals(10, commitHead(afterCrash)); + assertNull(afterCrash.commits.get(11)); + assertEquals(failedStage == Stage.COMMIT_AUTHORITY ? 12 : 10, + indexHead(afterCrash)); + assertEquals(failedStage == Stage.HISTORY_BODY ? 10 : 12, + bodyHead(afterCrash)); + new ArchiveHistoryTruncator(afterCrash.commits, afterCrash.index, + afterCrash.bodies).truncateAfter(10); + } + + try (Stores recovered = new Stores(archive)) { + assertEquals(10, commitHead(recovered)); + assertEquals(10, indexHead(recovered)); + assertEquals(10, bodyHead(recovered)); + new ArchiveHistoryTruncator(recovered.commits, recovered.index, + recovered.bodies).truncateAfter(10); + assertEquals(3, recovered.commits.size()); + } + } + } + + @Test + public void rejectsUnknownTargetBeforeShrinkingAnyStore() throws Exception { + Path archive = temporaryFolder.newFolder("unknown-target").toPath(); + initialize(archive); + try (Stores stores = new Stores(archive)) { + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveHistoryTruncator(stores.commits, stores.index, + stores.bodies).truncateAfter(7)); + assertEquals(12, commitHead(stores)); + assertEquals(12, indexHead(stores)); + assertEquals(12, bodyHead(stores)); + } + } + + private static void initialize(Path archive) throws Exception { + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096); + HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); + HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + List markers = new ArrayList<>(); + for (long epoch = 8; epoch <= 12; epoch++) { + BlockReverseDiff diff = diff(epoch); + HistoryLocation body = bodies.append(diff); + HistoryIndexLocation indexLocation = index.append(HistoryIndexRecord.from(diff, body)); + markers.add(new HistoryCommitMarker(diff.getMeta(), epoch - 1, body, indexLocation, + bytes(16, (int) epoch + 40), PARTICIPANTS)); + } + bodies.sync(); + index.sync(); + commits.commitAll(markers); + } + } + + private static long commitHead(Stores stores) { + return stores.commits.head().getMeta().getEpoch(); + } + + private static long indexHead(Stores stores) { + return stores.index.getScanResult().getHead().getRecord().getMeta().getEpoch(); + } + + private static long bodyHead(Stores stores) { + return stores.bodies.getScanResult().getHead().getDiff().getMeta().getEpoch(); + } + + private static BlockReverseDiff diff(long epoch) { + return new BlockReverseDiff(new BlockSnapshotMeta(epoch, epoch, + bytes(32, (int) epoch), bytes(32, (int) epoch - 1), epoch * 1_000), + Collections.singletonList(new DbGroup("account", Collections.singletonList( + new Entry(bytes(8, (int) epoch), OldValue.present(bytes(12, (int) epoch))))))); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } + + private static final class Stores implements AutoCloseable { + private final HistorySegmentStore bodies; + private final HistoryIndexStore index; + private final HistoryCommitStore commits; + + private Stores(Path archive) throws IOException { + bodies = new HistorySegmentStore(archive, new BlockHistoryCodec(), 4096); + index = new HistoryIndexStore(archive, new HistoryIndexCodec()); + commits = new HistoryCommitStore(archive, new HistoryCommitMarkerCodec()); + } + + @Override + public void close() throws IOException { + commits.close(); + index.close(); + bodies.close(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java index bd93a002dae..b7bcde794ad 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java @@ -177,12 +177,144 @@ public void persistsBatchedPrefixWithoutPerBlockFilesAndResumes() throws Excepti } try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter(archive, 4096, databases())) { assertEquals(1_000, reopened.committedHead().getMeta().getEpoch()); + assertEquals(3, reopened.getStartupScannedRecords()); assertEquals(diff(500).getMeta(), reopened.readCommitted(500).getMeta()); reopened.accept(diff(1_001)); assertEquals(1_001, reopened.committedHead().getMeta().getEpoch()); } } + @Test + public void scansOnlyTailAfterAStaleRestartCheckpoint() throws Exception { + Path archive = temporaryFolder.newFolder("stale-checkpoint").toPath(); + byte[] checkpointAtOne; + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + writer.accept(diff(1)); + checkpointAtOne = Files.readAllBytes(archive.resolve("restart.checkpoint")); + writer.accept(diff(2)); + } + Files.write(archive.resolve("restart.checkpoint"), checkpointAtOne); + + try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter(archive, 4096, databases())) { + assertEquals(2, reopened.committedHead().getMeta().getEpoch()); + assertEquals(6, reopened.getStartupScannedRecords()); + assertEquals(diff(2).getMeta(), reopened.readCommitted(2).getMeta()); + } + } + + @Test + public void truncatesInvalidBodyAndIndexTailWithoutRescanningPrefix() throws Exception { + Path archive = temporaryFolder.newFolder("invalid-data-tail").toPath(); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + List batch = new ArrayList<>(1_000); + for (int number = 1; number <= 1_000; number++) { + batch.add(diff(number)); + } + writer.acceptAll(batch); + } + Path lastSegment; + try (java.util.stream.Stream segments = Files.list(archive.resolve("history"))) { + lastSegment = segments.sorted().reduce((left, right) -> right) + .orElseThrow(AssertionError::new); + } + Files.write(lastSegment, new byte[]{1, 2, 3}, + java.nio.file.StandardOpenOption.APPEND); + Files.write(archive.resolve("state_history.idx"), new byte[]{1, 2, 3}, + java.nio.file.StandardOpenOption.APPEND); + + try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter(archive, 4096, databases())) { + assertEquals(1_000, reopened.committedHead().getMeta().getEpoch()); + assertEquals(3, reopened.getStartupScannedRecords()); + reopened.accept(diff(1_001)); + assertEquals(1_001, reopened.committedHead().getMeta().getEpoch()); + } + } + + @Test + public void boundsPreparedTailAcrossAnOversizedFlushFailure() throws Exception { + Path archive = temporaryFolder.newFolder("bounded-large-flush").toPath(); + List batch = new ArrayList<>(1_500); + for (int number = 1; number <= 1_500; number++) { + batch.add(diff(number)); + } + ArchiveHistoryWriter.DurabilityHook failSecondChunk = (stage, meta) -> { + if (stage == Stage.APPEND_BODY && meta.getEpoch() == 1_025) { + throw new java.io.IOException("injected second chunk failure"); + } + }; + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases(), + failSecondChunk)) { + assertThrows(ArchivePersistenceException.class, () -> writer.acceptAll(batch)); + assertEquals(1_024, writer.committedHead().getMeta().getEpoch()); + } + + try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter(archive, 4096, databases())) { + assertEquals(1_024, reopened.committedHead().getMeta().getEpoch()); + assertEquals(3, reopened.getStartupScannedRecords()); + } + } + + @Test + public void failsClosedOnCorruptRestartCheckpoint() throws Exception { + Path archive = temporaryFolder.newFolder("corrupt-checkpoint").toPath(); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + writer.accept(diff(1)); + } + Path checkpoint = archive.resolve("restart.checkpoint"); + byte[] encoded = Files.readAllBytes(checkpoint); + encoded[encoded.length - 1] ^= 1; + Files.write(checkpoint, encoded); + + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveHistoryWriter(archive, 4096, databases())); + } + + @Test + public void completesPreparedTruncationBeforeLoadingRestartCheckpoint() throws Exception { + Path archive = temporaryFolder.newFolder("writer-truncation-recovery").toPath(); + initializeHistory(archive, 3); + prepareTruncation(archive, 2); + + try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter( + archive, 4096, databases())) { + assertEquals(2, reopened.committedHead().getMeta().getEpoch()); + assertEquals(diff(2).getMeta(), reopened.readCommitted(2).getMeta()); + assertFalse(Files.exists(archive.resolve("truncation.intent"))); + assertEquals(3, reopened.getStartupScannedRecords()); + } + } + + @Test + public void failsClosedWhenDerivedAccountIndexIsAheadAfterRecovery() throws Exception { + Path archive = temporaryFolder.newFolder("writer-index-ahead").toPath(); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + writer.acceptAll(Arrays.asList(diff(1), diff(2), diff(3))); + } + prepareTruncation(archive, 2); + + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveHistoryWriter(archive, 4096, databases())); + ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + new HistoryCommitMarkerCodec()); + assertEquals(2, checkpoint.getMarker().getMeta().getEpoch()); + assertFalse(Files.exists(archive.resolve("truncation.intent"))); + } + + @Test + public void buildsPersistentServingGenerationFromCommittedWriterPrefix() throws Exception { + Path archive = temporaryFolder.newFolder("writer-serving-generation").toPath(); + byte[] key = bytes("key-1"); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + writer.acceptAll(Arrays.asList(diff(1), diff(2))); + try (PersistentServingKeyIndexGeneration generation = writer.buildServingGeneration( + archive.resolve("serving-shadow"), "generation-2")) { + assertEquals(2, generation.getIndexedThrough()); + assertEquals(1, generation.firstChangeAfter("account", key, 0, 2).getAsLong()); + assertFalse(generation.firstChangeAfter("properties", key, 0, 2).isPresent()); + } + } + } + @Test public void commitLogForceBoundaryFailurePreservesRecordAsUncertain() throws Exception { Path archive = temporaryFolder.newFolder("uncertain-marker").toPath(); @@ -208,6 +340,42 @@ private static Set databases() { return new java.util.LinkedHashSet<>(Arrays.asList("account", "properties")); } + private static void initializeHistory(Path archive, int lastEpoch) throws Exception { + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096); + HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); + HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + List markers = new ArrayList<>(); + for (int epoch = 1; epoch <= lastEpoch; epoch++) { + BlockReverseDiff diff = diff(epoch); + HistoryLocation body = bodies.append(diff); + HistoryIndexLocation indexLocation = index.append(HistoryIndexRecord.from(diff, body)); + markers.add(new HistoryCommitMarker(diff.getMeta(), epoch - 1L, body, indexLocation, + new byte[16], new ArrayList<>(databases()))); + } + bodies.sync(); + index.sync(); + commits.commitAll(markers); + ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), + commits.getRecordLength(), commits.head(), new HistoryCommitMarkerCodec()); + } + } + + private static void prepareTruncation(Path archive, long targetEpoch) throws Exception { + ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + new HistoryCommitMarkerCodec()); + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096, checkpoint); + HistoryIndexStore index = new HistoryIndexStore( + archive, new HistoryIndexCodec(), checkpoint); + HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec(), checkpoint)) { + ArchiveTruncationIntent.prepare(archive, commits, index, bodies, targetEpoch, + new HistoryCommitMarkerCodec()); + } + } + private static BlockReverseDiff diff(int number) { return new BlockReverseDiff(new BlockSnapshotMeta(number, number, hash(number), hash(number - 1), number * 3_000L), Collections.singletonList( diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantBatchFileTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantBatchFileTest.java new file mode 100644 index 00000000000..00e5652e3ba --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantBatchFileTest.java @@ -0,0 +1,225 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveParticipantBatchFile.Snapshot; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoveryStorage; + +public class ArchiveParticipantBatchFileTest { + + private static final List PARTICIPANTS = Arrays.asList("account", "account-asset"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void crashKeepsBusinessPayloadAndProgressOnTheSameOldVersion() throws Exception { + Path path = temporaryFolder.newFolder().toPath().resolve("account-asset.batch"); + ArchiveParticipantBatchFile normal = new ArchiveParticipantBatchFile(path, + "account-asset", PARTICIPANTS); + normal.store(bytes(12, 8), envelope("account-asset", marker(8))); + + ArchiveParticipantBatchFile failing = new ArchiveParticipantBatchFile(path, + "account-asset", PARTICIPANTS, temporary -> { + throw new IOException("injected after participant temporary force"); + }); + assertThrows(IOException.class, + () -> failing.store(bytes(12, 10), envelope("account-asset", marker(10)))); + Snapshot old = normal.load(); + assertArrayEquals(bytes(12, 8), old.getBusinessPayload()); + assertEquals(8, old.getProgress().getEpoch()); + + normal.store(bytes(12, 10), envelope("account-asset", marker(10))); + Snapshot current = normal.load(); + assertArrayEquals(bytes(12, 10), current.getBusinessPayload()); + assertEquals(10, current.getProgress().getEpoch()); + + byte[] corrupt = Files.readAllBytes(path); + corrupt[corrupt.length - 1] ^= 1; + Files.write(path, corrupt); + assertThrows(ArchivePersistenceException.class, normal::load); + } + + @Test + public void replayCrashLeavesOldBatchAndSecondRestartReplaysOnce() throws Exception { + try (Fixture fixture = fixture()) { + ArchiveParticipantBatchFile failingAsset = new ArchiveParticipantBatchFile( + fixture.assetPath, "account-asset", PARTICIPANTS, temporary -> { + throw new IOException("injected replay batch crash"); + }); + DurableBatchStorage first = new DurableBatchStorage(fixture, failingAsset); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveRecoveryExecutor(first).recover()); + assertEquals(Arrays.asList("truncate:10"), first.actions); + Snapshot afterCrash = fixture.batches.get("account-asset").load(); + assertArrayEquals(bytes(12, 8), afterCrash.getBusinessPayload()); + assertEquals(8, afterCrash.getProgress().getEpoch()); + + RecoverySnapshot restart = fixture.scanner.scan(); + assertEquals(10, restart.getHistoryHead()); + assertEquals(Long.valueOf(8), restart.getParticipantHeads().get("account-asset")); + assertEquals(8, restart.getReaderVisibleHead()); + + DurableBatchStorage second = new DurableBatchStorage(fixture, + fixture.batches.get("account-asset")); + new ArchiveRecoveryExecutor(second).recover(); + assertEquals(Arrays.asList("replay:account-asset:9-10", "publish:10"), second.actions); + Snapshot recovered = fixture.batches.get("account-asset").load(); + assertArrayEquals(bytes(12, 10), recovered.getBusinessPayload()); + assertEquals(10, recovered.getProgress().getEpoch()); + assertEquals(10, fixture.scanner.scan().getReaderVisibleHead()); + + DurableBatchStorage third = new DurableBatchStorage(fixture, + fixture.batches.get("account-asset")); + assertEquals(0, new ArchiveRecoveryExecutor(third).recover().getActions().size()); + assertEquals(0, third.actions.size()); + } + } + + private Fixture fixture() throws Exception { + Path directory = temporaryFolder.newFolder().toPath(); + HistoryCommitStore history = new HistoryCommitStore(directory, + new HistoryCommitMarkerCodec()); + List markers = new ArrayList<>(); + for (long epoch = 8; epoch <= 12; epoch++) { + markers.add(marker(epoch)); + } + history.commitAll(markers); + + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + Path checkpointPath = directory.resolve("progress/checkpoint.progress"); + Path readerPath = directory.resolve("progress/reader.progress"); + new ArchiveProgressFile(checkpointPath, codec).store(globalEnvelope( + Kind.APPLY_CHECKPOINT, history.get(10))); + new ArchiveProgressFile(readerPath, codec).store(globalEnvelope( + Kind.READER_VISIBLE, history.get(8))); + + Path accountPath = directory.resolve("participants/account.batch"); + Path assetPath = directory.resolve("participants/account-asset.batch"); + Map batches = new LinkedHashMap<>(); + batches.put("account", new ArchiveParticipantBatchFile(accountPath, + "account", PARTICIPANTS)); + batches.put("account-asset", new ArchiveParticipantBatchFile(assetPath, + "account-asset", PARTICIPANTS)); + batches.get("account").store(bytes(12, 10), envelope("account", history.get(10))); + batches.get("account-asset").store(bytes(12, 8), + envelope("account-asset", history.get(8))); + + ArchiveRecoveryAuthorityScanner scanner = + ArchiveRecoveryAuthorityScanner.forParticipantBatches(history, checkpointPath, + batches, readerPath, PARTICIPANTS); + return new Fixture(history, scanner, batches, assetPath, readerPath); + } + + private static ArchiveProgressEnvelope envelope(String participant, + HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, participant, + marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); + } + + private static ArchiveProgressEnvelope globalEnvelope(Kind kind, + HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); + } + + private static HistoryCommitMarker marker(long epoch) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, bytes(32, (int) epoch), + bytes(32, (int) epoch - 1), epoch * 1_000); + HistoryLocation body = new HistoryLocation(0, epoch * 100, 100, (int) epoch, + bytes(32, (int) epoch + 20)); + HistoryIndexLocation index = new HistoryIndexLocation(epoch * 50, 50, + bytes(32, (int) epoch + 30)); + return new HistoryCommitMarker(meta, epoch - 1, body, index, + bytes(16, (int) epoch + 40), PARTICIPANTS); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } + + private static final class DurableBatchStorage implements RecoveryStorage { + private final Fixture fixture; + private final ArchiveParticipantBatchFile assetReplayBatch; + private final List actions = new ArrayList<>(); + + private DurableBatchStorage(Fixture fixture, + ArchiveParticipantBatchFile assetReplayBatch) { + this.fixture = fixture; + this.assetReplayBatch = assetReplayBatch; + } + + @Override + public RecoverySnapshot scan() throws IOException { + return fixture.scanner.scan(); + } + + @Override + public void truncateHistoryAndSync(long historyHead) throws IOException { + HistoryCommitMarker head = fixture.history.head(); + while (head != null && head.getMeta().getEpoch() > historyHead) { + fixture.history.removeHead(head.getMeta()); + head = fixture.history.head(); + } + actions.add("truncate:" + historyHead); + } + + @Override + public void replayParticipantAndSyncProgress(String participant, long firstEpoch, + long lastEpoch) throws IOException { + ArchiveParticipantBatchFile batch = "account-asset".equals(participant) + ? assetReplayBatch : fixture.batches.get(participant); + batch.store(bytes(12, (int) lastEpoch), + envelope(participant, fixture.history.get(lastEpoch))); + actions.add("replay:" + participant + ":" + firstEpoch + "-" + lastEpoch); + } + + @Override + public void publishReaderHeadAndSync(long readerVisibleHead) throws IOException { + new ArchiveReaderHeadPublisher(fixture.history, fixture.readerPath, PARTICIPANTS) + .publish(readerVisibleHead); + actions.add("publish:" + readerVisibleHead); + } + } + + private static final class Fixture implements AutoCloseable { + private final HistoryCommitStore history; + private final ArchiveRecoveryAuthorityScanner scanner; + private final Map batches; + private final Path assetPath; + private final Path readerPath; + + private Fixture(HistoryCommitStore history, ArchiveRecoveryAuthorityScanner scanner, + Map batches, Path assetPath, Path readerPath) { + this.history = history; + this.scanner = scanner; + this.batches = batches; + this.assetPath = assetPath; + this.readerPath = readerPath; + } + + @Override + public void close() throws IOException { + history.close(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java new file mode 100644 index 00000000000..252fc2307f4 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java @@ -0,0 +1,139 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; + +public class ArchiveProgressEnvelopeTest { + + private static final List PARTICIPANTS = Arrays.asList( + "account", "account-asset", "storage-row"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void deterministicallyRoundTripsCheckpointParticipantAndReaderProgress() { + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + ArchiveProgressEnvelope checkpoint = checkpoint(10, 1); + byte[] first = codec.encode(checkpoint); + byte[] second = codec.encode(checkpoint(10, 1)); + assertArrayEquals(first, second); + assertEnvelope(checkpoint, codec.decode(first)); + + ArchiveProgressEnvelope progress = progress("account-asset", 10, 1); + assertEnvelope(progress, codec.decode(codec.encode(progress))); + + ArchiveProgressEnvelope reader = reader(10, 1); + assertEnvelope(reader, codec.decode(codec.encode(reader))); + } + + @Test + public void rejectsCorruptionTruncationAndInvalidIdentity() { + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + byte[] encoded = codec.encode(checkpoint(10, 1)); + encoded[encoded.length - 1] ^= 1; + assertThrows(IllegalArgumentException.class, () -> codec.decode(encoded)); + assertThrows(IllegalArgumentException.class, + () -> codec.decode(Arrays.copyOf(encoded, 20))); + + assertThrows(IllegalArgumentException.class, () -> new ArchiveProgressEnvelope( + Kind.PARTICIPANT_PROGRESS, "contract", 10, bytes(32, 1), bytes(16, 2), + bytes(32, 3), PARTICIPANTS)); + assertThrows(IllegalArgumentException.class, () -> new ArchiveProgressEnvelope( + Kind.APPLY_CHECKPOINT, null, 10, bytes(31, 1), bytes(16, 2), bytes(32, 3), + PARTICIPANTS)); + } + + @Test + public void rejectsEveryExpectedIdentityMismatch() { + ArchiveProgressEnvelope progress = progress("account-asset", 10, 1); + progress.requireIdentity(Kind.PARTICIPANT_PROGRESS, "account-asset", 10, + bytes(32, 1), bytes(16, 2), bytes(32, 3), PARTICIPANTS); + + assertThrows(ArchivePersistenceException.class, + () -> progress.requireIdentity(Kind.PARTICIPANT_PROGRESS, "storage-row", 10, + bytes(32, 1), bytes(16, 2), bytes(32, 3), PARTICIPANTS)); + assertThrows(ArchivePersistenceException.class, + () -> progress.requireIdentity(Kind.PARTICIPANT_PROGRESS, "account-asset", 10, + bytes(32, 9), bytes(16, 2), bytes(32, 3), PARTICIPANTS)); + assertThrows(ArchivePersistenceException.class, + () -> progress.requireIdentity(Kind.PARTICIPANT_PROGRESS, "account-asset", 10, + bytes(32, 1), bytes(16, 9), bytes(32, 3), PARTICIPANTS)); + assertThrows(ArchivePersistenceException.class, + () -> progress.requireIdentity(Kind.PARTICIPANT_PROGRESS, "account-asset", 10, + bytes(32, 1), bytes(16, 2), bytes(32, 9), PARTICIPANTS)); + assertThrows(ArchivePersistenceException.class, + () -> progress.requireIdentity(Kind.PARTICIPANT_PROGRESS, "account-asset", 11, + bytes(32, 1), bytes(16, 2), bytes(32, 3), PARTICIPANTS)); + assertThrows(ArchivePersistenceException.class, + () -> progress.requireIdentity(Kind.PARTICIPANT_PROGRESS, "account-asset", 10, + bytes(32, 1), bytes(16, 2), bytes(32, 3), + Arrays.asList("account", "account-asset"))); + } + + @Test + public void preservesOldAuthorityWhenCrashOccursBeforeAtomicReplace() throws Exception { + Path directory = temporaryFolder.newFolder("progress-file").toPath(); + Path path = directory.resolve("checkpoint.progress"); + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + new ArchiveProgressFile(path, codec).store(checkpoint(9, 1)); + + ArchiveProgressFile failing = new ArchiveProgressFile(path, codec, temporary -> { + throw new java.io.IOException("injected after temporary force"); + }); + assertThrows(java.io.IOException.class, () -> failing.store(checkpoint(10, 2))); + assertTrue(Files.exists(failing.getTemporaryPath())); + assertEquals(9, new ArchiveProgressFile(path, codec).load().getEpoch()); + + new ArchiveProgressFile(path, codec).store(checkpoint(10, 2)); + assertEquals(10, new ArchiveProgressFile(path, codec).load().getEpoch()); + byte[] corrupt = Files.readAllBytes(path); + corrupt[corrupt.length - 1] ^= 1; + Files.write(path, corrupt); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveProgressFile(path, codec).load()); + } + + private static ArchiveProgressEnvelope checkpoint(long epoch, int seed) { + return new ArchiveProgressEnvelope(Kind.APPLY_CHECKPOINT, null, epoch, bytes(32, seed), + bytes(16, seed + 1), bytes(32, seed + 2), PARTICIPANTS); + } + + private static ArchiveProgressEnvelope progress(String participant, long epoch, int seed) { + return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, participant, epoch, + bytes(32, seed), bytes(16, seed + 1), bytes(32, seed + 2), PARTICIPANTS); + } + + private static ArchiveProgressEnvelope reader(long epoch, int seed) { + return new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, epoch, bytes(32, seed), + bytes(16, seed + 1), bytes(32, seed + 2), PARTICIPANTS); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } + + private static void assertEnvelope(ArchiveProgressEnvelope expected, + ArchiveProgressEnvelope actual) { + assertEquals(expected.getKind(), actual.getKind()); + assertEquals(expected.getParticipant(), actual.getParticipant()); + assertEquals(expected.getEpoch(), actual.getEpoch()); + assertArrayEquals(expected.getBlockHash(), actual.getBlockHash()); + assertArrayEquals(expected.getBatchId(), actual.getBatchId()); + assertArrayEquals(expected.getPayloadDigest(), actual.getPayloadDigest()); + assertEquals(expected.getParticipants(), actual.getParticipants()); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryExecutorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryExecutorTest.java new file mode 100644 index 00000000000..7507b2fa3a0 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryExecutorTest.java @@ -0,0 +1,162 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoveryStorage; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; + +public class ArchiveRecoveryExecutorTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void secondRestartReadsDurableProgressAndExecutesOnlyRemainingParticipant() + throws Exception { + Path directory = temporaryFolder.newFolder("second-crash").toPath(); + List participants = Arrays.asList("account", "account-asset", "storage-row"); + DurableTestStorage.initialize(directory, 12, 10, 7, + heads("account", 10L, "account-asset", 8L, "storage-row", 7L)); + + DurableTestStorage firstStorage = new DurableTestStorage(directory, participants); + ArchiveRecoveryExecutor first = new ArchiveRecoveryExecutor(firstStorage, action -> { + if (action.getType() == ActionType.REPLAY_PARTICIPANT + && "account-asset".equals(action.getParticipant())) { + throw new IOException("injected crash after participant progress force"); + } + }); + assertThrows(ArchivePersistenceException.class, first::recover); + assertEquals(Arrays.asList("account-asset:9-10"), firstStorage.getReplays()); + + RecoverySnapshot afterCrash = new DurableTestStorage(directory, participants).scan(); + assertEquals(10, afterCrash.getHistoryHead()); + assertEquals(10, afterCrash.getParticipantHeads().get("account-asset").longValue()); + assertEquals(7, afterCrash.getParticipantHeads().get("storage-row").longValue()); + assertEquals(7, afterCrash.getReaderVisibleHead()); + + DurableTestStorage secondStorage = new DurableTestStorage(directory, participants); + new ArchiveRecoveryExecutor(secondStorage).recover(); + assertEquals(Arrays.asList("storage-row:8-10"), secondStorage.getReplays()); + + RecoverySnapshot recovered = new DurableTestStorage(directory, participants).scan(); + assertEquals(10, recovered.getHistoryHead()); + assertEquals(10, recovered.getCheckpointHead()); + assertEquals(10, recovered.getReaderVisibleHead()); + recovered.getParticipantHeads().values().forEach(head -> assertEquals(10, head.longValue())); + + DurableTestStorage thirdStorage = new DurableTestStorage(directory, participants); + assertEquals(0, new ArchiveRecoveryExecutor(thirdStorage).recover().getActions().size()); + assertEquals(0, thirdStorage.getReplays().size()); + } + + private static Map heads(Object... values) { + Map heads = new LinkedHashMap<>(); + for (int index = 0; index < values.length; index += 2) { + heads.put((String) values[index], (Long) values[index + 1]); + } + return heads; + } + + private static final class DurableTestStorage implements RecoveryStorage { + private static final String HISTORY = "history.head"; + private static final String CHECKPOINT = "checkpoint.head"; + private static final String READER = "reader.head"; + + private final Path directory; + private final List participants; + private final List replays = new ArrayList<>(); + + private DurableTestStorage(Path directory, List participants) { + this.directory = directory; + this.participants = new ArrayList<>(participants); + } + + private static void initialize(Path directory, long historyHead, long checkpointHead, + long readerHead, Map participantHeads) throws IOException { + Files.createDirectories(directory); + writeLong(directory.resolve(HISTORY), historyHead); + writeLong(directory.resolve(CHECKPOINT), checkpointHead); + writeLong(directory.resolve(READER), readerHead); + for (Map.Entry entry : participantHeads.entrySet()) { + writeLong(participantPath(directory, entry.getKey()), entry.getValue()); + } + } + + @Override + public RecoverySnapshot scan() throws IOException { + Map participantHeads = new LinkedHashMap<>(); + for (String participant : participants) { + participantHeads.put(participant, readLong(participantPath(directory, participant))); + } + return new RecoverySnapshot(readLong(directory.resolve(HISTORY)), + readLong(directory.resolve(CHECKPOINT)), participantHeads, + readLong(directory.resolve(READER))); + } + + @Override + public void truncateHistoryAndSync(long historyHead) throws IOException { + writeLong(directory.resolve(HISTORY), historyHead); + } + + @Override + public void replayParticipantAndSyncProgress(String participant, long firstEpoch, + long lastEpoch) throws IOException { + replays.add(participant + ":" + firstEpoch + "-" + lastEpoch); + writeLong(participantPath(directory, participant), lastEpoch); + } + + @Override + public void publishReaderHeadAndSync(long readerVisibleHead) throws IOException { + writeLong(directory.resolve(READER), readerVisibleHead); + } + + private List getReplays() { + return replays; + } + + private static Path participantPath(Path directory, String participant) { + return directory.resolve("participant-" + participant + ".head"); + } + + private static long readLong(Path path) throws IOException { + byte[] encoded = Files.readAllBytes(path); + if (encoded.length != Long.BYTES) { + throw new IOException("Invalid durable test progress length"); + } + return ByteBuffer.wrap(encoded).getLong(); + } + + private static void writeLong(Path path, long value) throws IOException { + Path temporary = path.resolveSibling(path.getFileName() + ".tmp"); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES).putLong(value); + buffer.flip(); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + HistorySegmentStore.syncDirectory(path.getParent()); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryPlannerTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryPlannerTest.java new file mode 100644 index 00000000000..e82adadbed1 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryPlannerTest.java @@ -0,0 +1,107 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryAction; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; + +public class ArchiveRecoveryPlannerTest { + + @Test + public void resumesOnlyRemainingParticipantAfterASecondCrash() { + Map firstHeads = heads("storage-row", 7L, "account", 10L, + "account-asset", 8L); + RecoveryPlan first = ArchiveRecoveryPlanner.plan(12, 10, firstHeads, 7); + + assertEquals(7, first.getSafeHeadBeforeRecovery()); + assertActions(first.getActions(), + action(ActionType.TRUNCATE_HISTORY, null, 10, 10), + action(ActionType.REPLAY_PARTICIPANT, "account-asset", 9, 10), + action(ActionType.REPLAY_PARTICIPANT, "storage-row", 8, 10), + action(ActionType.PUBLISH_READER_HEAD, null, 10, 10)); + + // The process crashes after truncating H and durably advancing only account-asset D[i]. + Map secondHeads = heads("storage-row", 7L, "account", 10L, + "account-asset", 10L); + RecoveryPlan second = ArchiveRecoveryPlanner.plan(10, 10, secondHeads, 7); + + assertActions(second.getActions(), + action(ActionType.REPLAY_PARTICIPANT, "storage-row", 8, 10), + action(ActionType.PUBLISH_READER_HEAD, null, 10, 10)); + + Map recoveredHeads = heads("storage-row", 10L, "account", 10L, + "account-asset", 10L); + RecoveryPlan recovered = ArchiveRecoveryPlanner.plan(10, 10, recoveredHeads, 10); + assertEquals(10, recovered.getSafeHeadBeforeRecovery()); + assertEquals(0, recovered.getActions().size()); + } + + @Test + public void chunksEveryParticipantReplayRange() { + RecoveryPlan plan = ArchiveRecoveryPlanner.plan(2_050, 2_050, + heads("account", 0L), 0); + + assertActions(plan.getActions(), + action(ActionType.REPLAY_PARTICIPANT, "account", 1, 1_024), + action(ActionType.REPLAY_PARTICIPANT, "account", 1_025, 2_048), + action(ActionType.REPLAY_PARTICIPANT, "account", 2_049, 2_050), + action(ActionType.PUBLISH_READER_HEAD, null, 2_050, 2_050)); + } + + @Test + public void rejectsEveryAheadOrUnsafeStateBeforePlanningActions() { + assertThrows(ArchivePersistenceException.class, + () -> ArchiveRecoveryPlanner.plan(9, 10, heads("account", 9L), 9)); + assertThrows(ArchivePersistenceException.class, + () -> ArchiveRecoveryPlanner.plan(10, 10, heads("account", 11L), 10)); + assertThrows(ArchivePersistenceException.class, + () -> ArchiveRecoveryPlanner.plan(10, 10, heads("account", 8L), 9)); + assertThrows(ArchivePersistenceException.class, + () -> ArchiveRecoveryPlanner.plan(10, 10, java.util.Collections.emptyMap(), 10)); + } + + private static Map heads(Object... values) { + Map heads = new LinkedHashMap<>(); + for (int index = 0; index < values.length; index += 2) { + heads.put((String) values[index], (Long) values[index + 1]); + } + return heads; + } + + private static ExpectedAction action(ActionType type, String participant, long first, + long last) { + return new ExpectedAction(type, participant, first, last); + } + + private static void assertActions(List actual, ExpectedAction... expected) { + assertEquals(expected.length, actual.size()); + for (int index = 0; index < expected.length; index++) { + ExpectedAction left = expected[index]; + RecoveryAction right = actual.get(index); + assertEquals(left.type, right.getType()); + assertEquals(left.participant, right.getParticipant()); + assertEquals(left.firstEpoch, right.getFirstEpoch()); + assertEquals(left.lastEpoch, right.getLastEpoch()); + } + } + + private static final class ExpectedAction { + private final ActionType type; + private final String participant; + private final long firstEpoch; + private final long lastEpoch; + + private ExpectedAction(ActionType type, String participant, long firstEpoch, long lastEpoch) { + this.type = type; + this.participant = participant; + this.firstEpoch = firstEpoch; + this.lastEpoch = lastEpoch; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryScannerTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryScannerTest.java new file mode 100644 index 00000000000..58cc50fd2c7 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryScannerTest.java @@ -0,0 +1,224 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryAction; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; + +public class ArchiveRecoveryScannerTest { + + private static final List PARTICIPANTS = Arrays.asList("account", "account-asset"); + + @Test + public void resolvesLaggingParticipantAgainstItsOwnCommittedEpoch() throws Exception { + TestHistory history = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); + TestProgress progress = progress(envelope(Kind.APPLY_CHECKPOINT, null, + history.markers.get(10L)), envelope(Kind.READER_VISIBLE, null, + history.markers.get(8L))); + progress.participantProgress.put("account", + envelope(Kind.PARTICIPANT_PROGRESS, "account", history.markers.get(10L))); + progress.participantProgress.put("account-asset", + envelope(Kind.PARTICIPANT_PROGRESS, "account-asset", history.markers.get(8L))); + + RecoverySnapshot snapshot = scanner(history, progress).scan(); + assertEquals(12, snapshot.getHistoryHead()); + assertEquals(10, snapshot.getCheckpointHead()); + assertEquals(Long.valueOf(10), snapshot.getParticipantHeads().get("account")); + assertEquals(Long.valueOf(8), snapshot.getParticipantHeads().get("account-asset")); + assertEquals(8, snapshot.getReaderVisibleHead()); + + RecoveryPlan plan = ArchiveRecoveryPlanner.plan(snapshot.getHistoryHead(), + snapshot.getCheckpointHead(), snapshot.getParticipantHeads(), + snapshot.getReaderVisibleHead()); + assertAction(plan.getActions().get(0), ActionType.TRUNCATE_HISTORY, null, 10, 10); + assertAction(plan.getActions().get(1), ActionType.REPLAY_PARTICIPANT, + "account-asset", 9, 10); + assertAction(plan.getActions().get(2), ActionType.PUBLISH_READER_HEAD, null, 10, 10); + } + + @Test + public void rejectsMissingHistoryAndEveryProgressSourceGap() { + TestHistory missingHistory = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); + TestProgress valid = validProgress(missingHistory); + missingHistory.markers.remove(8L); + assertThrows(ArchivePersistenceException.class, + () -> scanner(missingHistory, valid).scan()); + + TestHistory completeHistory = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); + TestProgress missingCheckpoint = validProgress(completeHistory); + missingCheckpoint.checkpoint = null; + assertThrows(ArchivePersistenceException.class, + () -> scanner(completeHistory, missingCheckpoint).scan()); + + TestProgress missingParticipant = validProgress(completeHistory); + missingParticipant.participantProgress.remove("account-asset"); + assertThrows(ArchivePersistenceException.class, + () -> scanner(completeHistory, missingParticipant).scan()); + + TestProgress unexpectedParticipant = validProgress(completeHistory); + unexpectedParticipant.participantProgress.put("storage-row", + unexpectedParticipant.participantProgress.get("account")); + assertThrows(ArchivePersistenceException.class, + () -> scanner(completeHistory, unexpectedParticipant).scan()); + + TestProgress missingReader = validProgress(completeHistory); + missingReader.readerVisible = null; + assertThrows(ArchivePersistenceException.class, + () -> scanner(completeHistory, missingReader).scan()); + } + + @Test + public void rejectsEveryEnvelopeIdentityMismatch() { + TestHistory history = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); + HistoryCommitMarker marker = history.markers.get(8L); + List mismatches = Arrays.asList( + new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, "account-asset", 8, + bytes(32, 90), marker.getBatchId(), marker.getHistoryLocation().getBodyDigest(), + PARTICIPANTS), + new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, "account-asset", 8, + marker.getMeta().getBlockHash(), bytes(16, 91), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS), + new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, "account-asset", 8, + marker.getMeta().getBlockHash(), marker.getBatchId(), bytes(32, 92), PARTICIPANTS), + new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, "account", 8, + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS), + new ArchiveProgressEnvelope(Kind.APPLY_CHECKPOINT, null, 8, + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS), + new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, "account-asset", 8, + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), + Arrays.asList("account", "account-asset", "storage-row"))); + + for (ArchiveProgressEnvelope mismatch : mismatches) { + TestProgress progress = validProgress(history); + progress.participantProgress.put("account-asset", mismatch); + assertThrows(ArchivePersistenceException.class, () -> scanner(history, progress).scan()); + } + } + + @Test + public void rejectsCommittedMarkerEpochOrParticipantSetMismatch() { + TestHistory wrongEpoch = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); + TestProgress epochProgress = validProgress(wrongEpoch); + wrongEpoch.markers.put(8L, marker(7, PARTICIPANTS)); + assertThrows(ArchivePersistenceException.class, + () -> scanner(wrongEpoch, epochProgress).scan()); + + TestHistory wrongSet = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); + TestProgress setProgress = validProgress(wrongSet); + wrongSet.markers.put(8L, + marker(8, Arrays.asList("account", "account-asset", "storage-row"))); + assertThrows(ArchivePersistenceException.class, + () -> scanner(wrongSet, setProgress).scan()); + } + + private static ArchiveRecoveryScanner scanner(TestHistory history, TestProgress progress) { + return new ArchiveRecoveryScanner(history, progress, PARTICIPANTS); + } + + private static TestProgress validProgress(TestHistory history) { + TestProgress progress = progress(envelope(Kind.APPLY_CHECKPOINT, null, + history.markers.get(10L)), envelope(Kind.READER_VISIBLE, null, + history.markers.get(8L))); + progress.participantProgress.put("account", + envelope(Kind.PARTICIPANT_PROGRESS, "account", history.markers.get(10L))); + progress.participantProgress.put("account-asset", + envelope(Kind.PARTICIPANT_PROGRESS, "account-asset", history.markers.get(8L))); + return progress; + } + + private static TestHistory history(HistoryCommitMarker... markers) { + TestHistory history = new TestHistory(); + for (HistoryCommitMarker marker : markers) { + history.markers.put(marker.getMeta().getEpoch(), marker); + } + return history; + } + + private static TestProgress progress(ArchiveProgressEnvelope checkpoint, + ArchiveProgressEnvelope readerVisible) { + TestProgress progress = new TestProgress(); + progress.checkpoint = checkpoint; + progress.readerVisible = readerVisible; + return progress; + } + + private static ArchiveProgressEnvelope envelope(Kind kind, String participant, + HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); + } + + private static HistoryCommitMarker marker(long epoch, List participants) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, bytes(32, (int) epoch), + bytes(32, (int) epoch - 1), epoch * 1_000); + HistoryLocation body = new HistoryLocation(0, epoch * 100, 100, (int) epoch, + bytes(32, (int) epoch + 20)); + HistoryIndexLocation index = new HistoryIndexLocation(epoch * 50, 50, + bytes(32, (int) epoch + 30)); + return new HistoryCommitMarker(meta, epoch - 1, body, index, + bytes(16, (int) epoch + 40), participants); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } + + private static void assertAction(RecoveryAction action, ActionType type, String participant, + long firstEpoch, long lastEpoch) { + assertEquals(type, action.getType()); + assertEquals(participant, action.getParticipant()); + assertEquals(firstEpoch, action.getFirstEpoch()); + assertEquals(lastEpoch, action.getLastEpoch()); + } + + private static final class TestHistory implements ArchiveRecoveryScanner.HistoryIdentitySource { + private final Map markers = new LinkedHashMap<>(); + + @Override + public long committedHeadEpoch() { + return 12; + } + + @Override + public HistoryCommitMarker committedMarker(long epoch) { + return markers.get(epoch); + } + } + + private static final class TestProgress implements ArchiveRecoveryScanner.ProgressIdentitySource { + private ArchiveProgressEnvelope checkpoint; + private final Map participantProgress = + new LinkedHashMap<>(); + private ArchiveProgressEnvelope readerVisible; + + @Override + public ArchiveProgressEnvelope loadCheckpoint() { + return checkpoint; + } + + @Override + public Map loadParticipantProgress() { + return participantProgress; + } + + @Override + public ArchiveProgressEnvelope loadReaderVisible() { + return readerVisible; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java new file mode 100644 index 00000000000..2db364d4377 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java @@ -0,0 +1,180 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveTruncationRecovery.Stage; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +public class ArchiveTruncationRecoveryTest { + + private static final List PARTICIPANTS = Collections.singletonList("account"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void preparedIntentRecoversBeforeCommitShrinkWithoutEpochZeroScan() throws Exception { + Path archive = temporaryFolder.newFolder("before-commit").toPath(); + initialize(archive); + prepare(archive); + assertHeads(archive, 12, 12, 12, 12); + + assertTrue(new ArchiveTruncationRecovery(archive, 4096).recover()); + assertRecovered(archive); + assertFalse(new ArchiveTruncationRecovery(archive, 4096).recover()); + } + + @Test + public void everyPostIntentCrashUsesIntentAndConvergesToTargetCheckpoint() throws Exception { + for (Stage failedStage : Stage.values()) { + Path archive = temporaryFolder.newFolder("intent-" + failedStage).toPath(); + initialize(archive); + prepare(archive); + ArchiveTruncationRecovery recovery = new ArchiveTruncationRecovery(archive, 4096, + stage -> { + if (stage == failedStage) { + throw new IOException("injected after " + stage); + } + }); + assertThrows(IOException.class, recovery::recover); + + ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + new HistoryCommitMarkerCodec()); + long expectedCheckpoint = failedStage == Stage.COMMIT_SHRUNK ? 12 : 10; + assertEquals(expectedCheckpoint, checkpoint.getMarker().getMeta().getEpoch()); + + assertTrue(new ArchiveTruncationRecovery(archive, 4096).recover()); + assertRecovered(archive); + } + } + + @Test + public void intentPreReplaceCrashNeverShrinksCommittedAuthority() throws Exception { + Path archive = temporaryFolder.newFolder("intent-pre-replace").toPath(); + initialize(archive); + ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + new HistoryCommitMarkerCodec()); + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096, checkpoint); + HistoryIndexStore index = new HistoryIndexStore( + archive, new HistoryIndexCodec(), checkpoint); + HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec(), checkpoint)) { + assertThrows(IOException.class, () -> ArchiveTruncationIntent.prepare(archive, + commits, index, bodies, 10, new HistoryCommitMarkerCodec(), temporary -> { + throw new IOException("injected before intent replace"); + })); + } + assertNull(ArchiveTruncationIntent.load(archive, new HistoryCommitMarkerCodec())); + assertFalse(new ArchiveTruncationRecovery(archive, 4096).recover()); + assertHeads(archive, 12, 12, 12, 12); + } + + @Test + public void corruptIntentFailsBeforeCommitShrink() throws Exception { + Path archive = temporaryFolder.newFolder("corrupt-intent").toPath(); + initialize(archive); + prepare(archive); + Path intentPath = archive.resolve("truncation.intent"); + byte[] corrupt = Files.readAllBytes(intentPath); + corrupt[corrupt.length - 1] ^= 1; + Files.write(intentPath, corrupt); + + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveTruncationRecovery(archive, 4096).recover()); + assertHeads(archive, 12, 12, 12, 12); + } + + private static void initialize(Path archive) throws Exception { + HistoryCommitMarker head; + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096); + HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); + HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + List markers = new ArrayList<>(); + for (long epoch = 8; epoch <= 12; epoch++) { + BlockReverseDiff diff = diff(epoch); + HistoryLocation body = bodies.append(diff); + HistoryIndexLocation indexLocation = index.append(HistoryIndexRecord.from(diff, body)); + markers.add(new HistoryCommitMarker(diff.getMeta(), epoch - 1, body, indexLocation, + bytes(16, (int) epoch + 40), PARTICIPANTS)); + } + bodies.sync(); + index.sync(); + commits.commitAll(markers); + head = commits.head(); + ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), + commits.getRecordLength(), head, new HistoryCommitMarkerCodec()); + } + } + + private static void prepare(Path archive) throws Exception { + ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + new HistoryCommitMarkerCodec()); + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096, checkpoint); + HistoryIndexStore index = new HistoryIndexStore( + archive, new HistoryIndexCodec(), checkpoint); + HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec(), checkpoint)) { + ArchiveTruncationIntent.prepare(archive, commits, index, bodies, 10, + new HistoryCommitMarkerCodec()); + } + } + + private static void assertRecovered(Path archive) throws Exception { + assertNull(ArchiveTruncationIntent.load(archive, new HistoryCommitMarkerCodec())); + assertHeads(archive, 10, 10, 10, 10); + } + + private static void assertHeads(Path archive, long checkpointEpoch, long commitEpoch, + long indexEpoch, long bodyEpoch) throws Exception { + ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + new HistoryCommitMarkerCodec()); + assertEquals(checkpointEpoch, checkpoint.getMarker().getMeta().getEpoch()); + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096, checkpoint); + HistoryIndexStore index = new HistoryIndexStore( + archive, new HistoryIndexCodec(), checkpoint); + HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec(), checkpoint)) { + assertEquals(commitEpoch, commits.head().getMeta().getEpoch()); + assertEquals(indexEpoch, index.getScanResult().getHead().getRecord().getMeta().getEpoch()); + assertEquals(bodyEpoch, bodies.getScanResult().getHead().getDiff().getMeta().getEpoch()); + if (checkpointEpoch == 10 && commitEpoch == 10 && indexEpoch == 10 && bodyEpoch == 10) { + assertEquals(1, commits.getStartupScannedRecords()); + assertEquals(1, index.getStartupScannedRecords()); + assertEquals(1, bodies.getStartupScannedRecords()); + } + } + } + + private static BlockReverseDiff diff(long epoch) { + return new BlockReverseDiff(new BlockSnapshotMeta(epoch, epoch, + bytes(32, (int) epoch), bytes(32, (int) epoch - 1), epoch * 1_000), + Collections.singletonList(new DbGroup("account", Collections.singletonList( + new Entry(bytes(8, (int) epoch), OldValue.present(bytes(12, (int) epoch))))))); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java b/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java new file mode 100644 index 00000000000..32f2b03db1e --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java @@ -0,0 +1,496 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.OptionalLong; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedHistory; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.archive.HistoryIndexRecord.KeyGroup; + +public class PersistentServingKeyIndexGenerationTest { + + private static final List PARTICIPANTS = Arrays.asList("account", "properties"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void persistsExactKeyChangesAndSourceIdentityAcrossReopen() throws Exception { + Path root = temporaryFolder.newFolder("persistent-serving").toPath(); + try (Fixture fixture = new Fixture(root.resolve("authoritative"))) { + fixture.append(1, group("account", bytes("cold"), bytes("hot"))); + fixture.append(2, group("properties", bytes("same"))); + fixture.append(3, group("account", bytes("hot"))); + fixture.sync(); + + Path generationPath = root.resolve("generation-1"); + byte[] expectedDigest = ServingKeyIndexGeneration.rebuild("memory", 0, hash(0), + fixture.markers, fixture.index::read, PARTICIPANTS, + ServingKeyIndexGeneration.IndexLayout.prototypeDefaults()) + .getAuthoritativePrefixDigest(); + try (PersistentServingKeyIndexGeneration generation = + PersistentServingKeyIndexGeneration.build(generationPath, "generation-1", 0, hash(0), + fixture.markers, fixture.index::read, PARTICIPANTS, hash(77))) { + assertEquals(0, generation.getIndexedFrom()); + assertEquals(3, generation.getIndexedThrough()); + assertArrayEquals(hash(3), generation.getHeadHash()); + assertArrayEquals(expectedDigest, generation.getAuthoritativePrefixDigest()); + assertArrayEquals(hash(77), generation.getLatestSourceIdentityDigest()); + assertTrue(generation.isLatestSourceIdentityBound()); + assertEquals(4, generation.getKeyChangeCount()); + assertEquals(1, change(generation, "account", bytes("hot"), 0, 3)); + assertEquals(3, change(generation, "account", bytes("hot"), 1, 3)); + assertEquals(2, change(generation, "properties", bytes("same"), 0, 3)); + assertFalse(generation.firstChangeAfter("account", bytes("missing"), 0, 3) + .isPresent()); + assertThrows(IllegalArgumentException.class, + () -> generation.firstChangeAfter("account-asset", bytes("hot"), 0, 3)); + } + + try (PersistentServingKeyIndexGeneration reopened = + PersistentServingKeyIndexGeneration.open(generationPath)) { + assertArrayEquals(hash(77), reopened.getLatestSourceIdentityDigest()); + assertEquals(3, change(reopened, "account", bytes("hot"), 2, 3)); + assertThrows(UnsupportedOperationException.class, + () -> reopened.changesInRange("account", new byte[0], null, 0, 3, 10)); + } + } + } + + @Test + public void catalogPinsOldGenerationUntilLastReaderReleasesIt() throws Exception { + Path root = temporaryFolder.newFolder("catalog").toPath(); + Path catalogRoot = root.resolve("catalog"); + try (Fixture fixture = new Fixture(root.resolve("authoritative"))) { + fixture.append(1, group("account", bytes("one"))); + fixture.sync(); + Path firstShadow = catalogRoot.resolve("shadow-1"); + try (PersistentServingKeyIndexGeneration ignored = + PersistentServingKeyIndexGeneration.build(firstShadow, "generation-1", 0, hash(0), + fixture.markers, fixture.index::read, PARTICIPANTS)) { + // Close the shadow engine before its directory is atomically installed. + } + + try (PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.create(catalogRoot, firstShadow, + reader(1, "generation-1"))) { + PersistentServingKeyIndexGeneration firstPin = catalog.pin(reader(1, "generation-1")); + assertEquals(1, catalog.getReferenceCount("generation-1")); + + fixture.append(2, group("properties", bytes("two"))); + fixture.sync(); + Path secondShadow = catalogRoot.resolve("shadow-2"); + try (PersistentServingKeyIndexGeneration ignored = + PersistentServingKeyIndexGeneration.build(secondShadow, "generation-2", 0, hash(0), + fixture.markers, fixture.index::read, PARTICIPANTS)) { + // Close before publication. + } + assertThrows(ArchivePersistenceException.class, + () -> catalog.publish("generation-1", secondShadow, + reader(1, "reader-behind"))); + assertThrows(ArchivePersistenceException.class, + () -> catalog.publish("generation-1", secondShadow, + reader(2, hash(99), "wrong-hash"))); + assertTrue(catalog.publish("generation-1", secondShadow, + reader(2, "generation-2"))); + assertEquals("generation-2", catalog.getCurrentGenerationId()); + assertThrows(ArchivePersistenceException.class, + () -> catalog.pin(reader(1, "reader-behind"))); + assertEquals(0, catalog.getReferenceCount("generation-2")); + assertTrue(catalog.generationExists("generation-1")); + assertEquals(1, change(firstPin, "account", bytes("one"), 0, 1)); + + firstPin.close(); + assertEquals(0, catalog.getReferenceCount("generation-1")); + assertFalse(catalog.generationExists("generation-1")); + try (PersistentServingKeyIndexGeneration secondPin = + catalog.pin(reader(2, "generation-2"))) { + assertEquals(2, change(secondPin, "properties", bytes("two"), 0, 2)); + } + } + + try (PersistentServingKeyIndexCatalog reopened = + PersistentServingKeyIndexCatalog.open(catalogRoot); + PersistentServingKeyIndexGeneration pin = + reopened.pin(reader(2, "generation-2"))) { + assertEquals("generation-2", pin.getGenerationId()); + assertEquals(2, pin.getIndexedThrough()); + } + } + } + + @Test + public void readSnapshotOwnsCatalogHistoryAndLatestPinsAsOneUnit() throws Exception { + Path root = temporaryFolder.newFolder("snapshot-pins").toPath(); + Path catalogRoot = root.resolve("catalog"); + try (Fixture fixture = new Fixture(root.resolve("authoritative"))) { + fixture.append(1, group("account", bytes("key"))); + fixture.sync(); + Path shadow = catalogRoot.resolve("shadow"); + try (PersistentServingKeyIndexGeneration ignored = + PersistentServingKeyIndexGeneration.build(shadow, "generation", 0, hash(0), + fixture.markers, fixture.index::read, PARTICIPANTS)) { + // Close before publication. + } + try (PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.create(catalogRoot, shadow, + reader(1, "generation"))) { + AtomicBoolean historyClosed = new AtomicBoolean(); + AtomicBoolean latestClosed = new AtomicBoolean(); + try (ArchiveReadSnapshot snapshot = ArchiveReadSnapshot.pin(0, catalog, + reader(1, "generation"), + serving -> history(serving, historyClosed), + serving -> latest(serving, latestClosed))) { + assertEquals(1, catalog.getReferenceCount("generation")); + assertArrayEquals(bytes("old"), snapshot.get("account", bytes("key")).getValue()); + } + assertTrue(historyClosed.get()); + assertTrue(latestClosed.get()); + assertEquals(0, catalog.getReferenceCount("generation")); + } + } + } + + @Test + public void catalogRejectsCorruptCurrentPointer() throws Exception { + Path root = temporaryFolder.newFolder("corrupt-current").toPath(); + Path catalogRoot = root.resolve("catalog"); + try (Fixture fixture = new Fixture(root.resolve("authoritative"))) { + fixture.append(1, group("account", bytes("key"))); + fixture.sync(); + Path shadow = catalogRoot.resolve("shadow"); + try (PersistentServingKeyIndexGeneration ignored = + PersistentServingKeyIndexGeneration.build(shadow, "generation", 0, hash(0), + fixture.markers, fixture.index::read, PARTICIPANTS)) { + // Close before publication. + } + try (PersistentServingKeyIndexCatalog ignored = + PersistentServingKeyIndexCatalog.create(catalogRoot, shadow, + reader(1, "generation"))) { + // Persist a valid initial catalog first. + } + Path current = catalogRoot.resolve("current"); + byte[] corrupt = Files.readAllBytes(current); + corrupt[corrupt.length - 1] ^= 1; + Files.write(current, corrupt); + assertThrows(ArchivePersistenceException.class, + () -> PersistentServingKeyIndexCatalog.open(catalogRoot)); + } + } + + @Test + public void snapshotPinsRealCommitIndexAndSegmentHandles() throws Exception { + Path root = temporaryFolder.newFolder("real-history-pin").toPath(); + Path archive = root.resolve("archive"); + Path catalogRoot = root.resolve("catalog"); + Path shadow = catalogRoot.resolve("shadow"); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, + new LinkedHashSet<>(PARTICIPANTS))) { + BlockReverseDiff diff = new BlockReverseDiff( + new BlockSnapshotMeta(1, 1, hash(1), hash(0), 1_000), + Collections.singletonList(new BlockReverseDiff.DbGroup("account", + Collections.singletonList(new BlockReverseDiff.Entry(bytes("key"), + OldValue.present(bytes("old"))))))); + writer.accept(diff); + try (PersistentServingKeyIndexGeneration ignored = + writer.buildServingGeneration(shadow, "generation", hash(77))) { + // Close before catalog publication. + } + } + + try (PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.create(catalogRoot, shadow, + reader(1, "generation")); + ArchiveReadSnapshot snapshot = ArchiveReadSnapshot.pin(0, catalog, + reader(1, "generation"), archive, 4096, + serving -> latest(serving, new AtomicBoolean()))) { + assertArrayEquals(bytes("old"), snapshot.get("account", bytes("key")).getValue()); + assertEquals(1, catalog.getReferenceCount("generation")); + } + } + + @Test + public void capsuleLoadsDurableReaderHeadAndReleasesPartialAcquireFailures() throws Exception { + Path root = temporaryFolder.newFolder("durable-capsule").toPath(); + Path archive = root.resolve("archive"); + Path catalogRoot = root.resolve("catalog"); + Path shadow = catalogRoot.resolve("shadow"); + Path unboundShadow = root.resolve("unbound-shadow"); + Path readerVisible = root.resolve("progress/reader-visible.progress"); + HistoryCommitMarker marker; + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, + new LinkedHashSet<>(PARTICIPANTS))) { + writer.accept(new BlockReverseDiff(new BlockSnapshotMeta(1, 1, hash(1), hash(0), 1_000), + Collections.singletonList(new BlockReverseDiff.DbGroup("account", + Collections.singletonList(new BlockReverseDiff.Entry(bytes("key"), + OldValue.present(bytes("old")))))))); + marker = writer.committedHead(); + try (PersistentServingKeyIndexGeneration ignored = + writer.buildServingGeneration(shadow, "generation", hash(77))) { + // Close before publication. + } + try (PersistentServingKeyIndexGeneration ignored = + writer.buildServingGeneration(unboundShadow, "unbound-generation")) { + // Legacy/unbound generations remain readable only outside the strict capsule. + } + } + ArchiveProgressEnvelope durableReader = new ArchiveProgressEnvelope( + ArchiveProgressEnvelope.Kind.READER_VISIBLE, null, 1, marker.getMeta().getBlockHash(), + marker.getBatchId(), marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + new ArchiveProgressFile(readerVisible, new ArchiveProgressEnvelopeCodec()) + .store(durableReader); + + try (PersistentServingKeyIndexCatalog unboundCatalog = + PersistentServingKeyIndexCatalog.create(root.resolve("unbound-catalog"), unboundShadow, + durableReader)) { + ArchiveGenerationCapsule unbound = new ArchiveGenerationCapsule(unboundCatalog, + readerVisible, archive, 4096, serving -> latest(serving, new AtomicBoolean())); + assertThrows(ArchivePersistenceException.class, () -> unbound.pin(0)); + assertEquals(0, unboundCatalog.getReferenceCount("unbound-generation")); + } + + try (PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.create(catalogRoot, shadow, durableReader)) { + ArchiveGenerationCapsule failing = new ArchiveGenerationCapsule(catalog, readerVisible, + archive, 4096, PersistentServingKeyIndexGenerationTest::failLatest); + assertThrows(IOException.class, () -> failing.pin(0)); + assertEquals(0, catalog.getReferenceCount("generation")); + + AtomicBoolean mismatchClosed = new AtomicBoolean(); + ArchiveGenerationCapsule mismatch = new ArchiveGenerationCapsule(catalog, readerVisible, + archive, 4096, serving -> latest(serving, mismatchClosed, hash(88))); + assertThrows(ArchivePersistenceException.class, () -> mismatch.pin(0)); + assertTrue(mismatchClosed.get()); + assertEquals(0, catalog.getReferenceCount("generation")); + + AtomicBoolean latestClosed = new AtomicBoolean(); + ArchiveGenerationCapsule capsule = new ArchiveGenerationCapsule(catalog, readerVisible, + archive, 4096, serving -> latest(serving, latestClosed)); + try (ArchiveReadSnapshot snapshot = capsule.pin(0)) { + assertEquals(1, catalog.getReferenceCount("generation")); + assertArrayEquals(bytes("old"), snapshot.get("account", bytes("key")).getValue()); + } + assertTrue(latestClosed.get()); + assertEquals(0, catalog.getReferenceCount("generation")); + } + } + + @Test + public void publicationCrashReopensAtOldOrNewAtomicGeneration() throws Exception { + for (PersistentServingKeyIndexCatalog.PublicationStage failedStage + : PersistentServingKeyIndexCatalog.PublicationStage.values()) { + Path root = temporaryFolder.newFolder("publish-" + failedStage).toPath(); + Path catalogRoot = root.resolve("catalog"); + try (Fixture fixture = new Fixture(root.resolve("authoritative"))) { + fixture.append(1, group("account", bytes("one"))); + fixture.sync(); + Path firstShadow = catalogRoot.resolve("shadow-1"); + try (PersistentServingKeyIndexGeneration ignored = + PersistentServingKeyIndexGeneration.build(firstShadow, "generation-1", 0, hash(0), + fixture.markers, fixture.index::read, PARTICIPANTS)) { + // Close before publication. + } + try (PersistentServingKeyIndexCatalog ignored = + PersistentServingKeyIndexCatalog.create(catalogRoot, firstShadow, + reader(1, "generation-1"))) { + // Establish old authority. + } + + fixture.append(2, group("properties", bytes("two"))); + fixture.sync(); + Path secondShadow = catalogRoot.resolve("shadow-2"); + try (PersistentServingKeyIndexGeneration ignored = + PersistentServingKeyIndexGeneration.build(secondShadow, "generation-2", 0, hash(0), + fixture.markers, fixture.index::read, PARTICIPANTS)) { + // Close before publication. + } + AtomicReference observed = + new AtomicReference<>(); + try (PersistentServingKeyIndexCatalog failing = + PersistentServingKeyIndexCatalog.open(catalogRoot, stage -> { + observed.set(stage); + if (stage == failedStage) { + throw new IOException("injected after " + stage); + } + })) { + assertThrows(IOException.class, () -> failing.publish("generation-1", secondShadow, + reader(2, "generation-2"))); + assertEquals(failedStage, observed.get()); + } + + try (PersistentServingKeyIndexCatalog reopened = + PersistentServingKeyIndexCatalog.open(catalogRoot)) { + String expected = failedStage + == PersistentServingKeyIndexCatalog.PublicationStage.GENERATION_INSTALLED + ? "generation-1" : "generation-2"; + String retired = expected.equals("generation-1") ? "generation-2" : "generation-1"; + assertEquals(expected, reopened.getCurrentGenerationId()); + assertTrue(reopened.generationExists(expected)); + assertFalse(reopened.generationExists(retired)); + } + } + } + } + + private static PinnedHistory history(PersistentServingKeyIndexGeneration serving, + AtomicBoolean closed) { + return new PinnedHistory() { + @Override + public long getIndexedFrom() { + return serving.getIndexedFrom(); + } + + @Override + public long getIndexedThrough() { + return serving.getIndexedThrough(); + } + + @Override + public byte[] getHeadHash() { + return serving.getHeadHash(); + } + + @Override + public byte[] getAuthoritativePrefixDigest() { + return serving.getAuthoritativePrefixDigest(); + } + + @Override + public OldValue read(String dbName, byte[] rawKey, long firstChangeBlock) { + return OldValue.present(bytes("old")); + } + + @Override + public void close() { + closed.set(true); + } + }; + } + + private static PinnedLatestState failLatest( + PersistentServingKeyIndexGeneration serving) throws IOException { + throw new IOException("injected latest snapshot failure for " + serving.getGenerationId()); + } + + private static ArchiveProgressEnvelope reader(int epoch, String generationId) { + return reader(epoch, hash(epoch), generationId); + } + + private static ArchiveProgressEnvelope reader(int epoch, byte[] blockHash, + String generationId) { + byte[] batch = new byte[16]; + byte[] digest = new byte[32]; + byte[] encoded = bytes(generationId); + System.arraycopy(encoded, 0, batch, 0, Math.min(batch.length, encoded.length)); + return new ArchiveProgressEnvelope(ArchiveProgressEnvelope.Kind.READER_VISIBLE, null, epoch, + blockHash, batch, digest, PARTICIPANTS); + } + + private static PinnedLatestState latest(PersistentServingKeyIndexGeneration serving, + AtomicBoolean closed) { + return latest(serving, closed, serving.getLatestSourceIdentityDigest()); + } + + private static PinnedLatestState latest(PersistentServingKeyIndexGeneration serving, + AtomicBoolean closed, byte[] sourceIdentityDigest) { + return new PinnedLatestState() { + @Override + public long getBlockNumber() { + return serving.getIndexedThrough(); + } + + @Override + public byte[] getBlockHash() { + return serving.getHeadHash(); + } + + @Override + public byte[] getSourceIdentityDigest() { + return Arrays.copyOf(sourceIdentityDigest, sourceIdentityDigest.length); + } + + @Override + public OldValue get(String dbName, byte[] physicalRawKey) { + return OldValue.absent(); + } + + @Override + public List range(String dbName, byte[] lowerInclusive, + byte[] upperExclusive) { + return Collections.emptyList(); + } + + @Override + public void close() { + closed.set(true); + } + }; + } + + private static long change(ServingKeyIndex generation, String database, byte[] key, + long target, long upper) throws IOException { + OptionalLong changed = generation.firstChangeAfter(database, key, target, upper); + assertTrue(changed.isPresent()); + return changed.getAsLong(); + } + + private static KeyGroup group(String database, byte[]... keys) { + return new KeyGroup(database, Arrays.asList(keys)); + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static final class Fixture implements AutoCloseable { + private final HistoryIndexStore index; + private final List markers = new ArrayList<>(); + + private Fixture(Path directory) throws IOException { + index = new HistoryIndexStore(directory, new HistoryIndexCodec()); + } + + private void append(int block, KeyGroup... groups) throws IOException { + BlockSnapshotMeta meta = new BlockSnapshotMeta(block, block, hash(block), hash(block - 1), + block * 1_000L); + HistoryLocation body = new HistoryLocation(0, block * 100L, 80, block, hash(block)); + HistoryIndexLocation location = index.append( + new HistoryIndexRecord(meta, body, Arrays.asList(groups))); + markers.add(new HistoryCommitMarker(meta, block - 1L, body, location, new byte[16], + PARTICIPANTS)); + } + + private void sync() throws IOException { + index.sync(); + } + + @Override + public void close() throws IOException { + index.close(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index addf7a83ff6..7dfb7b09db1 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -366,7 +366,7 @@ public void archiveDurabilityFailurePreventsCheckpointAndRefresh() throws Except .when(sink).awaitCommitted(1L); assertThrows(TronError.class, manager::flush); - verify(sink).accept(prepared(database)); + verify(sink).acceptAll(Collections.singletonList(prepared(database))); verify(sink).awaitCommitted(1L); verify(checkpoint, never()).updateByBatch(any(Map.class)); manager.shutdown(); @@ -455,8 +455,8 @@ public void flushPublishesOnlyTheNonRevertibleRange() throws Exception { manager.flush(); - verify(sink).accept(first); - verify(sink, never()).accept(second); + verify(sink).acceptAll(Collections.singletonList(first)); + verify(sink, never()).acceptAll(Collections.singletonList(second)); verify(sink).awaitCommitted(1L); verify(sink).releaseThrough(1L); manager.shutdown(); From 2753e1f73883feef10d9d465d73d65de5b449250 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 19 Aug 2026 22:52:56 +0800 Subject: [PATCH 011/161] feat(chainbase): pin archive engine generations Add durable engine identities and native LevelDB/RocksDB snapshot leases. Assemble exact Store generations under the SnapshotManager barrier and release partial acquisitions on authority drift. --- .../storage/EngineSourceIdentityFile.java | 182 +++++++++ .../leveldb/LevelDbDataSourceImpl.java | 180 +++++++-- .../rocksdb/RocksDbDataSourceImpl.java | 151 ++++++- .../core/db2/archive/ArchiveStateBarrier.java | 16 + .../archive/LatestStateGenerationAdapter.java | 275 +++++++++++++ .../LatestStateGenerationCoordinator.java | 368 ++++++++++++++++++ ...testStateGenerationCoordinatorFactory.java | 72 ++++ .../org/tron/core/db2/common/LevelDB.java | 51 ++- .../org/tron/core/db2/common/RocksDB.java | 53 ++- .../tron/core/db2/core/SnapshotManager.java | 8 +- .../storage/EngineSourceIdentityFileTest.java | 85 ++++ .../leveldb/LevelDbDataSourceImplTest.java | 125 ++++++ .../rocksdb/RocksDbDataSourceImplTest.java | 124 ++++++ .../tron/core/db2/SnapshotManagerTest.java | 110 ++++++ .../LatestStateGenerationAdapterTest.java | 269 +++++++++++++ ...StateGenerationCoordinatorFactoryTest.java | 334 ++++++++++++++++ .../LatestStateGenerationCoordinatorTest.java | 345 ++++++++++++++++ 17 files changed, 2691 insertions(+), 57 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/common/storage/EngineSourceIdentityFile.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStateBarrier.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java create mode 100644 framework/src/test/java/org/tron/common/storage/EngineSourceIdentityFileTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationAdapterTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactoryTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorTest.java diff --git a/chainbase/src/main/java/org/tron/common/storage/EngineSourceIdentityFile.java b/chainbase/src/main/java/org/tron/common/storage/EngineSourceIdentityFile.java new file mode 100644 index 00000000000..c59b4d54baa --- /dev/null +++ b/chainbase/src/main/java/org/tron/common/storage/EngineSourceIdentityFile.java @@ -0,0 +1,182 @@ +package org.tron.common.storage; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.text.Normalizer; +import java.util.Arrays; +import java.util.Locale; +import java.util.UUID; + +/** Durable engine identity used to bind archive generations across process restarts. */ +public final class EngineSourceIdentityFile { + + private static final int MAGIC = 0x454e4749; // ENGI + private static final short VERSION = 1; + private static final int MAX_SIZE = 4096; + private static final String FILE_NAME = ".archive-engine.identity"; + private static final String LOCK_NAME = ".archive-engine.identity.lock"; + + private EngineSourceIdentityFile() { + } + + public static synchronized String loadOrCreate(Path databaseDirectory, String engine, + String dbName) + throws IOException { + if (databaseDirectory == null) { + throw new IllegalArgumentException("databaseDirectory must not be null"); + } + String normalizedEngine = normalizeEngine(engine); + String normalizedDbName = normalizeDbName(dbName); + Files.createDirectories(databaseDirectory); + Path identityFile = databaseDirectory.resolve(FILE_NAME); + try (FileChannel lockChannel = FileChannel.open(databaseDirectory.resolve(LOCK_NAME), + StandardOpenOption.CREATE, StandardOpenOption.WRITE); + FileLock ignored = lockChannel.lock()) { + if (!Files.exists(identityFile)) { + Identity created = new Identity(normalizedEngine, normalizedDbName, UUID.randomUUID()); + persistNew(databaseDirectory, identityFile, created); + } + } + Identity identity = load(identityFile); + if (!normalizedEngine.equals(identity.engine) || !normalizedDbName.equals(identity.dbName)) { + throw new IOException("Archive engine source identity does not match engine/dbName"); + } + return identity.engine.toLowerCase(Locale.ROOT) + ":" + identity.dbName + ":" + + identity.uuid; + } + + static Path identityPath(Path databaseDirectory) { + return databaseDirectory.resolve(FILE_NAME); + } + + private static void persistNew(Path directory, Path destination, Identity identity) + throws IOException { + byte[] encoded = encode(identity); + Path temporary = directory.resolve(FILE_NAME + ".tmp." + UUID.randomUUID()); + try { + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(encoded); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + try { + Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE); + } catch (FileAlreadyExistsException raced) { + // Another opener established the immutable identity first; validate it below. + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("Engine identity filesystem does not support atomic create", + unsupported); + } + syncDirectory(directory); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static Identity load(Path identityFile) throws IOException { + if (!Files.isRegularFile(identityFile)) { + throw new IOException("Archive engine source identity is missing or not a regular file"); + } + byte[] encoded = Files.readAllBytes(identityFile); + if (encoded.length < 32 || encoded.length > MAX_SIZE) { + throw new IOException("Archive engine source identity length is invalid"); + } + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(encoded, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new IOException("Archive engine source identity checksum mismatch"); + } + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0) { + throw new IOException("Unsupported archive engine source identity"); + } + String engine = normalizeEngine(input.readUTF()); + String dbName = normalizeDbName(input.readUTF()); + UUID uuid = new UUID(input.readLong(), input.readLong()); + if (input.available() != Integer.BYTES) { + throw new IOException("Archive engine source identity payload mismatch"); + } + return new Identity(engine, dbName, uuid); + } catch (IllegalArgumentException invalid) { + throw new IOException("Archive engine source identity fields are invalid", invalid); + } + } + + private static byte[] encode(Identity identity) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeUTF(identity.engine); + output.writeUTF(identity.dbName); + output.writeLong(identity.uuid.getMostSignificantBits()); + output.writeLong(identity.uuid.getLeastSignificantBits()); + output.flush(); + byte[] payload = bytes.toByteArray(); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected engine identity encoding failure", impossible); + } + } + + private static String normalizeEngine(String engine) { + if (engine == null) { + throw new IllegalArgumentException("engine must not be null"); + } + String normalized = engine.trim().toUpperCase(Locale.ROOT); + if (!"LEVELDB".equals(normalized) && !"ROCKSDB".equals(normalized)) { + throw new IllegalArgumentException("Unsupported archive engine identity: " + engine); + } + return normalized; + } + + private static String normalizeDbName(String dbName) { + if (dbName == null) { + throw new IllegalArgumentException("dbName must not be null"); + } + String normalized = Normalizer.normalize(dbName, Normalizer.Form.NFC); + if (normalized.isEmpty() || !normalized.equals(dbName)) { + throw new IllegalArgumentException("dbName must be non-empty canonical NFC"); + } + return normalized; + } + + private static void syncDirectory(Path directory) throws IOException { + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + private static final class Identity { + private final String engine; + private final String dbName; + private final UUID uuid; + + private Identity(String engine, String dbName, UUID uuid) { + this.engine = engine; + this.dbName = dbName; + this.uuid = uuid; + } + } +} diff --git a/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java b/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java index aa85ac08f45..1cc6522c0c5 100644 --- a/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java +++ b/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java @@ -35,8 +35,9 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.StampedLock; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -45,12 +46,14 @@ import org.iq80.leveldb.DB; import org.iq80.leveldb.DBIterator; import org.iq80.leveldb.Options; -import org.iq80.leveldb.ReadOptions; +import org.iq80.leveldb.ReadOptions; +import org.iq80.leveldb.Snapshot; import org.iq80.leveldb.WriteBatch; import org.iq80.leveldb.WriteOptions; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.parameter.CommonParameter; -import org.tron.common.storage.WriteOptionsWrapper; +import org.tron.common.storage.EngineSourceIdentityFile; +import org.tron.common.storage.WriteOptionsWrapper; import org.tron.common.storage.metric.DbStat; import org.tron.common.utils.FileUtil; import org.tron.common.utils.StorageUtils; @@ -75,8 +78,10 @@ public class LevelDbDataSourceImpl extends DbStat implements DbSourceInter participants; + private final Map stores; + private final Map sourceIdentities; + private final byte[] sourceIdentityDigest; + + public LatestStateGenerationAdapter(List participants, + Map stores) { + this.participants = validateParticipants(participants); + TreeMap sorted = new TreeMap<>( + Objects.requireNonNull(stores, "stores")); + if (!new ArrayList<>(sorted.keySet()).equals(this.participants) + || sorted.containsValue(null)) { + throw new IllegalArgumentException("Latest-state snapshot Store set mismatch"); + } + TreeMap identities = new TreeMap<>(); + for (Map.Entry entry : sorted.entrySet()) { + SnapshotCapableStore store = entry.getValue(); + if (!entry.getKey().equals(store.getDbName()) + || store.getSourceIdentity() == null || store.getSourceIdentity().isEmpty()) { + throw new IllegalArgumentException("Latest-state snapshot source identity is invalid"); + } + identities.put(entry.getKey(), store.getSourceIdentity()); + } + this.stores = Collections.unmodifiableMap(sorted); + this.sourceIdentities = Collections.unmodifiableMap(identities); + this.sourceIdentityDigest = sourceIdentityDigest(identities); + } + + /** + * Converts the current DB abstraction only when every Store explicitly implements the stable + * snapshot lifecycle capability. Ordinary {@code get()} is never accepted as a substitute. + */ + public static LatestStateGenerationAdapter fromDatabases(List participants, + Map> databases) throws ArchivePersistenceException { + TreeMap capable = new TreeMap<>(); + for (Map.Entry> entry : + Objects.requireNonNull(databases, "databases").entrySet()) { + if (!(entry.getValue() instanceof SnapshotCapableStore)) { + throw new ArchivePersistenceException( + "DB does not expose a stable snapshot lifecycle: " + entry.getKey()); + } + capable.put(entry.getKey(), (SnapshotCapableStore) entry.getValue()); + } + return new LatestStateGenerationAdapter(participants, capable); + } + + @Override + public PinnedLatestState pin(PersistentServingKeyIndexGeneration serving) throws IOException { + Objects.requireNonNull(serving, "serving"); + return pin(serving.getGenerationId(), serving.getIndexedThrough(), serving.getHeadHash(), + serving.getParticipatingDatabases()); + } + + PinnedLatestState pin(String generationId, long blockNumber, byte[] blockHash, + List expectedParticipants) throws IOException { + if (generationId == null || generationId.isEmpty() || blockNumber < 0 + || blockHash == null || blockHash.length != 32 + || !participants.equals(expectedParticipants)) { + throw new ArchivePersistenceException("Latest-state generation identity mismatch"); + } + TreeMap acquired = new TreeMap<>(); + try { + for (Map.Entry entry : stores.entrySet()) { + SnapshotCapableStore store = entry.getValue(); + String expectedSource = sourceIdentities.get(entry.getKey()); + if (!expectedSource.equals(store.getSourceIdentity())) { + throw new ArchivePersistenceException( + "Latest-state Store source was replaced before pin: " + entry.getKey()); + } + StoreSnapshot snapshot = Objects.requireNonNull( + store.pin(blockNumber, blockHash), "pinned Store snapshot"); + acquired.put(entry.getKey(), snapshot); + validateSnapshot(entry.getKey(), expectedSource, blockNumber, blockHash, snapshot); + } + return new PinnedGeneration(generationId, blockNumber, blockHash, acquired, + sourceIdentityDigest); + } catch (IOException | RuntimeException failure) { + closeAll(acquired, failure); + throw failure; + } + } + + public byte[] getSourceIdentityDigest() { + return Arrays.copyOf(sourceIdentityDigest, sourceIdentityDigest.length); + } + + private static void validateSnapshot(String dbName, String sourceIdentity, long blockNumber, + byte[] blockHash, StoreSnapshot snapshot) throws ArchivePersistenceException { + if (!dbName.equals(snapshot.getDbName()) + || !sourceIdentity.equals(snapshot.getSourceIdentity()) + || snapshot.getBlockNumber() != blockNumber + || !Arrays.equals(blockHash, snapshot.getBlockHash())) { + throw new ArchivePersistenceException( + "Pinned latest-state Store snapshot identity mismatch: " + dbName); + } + } + + private static List validateParticipants(List participants) { + List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); + if (copy.isEmpty()) { + throw new IllegalArgumentException("Latest-state participant set must not be empty"); + } + String previous = null; + for (String participant : copy) { + if (participant == null || participant.isEmpty() + || previous != null && previous.compareTo(participant) >= 0) { + throw new IllegalArgumentException( + "Latest-state participants must be non-empty, unique, and sorted"); + } + previous = participant; + } + return Collections.unmodifiableList(copy); + } + + private static byte[] sourceIdentityDigest(Map identities) { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException impossible) { + throw new IllegalStateException("SHA-256 is unavailable", impossible); + } + digest.update(ByteBuffer.allocate(Integer.BYTES).putInt(identities.size()).array()); + for (Map.Entry entry : identities.entrySet()) { + update(digest, entry.getKey()); + update(digest, entry.getValue()); + } + return digest.digest(); + } + + private static void update(MessageDigest digest, String value) { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + digest.update(ByteBuffer.allocate(Integer.BYTES).putInt(encoded.length).array()); + digest.update(encoded); + } + + private static void closeAll(Map snapshots, Exception failure) { + List reverse = new ArrayList<>(snapshots.values()); + Collections.reverse(reverse); + for (StoreSnapshot snapshot : reverse) { + try { + snapshot.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + } + + /** Minimum capability that RocksDB/LevelDB wrappers must implement before production pinning. */ + public interface SnapshotCapableStore { + String getDbName(); + + String getSourceIdentity(); + + StoreSnapshot pin(long blockNumber, byte[] blockHash) throws IOException; + } + + /** Stable point-read view whose lifetime prevents the underlying engine from being replaced. */ + public interface StoreSnapshot extends Closeable { + String getDbName(); + + String getSourceIdentity(); + + long getBlockNumber(); + + byte[] getBlockHash(); + + byte[] get(byte[] physicalRawKey) throws IOException; + } + + private static final class PinnedGeneration implements PinnedLatestState { + private final String generationId; + private final long blockNumber; + private final byte[] blockHash; + private final Map snapshots; + private final byte[] sourceIdentityDigest; + private boolean closed; + + private PinnedGeneration(String generationId, long blockNumber, byte[] blockHash, + Map snapshots, byte[] sourceIdentityDigest) { + this.generationId = generationId; + this.blockNumber = blockNumber; + this.blockHash = Arrays.copyOf(blockHash, blockHash.length); + this.snapshots = Collections.unmodifiableMap(new TreeMap<>(snapshots)); + this.sourceIdentityDigest = Arrays.copyOf(sourceIdentityDigest, + sourceIdentityDigest.length); + } + + @Override + public long getBlockNumber() { + return blockNumber; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + @Override + public byte[] getSourceIdentityDigest() { + return Arrays.copyOf(sourceIdentityDigest, sourceIdentityDigest.length); + } + + @Override + public synchronized OldValue get(String dbName, byte[] physicalRawKey) throws IOException { + ensureOpen(); + StoreSnapshot snapshot = snapshots.get(dbName); + if (snapshot == null) { + throw new ArchivePersistenceException( + "Database is outside pinned latest generation: " + dbName); + } + return OldValue.fromNullable(snapshot.get(physicalRawKey)); + } + + @Override + public List range(String dbName, byte[] lowerInclusive, + byte[] upperExclusive) { + throw new UnsupportedOperationException( + "Latest-generation range is outside the Phase 1 point-query scope"); + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + IOException failure = null; + List reverse = new ArrayList<>(snapshots.values()); + Collections.reverse(reverse); + for (StoreSnapshot snapshot : reverse) { + try { + snapshot.close(); + } catch (IOException closeFailure) { + if (failure == null) { + failure = closeFailure; + } else { + failure.addSuppressed(closeFailure); + } + } + } + if (failure != null) { + throw failure; + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException( + "Pinned latest-state generation is closed: " + generationId); + } + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java new file mode 100644 index 00000000000..6037bdcb022 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java @@ -0,0 +1,368 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestStateFactory; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; + +/** Two-phase publisher for a latest-state generation acquired under one global state barrier. */ +public final class LatestStateGenerationCoordinator + implements PinnedLatestStateFactory, Closeable { + + private final List participants; + private final Map stores; + private final ArchiveStateBarrier barrier; + private final AuthorityReader authorityReader; + private final List retired = new ArrayList<>(); + private PublishedGeneration current; + private boolean closed; + + public LatestStateGenerationCoordinator(List participants, + Map stores, ArchiveStateBarrier barrier, + AuthorityReader authorityReader) { + this.participants = validateParticipants(participants); + TreeMap sorted = new TreeMap<>( + Objects.requireNonNull(stores, "stores")); + if (!new ArrayList<>(sorted.keySet()).equals(this.participants) + || sorted.containsValue(null)) { + throw new IllegalArgumentException("Latest-state coordinator Store set mismatch"); + } + this.stores = Collections.unmodifiableMap(sorted); + this.barrier = Objects.requireNonNull(barrier, "barrier"); + this.authorityReader = Objects.requireNonNull(authorityReader, "authorityReader"); + } + + /** Acquires a complete immutable candidate while the caller-supplied global barrier is held. */ + public synchronized Candidate acquire(String generationId) throws IOException { + ensureOpen(); + if (generationId == null || generationId.isEmpty()) { + throw new IllegalArgumentException("Latest-state generation id must not be empty"); + } + Acquisition acquisition = new Acquisition(); + try { + barrier.run(() -> { + ArchiveProgressEnvelope before = readAuthority(); + LatestStateGenerationAdapter adapter = new LatestStateGenerationAdapter(participants, + stores); + acquisition.pinned = adapter.pin(generationId, before.getEpoch(), before.getBlockHash(), + participants); + ArchiveProgressEnvelope after = readAuthority(); + if (!sameAuthority(before, after)) { + throw new ArchivePersistenceException( + "Reader-visible authority drifted while latest generation was pinned"); + } + acquisition.authority = before; + acquisition.sourceIdentityDigest = adapter.getSourceIdentityDigest(); + }); + } catch (IOException | RuntimeException failure) { + if (acquisition.pinned != null) { + try { + acquisition.pinned.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + throw failure; + } + return new Candidate(this, generationId, acquisition.authority, + acquisition.sourceIdentityDigest, acquisition.pinned); + } + + /** CAS-publishes a fully acquired candidate after its digest has been bound into serving data. */ + public synchronized boolean publish(String expectedGenerationId, Candidate candidate, + PersistentServingKeyIndexGeneration serving) throws IOException { + ensureOpen(); + Objects.requireNonNull(candidate, "candidate"); + Objects.requireNonNull(serving, "serving"); + String currentId = current == null ? null : current.generationId; + if (!Objects.equals(expectedGenerationId, currentId)) { + return false; + } + candidate.validateOwner(this); + candidate.validateServing(serving); + PublishedGeneration replacement = candidate.transfer(); + PublishedGeneration previous = current; + current = replacement; + if (previous != null) { + previous.retired = true; + retired.add(previous); + reap(previous); + } + return true; + } + + @Override + public synchronized PinnedLatestState pin(PersistentServingKeyIndexGeneration serving) + throws IOException { + Objects.requireNonNull(serving, "serving"); + return pin(serving.getGenerationId(), serving.getIndexedThrough(), serving.getHeadHash(), + serving.getLatestSourceIdentityDigest(), serving.getParticipatingDatabases()); + } + + synchronized PinnedLatestState pin(String generationId, long blockNumber, byte[] blockHash, + byte[] sourceIdentityDigest, List expectedParticipants) throws IOException { + ensureOpen(); + if (current == null || !current.generationId.equals(generationId) + || current.blockNumber != blockNumber || !Arrays.equals(current.blockHash, blockHash) + || !Arrays.equals(current.sourceIdentityDigest, sourceIdentityDigest) + || !participants.equals(expectedParticipants)) { + throw new ArchivePersistenceException("Published latest generation identity mismatch"); + } + current.references++; + return new RequestPin(this, current); + } + + public synchronized String getCurrentGenerationId() { + return current == null ? null : current.generationId; + } + + synchronized int getReferenceCount(String generationId) { + if (current != null && current.generationId.equals(generationId)) { + return current.references; + } + return 0; + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + if ((current != null && current.references != 0) + || retired.stream().anyMatch(generation -> generation.references != 0)) { + throw new IOException("Cannot close latest generation coordinator with pinned readers"); + } + closed = true; + if (current != null) { + current.root.close(); + current = null; + } + for (PublishedGeneration generation : new ArrayList<>(retired)) { + if (!generation.closed) { + generation.closed = true; + generation.root.close(); + } + } + retired.clear(); + } + + private ArchiveProgressEnvelope readAuthority() throws IOException { + ArchiveProgressEnvelope authority = Objects.requireNonNull(authorityReader.read(), + "reader-visible authority"); + if (authority.getKind() != Kind.READER_VISIBLE + || !participants.equals(authority.getParticipants())) { + throw new ArchivePersistenceException("Invalid reader-visible generation authority"); + } + return authority; + } + + private synchronized void release(PublishedGeneration generation) throws IOException { + if (generation.references <= 0) { + throw new IllegalStateException("Latest generation reference count underflow"); + } + generation.references--; + reap(generation); + } + + private void reap(PublishedGeneration generation) throws IOException { + if (generation.retired && generation.references == 0 && !generation.closed) { + generation.closed = true; + generation.root.close(); + retired.remove(generation); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Latest generation coordinator is closed"); + } + } + + private static boolean sameAuthority(ArchiveProgressEnvelope left, + ArchiveProgressEnvelope right) { + return left.getKind() == right.getKind() && left.getEpoch() == right.getEpoch() + && Arrays.equals(left.getBlockHash(), right.getBlockHash()) + && Arrays.equals(left.getBatchId(), right.getBatchId()) + && Arrays.equals(left.getPayloadDigest(), right.getPayloadDigest()) + && left.getParticipants().equals(right.getParticipants()); + } + + private static List validateParticipants(List participants) { + List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); + if (copy.isEmpty()) { + throw new IllegalArgumentException("Latest-state participant set must not be empty"); + } + String previous = null; + for (String participant : copy) { + if (participant == null || participant.isEmpty() + || previous != null && previous.compareTo(participant) >= 0) { + throw new IllegalArgumentException( + "Latest-state participants must be non-empty, unique, and sorted"); + } + previous = participant; + } + return Collections.unmodifiableList(copy); + } + + @FunctionalInterface + public interface AuthorityReader { + ArchiveProgressEnvelope read() throws IOException; + } + + private static final class Acquisition { + private ArchiveProgressEnvelope authority; + private byte[] sourceIdentityDigest; + private PinnedLatestState pinned; + } + + /** Acquired native snapshots owned by the caller until successful publication. */ + public static final class Candidate implements Closeable { + private final LatestStateGenerationCoordinator owner; + private final String generationId; + private final ArchiveProgressEnvelope authority; + private final byte[] sourceIdentityDigest; + private PinnedLatestState root; + private boolean published; + + private Candidate(LatestStateGenerationCoordinator owner, String generationId, + ArchiveProgressEnvelope authority, byte[] sourceIdentityDigest, PinnedLatestState root) { + this.owner = owner; + this.generationId = generationId; + this.authority = authority; + this.sourceIdentityDigest = Arrays.copyOf(sourceIdentityDigest, + sourceIdentityDigest.length); + this.root = root; + } + + public long getBlockNumber() { + return authority.getEpoch(); + } + + public byte[] getBlockHash() { + return authority.getBlockHash(); + } + + public byte[] getSourceIdentityDigest() { + return Arrays.copyOf(sourceIdentityDigest, sourceIdentityDigest.length); + } + + @Override + public synchronized void close() throws IOException { + if (!published && root != null) { + PinnedLatestState releasing = root; + root = null; + releasing.close(); + } + } + + private synchronized void validateOwner(LatestStateGenerationCoordinator expected) { + if (owner != expected || published || root == null) { + throw new IllegalStateException("Latest generation candidate is not publishable"); + } + } + + private synchronized void validateServing(PersistentServingKeyIndexGeneration serving) { + if (!generationId.equals(serving.getGenerationId()) + || authority.getEpoch() != serving.getIndexedThrough() + || !Arrays.equals(authority.getBlockHash(), serving.getHeadHash()) + || !Arrays.equals(sourceIdentityDigest, serving.getLatestSourceIdentityDigest()) + || !authority.getParticipants().equals(serving.getParticipatingDatabases())) { + throw new IllegalArgumentException( + "Serving generation does not match latest-state candidate"); + } + } + + private synchronized PublishedGeneration transfer() { + published = true; + PublishedGeneration result = new PublishedGeneration(generationId, authority.getEpoch(), + authority.getBlockHash(), sourceIdentityDigest, root); + root = null; + return result; + } + } + + private static final class PublishedGeneration { + private final String generationId; + private final long blockNumber; + private final byte[] blockHash; + private final byte[] sourceIdentityDigest; + private final PinnedLatestState root; + private int references; + private boolean retired; + private boolean closed; + + private PublishedGeneration(String generationId, long blockNumber, byte[] blockHash, + byte[] sourceIdentityDigest, PinnedLatestState root) { + this.generationId = generationId; + this.blockNumber = blockNumber; + this.blockHash = Arrays.copyOf(blockHash, blockHash.length); + this.sourceIdentityDigest = Arrays.copyOf(sourceIdentityDigest, + sourceIdentityDigest.length); + this.root = root; + } + } + + private static final class RequestPin implements PinnedLatestState { + private final LatestStateGenerationCoordinator owner; + private final PublishedGeneration generation; + private boolean closed; + + private RequestPin(LatestStateGenerationCoordinator owner, + PublishedGeneration generation) { + this.owner = owner; + this.generation = generation; + } + + @Override + public long getBlockNumber() { + return generation.blockNumber; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(generation.blockHash, generation.blockHash.length); + } + + @Override + public byte[] getSourceIdentityDigest() { + return Arrays.copyOf(generation.sourceIdentityDigest, + generation.sourceIdentityDigest.length); + } + + @Override + public synchronized OldValue get(String dbName, byte[] physicalRawKey) throws IOException { + ensureOpen(); + return generation.root.get(dbName, physicalRawKey); + } + + @Override + public List range(String dbName, byte[] lowerInclusive, + byte[] upperExclusive) { + throw new UnsupportedOperationException( + "Latest-generation range is outside the Phase 1 point-query scope"); + } + + @Override + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + owner.release(generation); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Pinned latest generation is closed"); + } + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java new file mode 100644 index 00000000000..99117720c67 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java @@ -0,0 +1,72 @@ +package org.tron.core.db2.archive; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.TreeSet; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; +import org.tron.core.db2.common.DB; +import org.tron.core.db2.common.LevelDB; +import org.tron.core.db2.common.RocksDB; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.Snapshot; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.core.SnapshotRoot; + +/** Builds a latest-state coordinator from one frozen SnapshotManager Store registry. */ +public final class LatestStateGenerationCoordinatorFactory { + + private LatestStateGenerationCoordinatorFactory() { + } + + public static LatestStateGenerationCoordinator create(SnapshotManager manager, + Path readerVisiblePath) throws ArchivePersistenceException { + Objects.requireNonNull(manager, "manager"); + Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); + List registered = new ArrayList<>(manager.getDbs()); + try { + ArchiveStoreScope.validate(registered); + } catch (IllegalStateException invalid) { + throw new ArchivePersistenceException("Invalid SnapshotManager archive Store registry", + invalid); + } + + TreeMap stateDatabases = new TreeMap<>(); + for (Chainbase database : registered) { + if (ArchiveStoreScope.isStateDatabase(database.getDbName())) { + stateDatabases.put(database.getDbName(), database); + } + } + TreeSet expected = new TreeSet<>(ArchiveStoreScope.getStateDatabases()); + if (!stateDatabases.keySet().equals(expected)) { + throw new ArchivePersistenceException( + "SnapshotManager archive state Store set is incomplete or unexpected"); + } + + TreeMap stores = new TreeMap<>(); + for (Map.Entry entry : stateDatabases.entrySet()) { + Snapshot root = entry.getValue().getHead().getRoot(); + if (!Snapshot.isRoot(root)) { + throw new ArchivePersistenceException( + "Archive state Store does not resolve to SnapshotRoot: " + entry.getKey()); + } + DB engine = ((SnapshotRoot) root).getDb(); + if (!(engine instanceof LevelDB || engine instanceof RocksDB) + || !(engine instanceof SnapshotCapableStore) + || !entry.getKey().equals(engine.getDbName())) { + throw new ArchivePersistenceException( + "Archive state Store root lacks a supported snapshot engine: " + entry.getKey()); + } + stores.put(entry.getKey(), (SnapshotCapableStore) engine); + } + + List participants = new ArrayList<>(stores.keySet()); + ArchiveProgressFile readerVisible = new ArchiveProgressFile(readerVisiblePath, + new ArchiveProgressEnvelopeCodec()); + return new LatestStateGenerationCoordinator(participants, stores, + manager::withArchiveStateBarrier, readerVisible::load); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java b/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java index 5942bb7444c..f97e73546c1 100644 --- a/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java +++ b/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java @@ -1,6 +1,8 @@ package org.tron.core.db2.common; import com.google.common.collect.Maps; +import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import lombok.Getter; @@ -8,8 +10,10 @@ import org.tron.common.storage.WriteOptionsWrapper; import org.tron.common.storage.leveldb.LevelDbDataSourceImpl; import org.tron.core.db.common.iterator.DBIterator; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; -public class LevelDB implements DB, Flusher { +public class LevelDB implements DB, Flusher, SnapshotCapableStore { @Getter private LevelDbDataSourceImpl db; @@ -50,6 +54,51 @@ public String getDbName() { return db.getDBName(); } + @Override + public String getSourceIdentity() { + return db.getSnapshotSourceIdentity(); + } + + @Override + public StoreSnapshot pin(long blockNumber, byte[] blockHash) { + if (blockNumber < 0 || blockHash == null || blockHash.length != 32) { + throw new IllegalArgumentException("Invalid LevelDB snapshot block identity"); + } + LevelDbDataSourceImpl.PinnedSnapshot pinned = db.pinSnapshot(); + byte[] expectedHash = Arrays.copyOf(blockHash, blockHash.length); + return new StoreSnapshot() { + @Override + public String getDbName() { + return LevelDB.this.getDbName(); + } + + @Override + public String getSourceIdentity() { + return pinned.getSourceIdentity(); + } + + @Override + public long getBlockNumber() { + return blockNumber; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(expectedHash, expectedHash.length); + } + + @Override + public byte[] get(byte[] physicalRawKey) { + return pinned.get(physicalRawKey); + } + + @Override + public void close() throws IOException { + pinned.close(); + } + }; + } + @Override public DBIterator iterator() { return db.iterator(); diff --git a/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java b/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java index 1d67438eceb..cd88f34ae65 100644 --- a/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java +++ b/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java @@ -1,6 +1,8 @@ package org.tron.core.db2.common; import com.google.common.collect.Maps; +import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import lombok.Getter; @@ -8,8 +10,10 @@ import org.tron.common.storage.WriteOptionsWrapper; import org.tron.common.storage.rocksdb.RocksDbDataSourceImpl; import org.tron.core.db.common.iterator.DBIterator; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; -public class RocksDB implements DB, Flusher { +public class RocksDB implements DB, Flusher, SnapshotCapableStore { @Getter private RocksDbDataSourceImpl db; @@ -51,6 +55,51 @@ public String getDbName() { return db.getDBName(); } + @Override + public String getSourceIdentity() { + return db.getSnapshotSourceIdentity(); + } + + @Override + public StoreSnapshot pin(long blockNumber, byte[] blockHash) { + if (blockNumber < 0 || blockHash == null || blockHash.length != 32) { + throw new IllegalArgumentException("Invalid RocksDB snapshot block identity"); + } + RocksDbDataSourceImpl.PinnedSnapshot pinned = db.pinSnapshot(); + byte[] expectedHash = Arrays.copyOf(blockHash, blockHash.length); + return new StoreSnapshot() { + @Override + public String getDbName() { + return RocksDB.this.getDbName(); + } + + @Override + public String getSourceIdentity() { + return pinned.getSourceIdentity(); + } + + @Override + public long getBlockNumber() { + return blockNumber; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(expectedHash, expectedHash.length); + } + + @Override + public byte[] get(byte[] physicalRawKey) { + return pinned.get(physicalRawKey); + } + + @Override + public void close() throws IOException { + pinned.close(); + } + }; + } + @Override public DBIterator iterator() { return db.iterator(); @@ -84,4 +133,4 @@ public DB newInstance() { public void stat() { this.db.stat(); } -} \ No newline at end of file +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index 38f34307b1f..d90917a1c84 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -37,6 +37,7 @@ import org.tron.core.db.TronDatabase; import org.tron.core.db2.ISession; import org.tron.core.db2.archive.ArchiveStoreScope; +import org.tron.core.db2.archive.ArchiveStateBarrier.ArchiveStateAction; import org.tron.core.db2.archive.BlockChangeView; import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockReverseDiffSink; @@ -181,7 +182,7 @@ private void retreat() { --size; } - public void merge() { + public synchronized void merge() { if (activeSession <= 0) { throw new RevokingStoreIllegalStateException(activeSession); } @@ -312,6 +313,11 @@ public synchronized void installArchiveCollector(OldValueCollector collector, blockReverseDiffSink = Objects.requireNonNull(sink, "sink"); } + /** Runs latest-state snapshot acquisition inside the canonical apply/flush monitor. */ + public synchronized void withArchiveStateBarrier(ArchiveStateAction action) throws IOException { + Objects.requireNonNull(action, "action").run(); + } + public void markArchiveReadableThrough(long epoch) { archiveReadableEpoch = epoch; } diff --git a/framework/src/test/java/org/tron/common/storage/EngineSourceIdentityFileTest.java b/framework/src/test/java/org/tron/common/storage/EngineSourceIdentityFileTest.java new file mode 100644 index 00000000000..32349d96417 --- /dev/null +++ b/framework/src/test/java/org/tron/common/storage/EngineSourceIdentityFileTest.java @@ -0,0 +1,85 @@ +package org.tron.common.storage; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class EngineSourceIdentityFileTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void identityIsStableAcrossReopenAndBoundToEngineAndDbName() throws Exception { + Path database = temporaryFolder.newFolder("stable").toPath(); + String first = EngineSourceIdentityFile.loadOrCreate(database, "rocksdb", "account"); + String reopened = EngineSourceIdentityFile.loadOrCreate(database, "ROCKSDB", "account"); + + assertEquals(first, reopened); + assertTrue(first.startsWith("rocksdb:account:")); + assertThrows(IOException.class, + () -> EngineSourceIdentityFile.loadOrCreate(database, "LEVELDB", "account")); + assertThrows(IOException.class, + () -> EngineSourceIdentityFile.loadOrCreate(database, "ROCKSDB", "properties")); + } + + @Test + public void missingIdentityCreatesANewDurableUuid() throws Exception { + Path database = temporaryFolder.newFolder("recreated").toPath(); + String first = EngineSourceIdentityFile.loadOrCreate(database, "LEVELDB", "account"); + Files.delete(EngineSourceIdentityFile.identityPath(database)); + String recreated = EngineSourceIdentityFile.loadOrCreate(database, "LEVELDB", "account"); + + assertNotEquals(first, recreated); + assertEquals(recreated, + EngineSourceIdentityFile.loadOrCreate(database, "LEVELDB", "account")); + } + + @Test + public void corruptionFailsClosedWithoutReplacingIdentity() throws Exception { + Path database = temporaryFolder.newFolder("corrupt").toPath(); + EngineSourceIdentityFile.loadOrCreate(database, "ROCKSDB", "account"); + Path identity = EngineSourceIdentityFile.identityPath(database); + byte[] corrupt = Files.readAllBytes(identity); + corrupt[corrupt.length - 1] ^= 1; + Files.write(identity, corrupt); + + assertThrows(IOException.class, + () -> EngineSourceIdentityFile.loadOrCreate(database, "ROCKSDB", "account")); + assertEquals(corrupt.length, Files.size(identity)); + } + + @Test + public void concurrentOpenersEstablishOneIdentity() throws Exception { + Path database = temporaryFolder.newFolder("concurrent").toPath(); + ExecutorService executor = Executors.newFixedThreadPool(8); + Set identities = ConcurrentHashMap.newKeySet(); + try { + Future[] opens = new Future[8]; + for (int i = 0; i < opens.length; i++) { + opens[i] = executor.submit(() -> identities.add( + EngineSourceIdentityFile.loadOrCreate(database, "ROCKSDB", "account"))); + } + for (Future open : opens) { + open.get(); + } + } finally { + executor.shutdownNow(); + } + + assertEquals(1, identities.size()); + } +} diff --git a/framework/src/test/java/org/tron/common/storage/leveldb/LevelDbDataSourceImplTest.java b/framework/src/test/java/org/tron/common/storage/leveldb/LevelDbDataSourceImplTest.java index 41e8749e1ec..7c0d6b12391 100644 --- a/framework/src/test/java/org/tron/common/storage/leveldb/LevelDbDataSourceImplTest.java +++ b/framework/src/test/java/org/tron/common/storage/leveldb/LevelDbDataSourceImplTest.java @@ -18,7 +18,9 @@ package org.tron.common.storage.leveldb; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -29,9 +31,15 @@ import ch.qos.logback.core.read.ListAppender; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import org.junit.AfterClass; import org.junit.Assert; @@ -51,6 +59,8 @@ import org.tron.common.utils.ReflectUtils; import org.tron.common.utils.StorageUtils; import org.tron.core.config.args.Args; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; import org.tron.core.exception.TronError; /** @@ -189,4 +199,119 @@ public void fastOpen() { dbLogger.detachAppender(dbAppender); } } + + @Test + public void nativeSnapshotKeepsOldValueAfterLiveWrite() throws Exception { + LevelDbDataSourceImpl dataSource = new LevelDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), "nativeSnapshotKeepsOldValue"); + dataSource.putData(key1, value1); + try (LevelDbDataSourceImpl.PinnedSnapshot snapshot = dataSource.pinSnapshot()) { + dataSource.putData(key1, "replacement".getBytes()); + assertArrayEquals(value1, snapshot.get(key1)); + assertEquals(dataSource.getSnapshotSourceIdentity(), snapshot.getSourceIdentity()); + } + dataSource.closeDB(); + } + + @Test + public void levelDbWrapperExposesSnapshotCapability() throws Exception { + LevelDbDataSourceImpl dataSource = new LevelDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), "wrapperSnapshotCapability"); + org.tron.core.db2.common.LevelDB wrapper = new org.tron.core.db2.common.LevelDB(dataSource); + wrapper.put(key1, value1); + assertTrue(wrapper instanceof SnapshotCapableStore); + try (StoreSnapshot snapshot = wrapper.pin(1, new byte[32])) { + wrapper.put(key1, "20000".getBytes()); + assertArrayEquals(value1, snapshot.get(key1)); + assertEquals(wrapper.getSourceIdentity(), snapshot.getSourceIdentity()); + } finally { + wrapper.close(); + } + } + + @Test + public void closeWaitsForCrossThreadSnapshotRelease() throws Exception { + LevelDbDataSourceImpl dataSource = new LevelDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), "closeWaitsForSnapshot"); + LevelDbDataSourceImpl.PinnedSnapshot snapshot = dataSource.pinSnapshot(); + ExecutorService executor = Executors.newFixedThreadPool(2); + CountDownLatch closeStarted = new CountDownLatch(1); + try { + Future close = executor.submit(() -> { + closeStarted.countDown(); + dataSource.closeDB(); + }); + assertTrue(closeStarted.await(5, TimeUnit.SECONDS)); + assertThrows(TimeoutException.class, () -> close.get(100, TimeUnit.MILLISECONDS)); + executor.submit(() -> { + snapshot.close(); + return null; + }).get(5, TimeUnit.SECONDS); + close.get(5, TimeUnit.SECONDS); + } finally { + snapshot.close(); + executor.shutdownNow(); + dataSource.closeDB(); + } + } + + @Test + public void resetWaitsForPinAndChangesEngineSourceIdentity() throws Exception { + LevelDbDataSourceImpl dataSource = new LevelDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), "resetWaitsForSnapshot"); + String originalIdentity = dataSource.getSnapshotSourceIdentity(); + LevelDbDataSourceImpl.PinnedSnapshot snapshot = dataSource.pinSnapshot(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + CountDownLatch resetStarted = new CountDownLatch(1); + try { + Future reset = executor.submit(() -> { + resetStarted.countDown(); + dataSource.resetDb(); + }); + assertTrue(resetStarted.await(5, TimeUnit.SECONDS)); + assertThrows(TimeoutException.class, () -> reset.get(100, TimeUnit.MILLISECONDS)); + snapshot.close(); + reset.get(5, TimeUnit.SECONDS); + assertNotEquals(originalIdentity, dataSource.getSnapshotSourceIdentity()); + assertThrows(IllegalStateException.class, () -> snapshot.get(key1)); + } finally { + snapshot.close(); + executor.shutdownNow(); + dataSource.closeDB(); + } + } + + @Test + public void sourceIdentityPersistsAcrossProcessStyleReopen() { + String name = "sourceIdentityPersistsAcrossReopen"; + LevelDbDataSourceImpl first = new LevelDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), name); + String identity = first.getSnapshotSourceIdentity(); + first.closeDB(); + + LevelDbDataSourceImpl reopened = new LevelDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), name); + try { + assertEquals(identity, reopened.getSnapshotSourceIdentity()); + } finally { + reopened.closeDB(); + } + } + + @Test + public void corruptSourceIdentityFailsBeforeDatabaseReopen() throws IOException { + String name = "corruptSourceIdentityFailsClosed"; + LevelDbDataSourceImpl first = new LevelDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), name); + Path identityPath = first.getDbPath().resolve(".archive-engine.identity"); + first.closeDB(); + + byte[] corrupted = Files.readAllBytes(identityPath); + corrupted[corrupted.length - 1] ^= 1; + Files.write(identityPath, corrupted); + + assertThrows(TronError.class, () -> new LevelDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), name)); + assertArrayEquals(corrupted, Files.readAllBytes(identityPath)); + } } diff --git a/framework/src/test/java/org/tron/common/storage/rocksdb/RocksDbDataSourceImplTest.java b/framework/src/test/java/org/tron/common/storage/rocksdb/RocksDbDataSourceImplTest.java index b0f13eb9154..02fce193c0a 100644 --- a/framework/src/test/java/org/tron/common/storage/rocksdb/RocksDbDataSourceImplTest.java +++ b/framework/src/test/java/org/tron/common/storage/rocksdb/RocksDbDataSourceImplTest.java @@ -1,14 +1,24 @@ package org.tron.common.storage.rocksdb; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.tron.common.TestConstants.TEST_CONF; import static org.tron.common.TestConstants.assumeLevelDbAvailable; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; @@ -24,6 +34,8 @@ import org.tron.common.utils.PropUtil; import org.tron.common.utils.StorageUtils; import org.tron.core.config.args.Args; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; import org.tron.core.exception.TronError; /** @@ -139,6 +151,118 @@ public void backupAndDelete() throws RocksDBException { dataSource.closeDB(); } + @Test + public void nativeSnapshotKeepsOldValueAfterLiveWrite() { + RocksDbDataSourceImpl dataSource = new RocksDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), "nativeSnapshotKeepsOldValue"); + dataSource.putData(key1, value1); + try (RocksDbDataSourceImpl.PinnedSnapshot snapshot = dataSource.pinSnapshot()) { + dataSource.putData(key1, "replacement".getBytes()); + assertArrayEquals(value1, snapshot.get(key1)); + assertEquals(dataSource.getSnapshotSourceIdentity(), snapshot.getSourceIdentity()); + } + dataSource.closeDB(); + } + + @Test + public void rocksDbWrapperExposesSnapshotCapability() throws Exception { + RocksDbDataSourceImpl dataSource = new RocksDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), "wrapperSnapshotCapability"); + org.tron.core.db2.common.RocksDB wrapper = new org.tron.core.db2.common.RocksDB(dataSource); + wrapper.put(key1, value1); + assertTrue(wrapper instanceof SnapshotCapableStore); + try (StoreSnapshot snapshot = wrapper.pin(1, new byte[32])) { + wrapper.put(key1, "20000".getBytes()); + assertArrayEquals(value1, snapshot.get(key1)); + assertEquals(wrapper.getSourceIdentity(), snapshot.getSourceIdentity()); + } finally { + wrapper.close(); + } + } + + @Test + public void closeWaitsForCrossThreadSnapshotRelease() throws Exception { + RocksDbDataSourceImpl dataSource = new RocksDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), "closeWaitsForSnapshot"); + RocksDbDataSourceImpl.PinnedSnapshot snapshot = dataSource.pinSnapshot(); + ExecutorService executor = Executors.newFixedThreadPool(2); + CountDownLatch closeStarted = new CountDownLatch(1); + try { + Future close = executor.submit(() -> { + closeStarted.countDown(); + dataSource.closeDB(); + }); + assertTrue(closeStarted.await(5, TimeUnit.SECONDS)); + assertThrows(TimeoutException.class, () -> close.get(100, TimeUnit.MILLISECONDS)); + executor.submit(snapshot::close).get(5, TimeUnit.SECONDS); + close.get(5, TimeUnit.SECONDS); + } finally { + snapshot.close(); + executor.shutdownNow(); + dataSource.closeDB(); + } + } + + @Test + public void resetWaitsForPinAndChangesEngineSourceIdentity() throws Exception { + RocksDbDataSourceImpl dataSource = new RocksDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), "resetWaitsForSnapshot"); + String originalIdentity = dataSource.getSnapshotSourceIdentity(); + RocksDbDataSourceImpl.PinnedSnapshot snapshot = dataSource.pinSnapshot(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + CountDownLatch resetStarted = new CountDownLatch(1); + try { + Future reset = executor.submit(() -> { + resetStarted.countDown(); + dataSource.resetDb(); + }); + assertTrue(resetStarted.await(5, TimeUnit.SECONDS)); + assertThrows(TimeoutException.class, () -> reset.get(100, TimeUnit.MILLISECONDS)); + snapshot.close(); + reset.get(5, TimeUnit.SECONDS); + assertNotEquals(originalIdentity, dataSource.getSnapshotSourceIdentity()); + assertThrows(IllegalStateException.class, () -> snapshot.get(key1)); + } finally { + snapshot.close(); + executor.shutdownNow(); + dataSource.closeDB(); + } + } + + @Test + public void sourceIdentityPersistsAcrossProcessStyleReopen() { + String name = "sourceIdentityPersistsAcrossReopen"; + RocksDbDataSourceImpl first = new RocksDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), name); + String identity = first.getSnapshotSourceIdentity(); + first.closeDB(); + + RocksDbDataSourceImpl reopened = new RocksDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), name); + try { + assertEquals(identity, reopened.getSnapshotSourceIdentity()); + } finally { + reopened.closeDB(); + } + } + + @Test + public void corruptSourceIdentityFailsBeforeDatabaseReopen() throws IOException { + String name = "corruptSourceIdentityFailsClosed"; + RocksDbDataSourceImpl first = new RocksDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), name); + Path identityPath = first.getDbPath().resolve(".archive-engine.identity"); + first.closeDB(); + + byte[] corrupted = Files.readAllBytes(identityPath); + corrupted[corrupted.length - 1] ^= 1; + Files.write(identityPath, corrupted); + + assertThrows(RuntimeException.class, () -> new RocksDbDataSourceImpl( + Args.getInstance().getOutputDirectory(), name)); + assertArrayEquals(corrupted, Files.readAllBytes(identityPath)); + } + private void makeExceptionDb(String dbName) { RocksDbDataSourceImpl dataSource = new RocksDbDataSourceImpl( Args.getInstance().getOutputDirectory(), "test_initDb"); diff --git a/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java b/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java index 8830c472e8e..d2939aaad59 100644 --- a/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java +++ b/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java @@ -6,9 +6,16 @@ import com.google.common.collect.Maps; import com.google.common.primitives.Longs; import com.google.protobuf.ByteString; +import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; @@ -140,4 +147,107 @@ public void testFlushError() { TronError thrown = Assert.assertThrows(TronError.class, manager::flush); Assert.assertEquals(TronError.ErrCode.DB_FLUSH, thrown.getErrCode()); } + + @Test + public void archiveStateBarrierBlocksSessionAdvanceAndFlush() throws Exception { + SnapshotManager manager = new SnapshotManager(""); + manager.enable(); + ExecutorService executor = Executors.newFixedThreadPool(3); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + ISession acquired = null; + try { + Future barrier = executor.submit(() -> { + manager.withArchiveStateBarrier(() -> { + entered.countDown(); + try { + if (!release.await(5, TimeUnit.SECONDS)) { + throw new IOException("Timed out waiting to release archive state barrier"); + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted inside archive state barrier", interrupted); + } + }); + return null; + }); + Assert.assertTrue(entered.await(5, TimeUnit.SECONDS)); + + Future session = executor.submit(() -> manager.buildSession()); + Future flush = executor.submit(manager::flush); + Assert.assertThrows(TimeoutException.class, + () -> session.get(100, TimeUnit.MILLISECONDS)); + Assert.assertThrows(TimeoutException.class, + () -> flush.get(100, TimeUnit.MILLISECONDS)); + + release.countDown(); + barrier.get(5, TimeUnit.SECONDS); + acquired = session.get(5, TimeUnit.SECONDS); + flush.get(5, TimeUnit.SECONDS); + } finally { + release.countDown(); + if (acquired != null) { + acquired.close(); + } + executor.shutdownNow(); + } + } + + @Test + public void archiveStateBarrierReleasesMonitorAfterFailure() throws Exception { + SnapshotManager manager = new SnapshotManager(""); + manager.enable(); + + IOException failure = Assert.assertThrows(IOException.class, + () -> manager.withArchiveStateBarrier(() -> { + throw new IOException("injected barrier failure"); + })); + Assert.assertEquals("injected barrier failure", failure.getMessage()); + try (ISession ignored = manager.buildSession()) { + Assert.assertEquals(1, manager.size()); + } + Assert.assertEquals(0, manager.size()); + } + + @Test + public void archiveStateBarrierBlocksNestedSessionMerge() throws Exception { + SnapshotManager manager = new SnapshotManager(""); + manager.enable(); + ISession parent = manager.buildSession(); + ISession child = manager.buildSession(); + ExecutorService executor = Executors.newFixedThreadPool(2); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + try { + Future barrier = executor.submit(() -> { + manager.withArchiveStateBarrier(() -> { + entered.countDown(); + try { + if (!release.await(5, TimeUnit.SECONDS)) { + throw new IOException("Timed out waiting to release archive state barrier"); + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted inside archive state barrier", interrupted); + } + }); + return null; + }); + Assert.assertTrue(entered.await(5, TimeUnit.SECONDS)); + Future merge = executor.submit(child::merge); + Assert.assertThrows(TimeoutException.class, + () -> merge.get(100, TimeUnit.MILLISECONDS)); + + release.countDown(); + barrier.get(5, TimeUnit.SECONDS); + merge.get(5, TimeUnit.SECONDS); + Assert.assertEquals(1, manager.size()); + } finally { + release.countDown(); + child.close(); + parent.close(); + executor.shutdownNow(); + } + Assert.assertEquals(0, manager.size()); + } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationAdapterTest.java b/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationAdapterTest.java new file mode 100644 index 00000000000..642272e876a --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationAdapterTest.java @@ -0,0 +1,269 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; +import org.tron.core.db2.common.DB; + +public class LatestStateGenerationAdapterTest { + + private static final List PARTICIPANTS = Arrays.asList("account", "properties"); + + @Test + public void currentDbAbstractionFailsClosedInsteadOfUsingOrdinaryGet() { + Map> databases = new LinkedHashMap<>(); + databases.put("account", new OrdinaryDb("account")); + databases.put("properties", new OrdinaryDb("properties")); + + ArchivePersistenceException failure = assertThrows(ArchivePersistenceException.class, + () -> LatestStateGenerationAdapter.fromDatabases(PARTICIPANTS, databases)); + assertTrue(failure.getMessage().contains("stable snapshot lifecycle")); + assertFalse(((OrdinaryDb) databases.get("account")).read); + assertFalse(((OrdinaryDb) databases.get("properties")).read); + } + + @Test + public void pinsExactGenerationAndSurvivesLiveSourceReplacement() throws Exception { + FakeStore account = new FakeStore("account", "rocksdb:/state/account", bytes("old")); + FakeStore properties = new FakeStore("properties", "leveldb:/state/properties", + bytes("property")); + LatestStateGenerationAdapter adapter = adapter(account, properties); + byte[] expectedDigest = adapter.getSourceIdentityDigest(); + + try (PinnedLatestState pinned = adapter.pin("generation-1", 7, hash(7), PARTICIPANTS)) { + account.replace("rocksdb:/replacement/account", bytes("new")); + assertArrayEquals(bytes("old"), pinned.get("account", bytes("key")).getValue()); + assertArrayEquals(expectedDigest, pinned.getSourceIdentityDigest()); + assertThrows(UnsupportedOperationException.class, + () -> pinned.range("account", new byte[0], null)); + } + assertEquals(1, account.closedSnapshots.get()); + assertEquals(1, properties.closedSnapshots.get()); + } + + @Test + public void releasesPartialAcquireAndRejectsReplacementIdentity() throws Exception { + FakeStore account = new FakeStore("account", "rocksdb:/state/account", bytes("old")); + FakeStore properties = new FakeStore("properties", "leveldb:/state/properties", + bytes("property")); + properties.failPin.set(true); + LatestStateGenerationAdapter adapter = adapter(account, properties); + + assertThrows(IOException.class, + () -> adapter.pin("generation-1", 7, hash(7), PARTICIPANTS)); + assertEquals(1, account.closedSnapshots.get()); + + properties.failPin.set(false); + properties.replaceAfterPin.set(true); + assertThrows(ArchivePersistenceException.class, + () -> adapter.pin("generation-1", 7, hash(7), PARTICIPANTS)); + assertEquals(2, account.closedSnapshots.get()); + assertEquals(1, properties.closedSnapshots.get()); + } + + @Test + public void rejectsParticipantAndSnapshotBlockIdentityMismatch() { + FakeStore account = new FakeStore("account", "rocksdb:/state/account", bytes("old")); + FakeStore properties = new FakeStore("properties", "leveldb:/state/properties", + bytes("property")); + LatestStateGenerationAdapter adapter = adapter(account, properties); + + assertThrows(ArchivePersistenceException.class, + () -> adapter.pin("generation-1", 7, hash(7), Collections.singletonList("account"))); + properties.wrongBlock.set(true); + assertThrows(ArchivePersistenceException.class, + () -> adapter.pin("generation-1", 7, hash(7), PARTICIPANTS)); + assertEquals(1, account.closedSnapshots.get()); + assertEquals(1, properties.closedSnapshots.get()); + } + + @Test + public void rejectsSourceReplacementBeforeAcquiringAnySnapshot() { + FakeStore account = new FakeStore("account", "rocksdb:/state/account", bytes("old")); + FakeStore properties = new FakeStore("properties", "leveldb:/state/properties", + bytes("property")); + LatestStateGenerationAdapter adapter = adapter(account, properties); + account.replace("rocksdb:/replacement/account", bytes("new")); + + assertThrows(ArchivePersistenceException.class, + () -> adapter.pin("generation-1", 7, hash(7), PARTICIPANTS)); + assertEquals(0, account.closedSnapshots.get()); + assertEquals(0, properties.closedSnapshots.get()); + } + + private static LatestStateGenerationAdapter adapter(FakeStore... stores) { + Map indexed = new LinkedHashMap<>(); + for (FakeStore store : stores) { + indexed.put(store.dbName, store); + } + return new LatestStateGenerationAdapter(PARTICIPANTS, indexed); + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static final class FakeStore implements SnapshotCapableStore { + private final String dbName; + private final AtomicBoolean failPin = new AtomicBoolean(); + private final AtomicBoolean wrongBlock = new AtomicBoolean(); + private final AtomicBoolean replaceAfterPin = new AtomicBoolean(); + private final AtomicInteger closedSnapshots = new AtomicInteger(); + private String identity; + private byte[] value; + + private FakeStore(String dbName, String identity, byte[] value) { + this.dbName = dbName; + this.identity = identity; + this.value = Arrays.copyOf(value, value.length); + } + + private void replace(String replacementIdentity, byte[] replacementValue) { + identity = replacementIdentity; + value = Arrays.copyOf(replacementValue, replacementValue.length); + } + + @Override + public String getDbName() { + return dbName; + } + + @Override + public String getSourceIdentity() { + return identity; + } + + @Override + public StoreSnapshot pin(long blockNumber, byte[] blockHash) throws IOException { + if (failPin.get()) { + throw new IOException("injected pin failure"); + } + String candidateIdentity = identity; + byte[] pinnedValue = Arrays.copyOf(value, value.length); + if (replaceAfterPin.get()) { + identity = identity + ":replacement"; + candidateIdentity = identity; + } + final String pinnedIdentity = candidateIdentity; + long pinnedBlock = wrongBlock.get() ? blockNumber + 1 : blockNumber; + return new StoreSnapshot() { + private boolean closed; + + @Override + public String getDbName() { + return dbName; + } + + @Override + public String getSourceIdentity() { + return pinnedIdentity; + } + + @Override + public long getBlockNumber() { + return pinnedBlock; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + @Override + public byte[] get(byte[] physicalRawKey) { + if (closed) { + throw new IllegalStateException("snapshot is closed"); + } + return Arrays.copyOf(pinnedValue, pinnedValue.length); + } + + @Override + public void close() { + if (!closed) { + closed = true; + closedSnapshots.incrementAndGet(); + } + } + }; + } + } + + private static final class OrdinaryDb implements DB { + private final String dbName; + private boolean read; + + private OrdinaryDb(String dbName) { + this.dbName = dbName; + } + + @Override + public byte[] get(byte[] key) { + read = true; + return null; + } + + @Override + public void put(byte[] key, byte[] value) { + } + + @Override + public long size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void remove(byte[] key) { + } + + @Override + public Iterator> iterator() { + return Collections.>emptyList().iterator(); + } + + @Override + public void close() { + } + + @Override + public String getDbName() { + return dbName; + } + + @Override + public void stat() { + } + + @Override + public DB newInstance() { + return this; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactoryTest.java b/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactoryTest.java new file mode 100644 index 00000000000..381c429002a --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactoryTest.java @@ -0,0 +1,334 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.tron.common.TestConstants.TEST_CONF; + +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.storage.leveldb.LevelDbDataSourceImpl; +import org.tron.common.storage.rocksdb.RocksDbDataSourceImpl; +import org.tron.core.config.args.Args; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; +import org.tron.core.db2.common.DB; +import org.tron.core.db2.common.LevelDB; +import org.tron.core.db2.common.RocksDB; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.core.SnapshotRoot; + +public class LatestStateGenerationCoordinatorFactoryTest { + + private final List openRegistries = new ArrayList<>(); + + static { + org.rocksdb.RocksDB.loadLibrary(); + } + + @BeforeClass + public static void initConfiguration() { + Args.setParam(new String[0], TEST_CONF); + } + + @AfterClass + public static void clearConfiguration() { + Args.clearParam(); + } + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @After + public void closeEngines() { + openRegistries.forEach(Registry::close); + openRegistries.clear(); + } + + @Test + public void assemblesExactMixedEnginesAndIgnoresDerivedStore() throws Exception { + Registry registry = registry(true); + Path readerVisible = temporaryFolder.newFile("reader-visible").toPath(); + storeReader(readerVisible, 1, registry.participants); + + try (LatestStateGenerationCoordinator coordinator = + LatestStateGenerationCoordinatorFactory.create(registry.manager, readerVisible); + LatestStateGenerationCoordinator.Candidate candidate = + coordinator.acquire("generation-1")) { + assertEquals(ArchiveStoreScope.getStateDatabases().size(), totalPins(registry)); + } + assertEquals(ArchiveStoreScope.getStateDatabases().size(), totalCloses(registry)); + } + + @Test + public void rejectsMissingDuplicateAndNonCapableStateRoots() throws Exception { + Path readerVisible = temporaryFolder.newFile("invalid-reader-visible").toPath(); + Registry missing = registry(false); + missing.manager.getDbs().removeIf(database -> "witness".equals(database.getDbName())); + assertThrows(ArchivePersistenceException.class, + () -> LatestStateGenerationCoordinatorFactory.create(missing.manager, readerVisible)); + + Registry duplicate = registry(false); + duplicate.manager.getDbs().add(new Chainbase(new SnapshotRoot( + duplicate.engines.get("account")))); + assertThrows(ArchivePersistenceException.class, + () -> LatestStateGenerationCoordinatorFactory.create(duplicate.manager, readerVisible)); + + SnapshotManager nonCapable = new SnapshotManager(""); + for (String participant : sortedParticipants()) { + nonCapable.getDbs().add(new Chainbase(new SnapshotRoot(nonCapable(participant)))); + } + assertThrows(ArchivePersistenceException.class, + () -> LatestStateGenerationCoordinatorFactory.create(nonCapable, readerVisible)); + } + + @Test + public void rejectsReaderSetMismatchAndReleasesPartialAcquire() throws Exception { + Registry registry = registry(false); + Path readerVisible = temporaryFolder.newFile("partial-reader-visible").toPath(); + List unexpectedDerived = new ArrayList<>(registry.participants); + unexpectedDerived.add("accountTrie"); + java.util.Collections.sort(unexpectedDerived); + storeReader(readerVisible, 1, unexpectedDerived); + + try (LatestStateGenerationCoordinator coordinator = + LatestStateGenerationCoordinatorFactory.create(registry.manager, readerVisible)) { + assertThrows(ArchivePersistenceException.class, + () -> coordinator.acquire("generation-mismatch")); + assertEquals(0, totalPins(registry)); + + storeReader(readerVisible, 1, registry.participants); + registry.probes.get("properties").failPin.set(true); + assertThrows(IllegalStateException.class, + () -> coordinator.acquire("generation-partial")); + assertTrue(totalPins(registry) > 0); + assertEquals(totalPins(registry), totalCloses(registry)); + } + AtomicBoolean reentered = new AtomicBoolean(); + registry.manager.withArchiveStateBarrier(() -> reentered.set(true)); + assertTrue(reentered.get()); + } + + @Test + public void readerDriftClosesEveryMixedEngineSnapshot() throws Exception { + Registry registry = registry(false); + Path readerVisible = temporaryFolder.newFile("drifting-reader-visible").toPath(); + storeReader(readerVisible, 1, registry.participants); + AtomicBoolean changed = new AtomicBoolean(); + registry.probes.firstEntry().getValue().onPin = () -> { + if (changed.compareAndSet(false, true)) { + try { + storeReader(readerVisible, 2, registry.participants); + } catch (java.io.IOException failure) { + throw new UncheckedIOException(failure); + } + } + }; + + try (LatestStateGenerationCoordinator coordinator = + LatestStateGenerationCoordinatorFactory.create(registry.manager, readerVisible)) { + assertThrows(ArchivePersistenceException.class, + () -> coordinator.acquire("generation-drift")); + } + assertEquals(ArchiveStoreScope.getStateDatabases().size(), totalPins(registry)); + assertEquals(totalPins(registry), totalCloses(registry)); + } + + private Registry registry(boolean includeDerived) { + SnapshotManager manager = new SnapshotManager(""); + TreeMap probes = new TreeMap<>(); + TreeMap> engines = new TreeMap<>(); + List participants = sortedParticipants(); + for (int index = 0; index < participants.size(); index++) { + String participant = participants.get(index); + Probe probe = new Probe(participant, (index & 1) == 0 ? "leveldb" : "rocksdb"); + DB engine = (index & 1) == 0 + ? new FakeLevelDB(probe) : new FakeRocksDB(probe); + probes.put(participant, probe); + engines.put(participant, engine); + manager.getDbs().add(new Chainbase(new SnapshotRoot(engine))); + } + if (includeDerived) { + manager.getDbs().add(new Chainbase(new SnapshotRoot(nonCapable("accountTrie")))); + } + Registry registry = new Registry(manager, participants, probes, engines); + openRegistries.add(registry); + return registry; + } + + @SuppressWarnings("unchecked") + private static DB nonCapable(String dbName) { + DB database = mock(DB.class); + when(database.getDbName()).thenReturn(dbName); + return database; + } + + private static List sortedParticipants() { + String[] participants = ArchiveStoreScope.getStateDatabases().toArray(new String[0]); + Arrays.sort(participants); + return Arrays.asList(participants); + } + + private static void storeReader(Path path, int epoch, List participants) + throws java.io.IOException { + new ArchiveProgressFile(path, new ArchiveProgressEnvelopeCodec()).store( + new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, epoch, hash(epoch), new byte[16], + new byte[32], participants)); + } + + private static int totalPins(Registry registry) { + return registry.probes.values().stream().mapToInt(probe -> probe.pins.get()).sum(); + } + + private static int totalCloses(Registry registry) { + return registry.probes.values().stream().mapToInt(probe -> probe.closes.get()).sum(); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static final class Registry { + private final SnapshotManager manager; + private final List participants; + private final TreeMap probes; + private final Map> engines; + + private Registry(SnapshotManager manager, List participants, + TreeMap probes, Map> engines) { + this.manager = manager; + this.participants = participants; + this.probes = probes; + this.engines = engines; + } + + private void close() { + engines.values().forEach(DB::close); + } + } + + private static final class Probe { + private final String dbName; + private final String sourceIdentity; + private final AtomicBoolean failPin = new AtomicBoolean(); + private final AtomicInteger pins = new AtomicInteger(); + private final AtomicInteger closes = new AtomicInteger(); + private Runnable onPin = () -> { }; + + private Probe(String dbName, String engine) { + this.dbName = dbName; + this.sourceIdentity = engine + ":" + dbName + ":00000000-0000-0000-0000-000000000001"; + } + + private StoreSnapshot pin(long blockNumber, byte[] blockHash) { + if (failPin.get()) { + throw new IllegalStateException("injected engine pin failure: " + dbName); + } + pins.incrementAndGet(); + onPin.run(); + byte[] expectedHash = Arrays.copyOf(blockHash, blockHash.length); + return new StoreSnapshot() { + private boolean closed; + + @Override + public String getDbName() { + return dbName; + } + + @Override + public String getSourceIdentity() { + return sourceIdentity; + } + + @Override + public long getBlockNumber() { + return blockNumber; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(expectedHash, expectedHash.length); + } + + @Override + public byte[] get(byte[] physicalRawKey) { + return null; + } + + @Override + public void close() { + if (!closed) { + closed = true; + closes.incrementAndGet(); + } + } + }; + } + } + + private static final class FakeLevelDB extends LevelDB { + private final Probe probe; + + private FakeLevelDB(Probe probe) { + super(mock(LevelDbDataSourceImpl.class)); + this.probe = probe; + } + + @Override + public String getDbName() { + return probe.dbName; + } + + @Override + public String getSourceIdentity() { + return probe.sourceIdentity; + } + + @Override + public StoreSnapshot pin(long blockNumber, byte[] blockHash) { + return probe.pin(blockNumber, blockHash); + } + } + + private static final class FakeRocksDB extends RocksDB { + private final Probe probe; + + private FakeRocksDB(Probe probe) { + super(mock(RocksDbDataSourceImpl.class)); + this.probe = probe; + } + + @Override + public String getDbName() { + return probe.dbName; + } + + @Override + public String getSourceIdentity() { + return probe.sourceIdentity; + } + + @Override + public StoreSnapshot pin(long blockNumber, byte[] blockHash) { + return probe.pin(blockNumber, blockHash); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorTest.java new file mode 100644 index 00000000000..937f027fe12 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorTest.java @@ -0,0 +1,345 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; +import org.tron.core.db2.core.SnapshotManager; + +public class LatestStateGenerationCoordinatorTest { + + private static final List PARTICIPANTS = Arrays.asList("account", "properties"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void acquiresEveryStoreInsideBarrierAndPublishesBoundGeneration() throws Exception { + Path root = temporaryFolder.newFolder("coordinator").toPath(); + FakeBarrier barrier = new FakeBarrier(); + FakeStore account = new FakeStore("account", "rocksdb:account", bytes("account-v1"), barrier); + FakeStore properties = new FakeStore("properties", "leveldb:properties", + bytes("properties-v1"), barrier); + Map stores = stores(account, properties); + AtomicReference authority = new AtomicReference<>(); + + try (ArchiveHistoryWriter writer = writer(root.resolve("archive")); + LatestStateGenerationCoordinator coordinator = new LatestStateGenerationCoordinator( + PARTICIPANTS, stores, barrier::run, authority::get)) { + writer.accept(diff(1, "account", "key", "old")); + authority.set(reader(writer.committedHead())); + try (LatestStateGenerationCoordinator.Candidate candidate = + coordinator.acquire("generation-1"); + PersistentServingKeyIndexGeneration serving = writer.buildServingGeneration( + root.resolve("generation-1"), "generation-1", + candidate.getSourceIdentityDigest())) { + assertFalse(barrier.active.get()); + assertTrue(coordinator.publish(null, candidate, serving)); + account.value = bytes("account-live-v2"); + try (PinnedLatestState pinned = coordinator.pin(serving)) { + assertArrayEquals(bytes("account-v1"), + pinned.get("account", bytes("key")).getValue()); + assertArrayEquals(candidate.getSourceIdentityDigest(), + pinned.getSourceIdentityDigest()); + assertEquals(1, coordinator.getReferenceCount("generation-1")); + } + } + } + assertEquals(1, account.closedSnapshots.get()); + assertEquals(1, properties.closedSnapshots.get()); + } + + @Test + public void authorityDriftAndPartialAcquireReleaseEverySnapshot() throws Exception { + FakeBarrier barrier = new FakeBarrier(); + FakeStore account = new FakeStore("account", "rocksdb:account", bytes("account"), barrier); + FakeStore properties = new FakeStore("properties", "leveldb:properties", + bytes("properties"), barrier); + AtomicInteger reads = new AtomicInteger(); + LatestStateGenerationCoordinator drifting = new LatestStateGenerationCoordinator( + PARTICIPANTS, stores(account, properties), barrier::run, + () -> reads.getAndIncrement() == 0 ? reader(1) : reader(2)); + try { + assertThrows(ArchivePersistenceException.class, + () -> drifting.acquire("generation-1")); + assertFalse(barrier.active.get()); + assertEquals(1, account.closedSnapshots.get()); + assertEquals(1, properties.closedSnapshots.get()); + } finally { + drifting.close(); + } + + properties.failPin.set(true); + LatestStateGenerationCoordinator partial = new LatestStateGenerationCoordinator( + PARTICIPANTS, stores(account, properties), barrier::run, () -> reader(1)); + try { + assertThrows(IOException.class, () -> partial.acquire("generation-2")); + assertFalse(barrier.active.get()); + assertEquals(2, account.closedSnapshots.get()); + assertEquals(1, properties.closedSnapshots.get()); + } finally { + partial.close(); + } + } + + @Test + public void pinnedOldGenerationSurvivesReplacementUntilLastReaderCloses() throws Exception { + Path root = temporaryFolder.newFolder("replacement").toPath(); + FakeBarrier barrier = new FakeBarrier(); + FakeStore account = new FakeStore("account", "rocksdb:account", bytes("account-v1"), barrier); + FakeStore properties = new FakeStore("properties", "leveldb:properties", + bytes("properties-v1"), barrier); + AtomicReference authority = new AtomicReference<>(); + LatestStateGenerationCoordinator coordinator = new LatestStateGenerationCoordinator( + PARTICIPANTS, stores(account, properties), barrier::run, authority::get); + PinnedLatestState oldPin = null; + PinnedLatestState newPin = null; + try (ArchiveHistoryWriter writer = writer(root.resolve("archive"))) { + writer.accept(diff(1, "account", "key", "old-1")); + authority.set(reader(writer.committedHead())); + try (LatestStateGenerationCoordinator.Candidate first = coordinator.acquire("generation-1"); + PersistentServingKeyIndexGeneration serving1 = writer.buildServingGeneration( + root.resolve("generation-1"), "generation-1", first.getSourceIdentityDigest())) { + assertTrue(coordinator.publish(null, first, serving1)); + oldPin = coordinator.pin(serving1); + } + + account.value = bytes("account-v2"); + properties.value = bytes("properties-v2"); + writer.accept(diff(2, "properties", "key", "old-2")); + authority.set(reader(writer.committedHead())); + try (LatestStateGenerationCoordinator.Candidate second = coordinator.acquire("generation-2"); + PersistentServingKeyIndexGeneration serving2 = writer.buildServingGeneration( + root.resolve("generation-2"), "generation-2", second.getSourceIdentityDigest())) { + assertTrue(coordinator.publish("generation-1", second, serving2)); + newPin = coordinator.pin(serving2); + assertArrayEquals(bytes("account-v1"), + oldPin.get("account", bytes("key")).getValue()); + assertArrayEquals(bytes("account-v2"), + newPin.get("account", bytes("key")).getValue()); + assertThrows(IOException.class, coordinator::close); + } + + oldPin.close(); + oldPin = null; + assertEquals(1, account.closedSnapshots.get()); + assertEquals(1, properties.closedSnapshots.get()); + newPin.close(); + newPin = null; + coordinator.close(); + assertEquals(2, account.closedSnapshots.get()); + assertEquals(2, properties.closedSnapshots.get()); + } finally { + if (oldPin != null) { + oldPin.close(); + } + if (newPin != null) { + newPin.close(); + } + coordinator.close(); + } + } + + @Test + public void snapshotManagerBarrierReleasesPartialAcquireAndMonitor() throws Exception { + SnapshotManager manager = new SnapshotManager(""); + manager.enable(); + SnapshotManagerBarrier barrier = new SnapshotManagerBarrier(manager); + FakeStore account = new FakeStore("account", "rocksdb:account", bytes("account"), + barrier.active); + FakeStore properties = new FakeStore("properties", "leveldb:properties", bytes("properties"), + barrier.active); + properties.failPin.set(true); + + try (LatestStateGenerationCoordinator coordinator = new LatestStateGenerationCoordinator( + PARTICIPANTS, stores(account, properties), barrier::run, () -> reader(1))) { + assertThrows(IOException.class, () -> coordinator.acquire("generation-1")); + assertFalse(barrier.active.get()); + assertEquals(1, account.closedSnapshots.get()); + assertEquals(0, properties.closedSnapshots.get()); + } + try (org.tron.core.db2.ISession ignored = manager.buildSession()) { + assertEquals(1, manager.size()); + } + assertEquals(0, manager.size()); + } + + private static ArchiveHistoryWriter writer(Path archive) throws IOException { + return new ArchiveHistoryWriter(archive, 4096, new LinkedHashSet<>(PARTICIPANTS)); + } + + private static BlockReverseDiff diff(int block, String database, String key, String oldValue) { + return new BlockReverseDiff(new BlockSnapshotMeta(block, block, hash(block), hash(block - 1), + block * 1_000L), Collections.singletonList(new BlockReverseDiff.DbGroup(database, + Collections.singletonList(new BlockReverseDiff.Entry(bytes(key), + OldValue.present(bytes(oldValue))))))); + } + + private static ArchiveProgressEnvelope reader(HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(ArchiveProgressEnvelope.Kind.READER_VISIBLE, null, + marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private static ArchiveProgressEnvelope reader(int epoch) { + return new ArchiveProgressEnvelope(ArchiveProgressEnvelope.Kind.READER_VISIBLE, null, epoch, + hash(epoch), new byte[16], new byte[32], PARTICIPANTS); + } + + private static Map stores(FakeStore... stores) { + Map indexed = new LinkedHashMap<>(); + for (FakeStore store : stores) { + indexed.put(store.dbName, store); + } + return indexed; + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static final class FakeBarrier { + private final AtomicBoolean active = new AtomicBoolean(); + + private void run(ArchiveStateBarrier.ArchiveStateAction action) throws IOException { + if (!active.compareAndSet(false, true)) { + throw new IllegalStateException("barrier is already active"); + } + try { + action.run(); + } finally { + if (!active.compareAndSet(true, false)) { + throw new IllegalStateException("barrier is not active"); + } + } + } + } + + private static final class SnapshotManagerBarrier { + private final SnapshotManager manager; + private final AtomicBoolean active = new AtomicBoolean(); + + private SnapshotManagerBarrier(SnapshotManager manager) { + this.manager = manager; + } + + private void run(ArchiveStateBarrier.ArchiveStateAction action) throws IOException { + manager.withArchiveStateBarrier(() -> { + if (!active.compareAndSet(false, true)) { + throw new IllegalStateException("barrier is already active"); + } + try { + action.run(); + } finally { + if (!active.compareAndSet(true, false)) { + throw new IllegalStateException("barrier is not active"); + } + } + }); + } + } + + private static final class FakeStore implements SnapshotCapableStore { + private final String dbName; + private final String sourceIdentity; + private final AtomicBoolean barrierActive; + private final AtomicBoolean failPin = new AtomicBoolean(); + private final AtomicInteger closedSnapshots = new AtomicInteger(); + private byte[] value; + + private FakeStore(String dbName, String sourceIdentity, byte[] value, FakeBarrier barrier) { + this(dbName, sourceIdentity, value, barrier.active); + } + + private FakeStore(String dbName, String sourceIdentity, byte[] value, + AtomicBoolean barrierActive) { + this.dbName = dbName; + this.sourceIdentity = sourceIdentity; + this.value = value; + this.barrierActive = barrierActive; + } + + @Override + public String getDbName() { + return dbName; + } + + @Override + public String getSourceIdentity() { + return sourceIdentity; + } + + @Override + public StoreSnapshot pin(long blockNumber, byte[] blockHash) throws IOException { + if (!barrierActive.get()) { + throw new AssertionError("Store snapshot acquired outside global barrier"); + } + if (failPin.get()) { + throw new IOException("injected Store pin failure"); + } + byte[] pinnedValue = Arrays.copyOf(value, value.length); + return new StoreSnapshot() { + private boolean closed; + + @Override + public String getDbName() { + return dbName; + } + + @Override + public String getSourceIdentity() { + return sourceIdentity; + } + + @Override + public long getBlockNumber() { + return blockNumber; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + @Override + public byte[] get(byte[] physicalRawKey) { + return Arrays.copyOf(pinnedValue, pinnedValue.length); + } + + @Override + public void close() { + if (!closed) { + closed = true; + closedSnapshots.incrementAndGet(); + } + } + }; + } + } +} From 84929f6f223f8b8b035fac8659b7929d421c35af Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 19 Aug 2026 22:53:16 +0800 Subject: [PATCH 012/161] feat(chainbase): publish archive recovery state Apply business mutations with participant progress in native sync batches. Freshly validate H/C/D authorities under the archive barrier and advance reader visibility only after exact convergence. --- .../archive/ArchiveReaderPublicationGate.java | 172 ++++++++++++ .../ArchiveRecoveryAuthorityScanner.java | 168 +++++++++++ .../archive/LevelDbArchiveParticipant.java | 172 ++++++++++++ .../archive/RocksDbArchiveParticipant.java | 178 ++++++++++++ .../RocksDbArchiveRecoveryStorage.java | 178 ++++++++++++ .../ArchiveReaderPublicationGateTest.java | 260 ++++++++++++++++++ .../ArchiveRecoveryAuthorityScannerTest.java | 260 ++++++++++++++++++ .../LevelDbArchiveParticipantTest.java | 125 +++++++++ .../RocksDbArchiveParticipantTest.java | 163 +++++++++++ .../RocksDbArchiveRecoveryStorageTest.java | 138 ++++++++++ 10 files changed, 1814 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorage.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScannerTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorageTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java new file mode 100644 index 00000000000..ef57ec843c0 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java @@ -0,0 +1,172 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; + +/** Publishes reader-visible R only after fresh H/C/D identity convergence under one barrier. */ +public final class ArchiveReaderPublicationGate { + + private final HistoryCommitStore history; + private final ProgressSource checkpointSource; + private final Map participantSources; + private final Path readerVisiblePath; + private final ArchiveProgressFile readerVisibleFile; + private final ArchiveReaderHeadPublisher publisher; + private final List participants; + private final ArchiveStateBarrier barrier; + + public ArchiveReaderPublicationGate(HistoryCommitStore history, + ProgressSource checkpointSource, Map participantSources, + Path readerVisiblePath, List participants, ArchiveStateBarrier barrier) { + this(history, checkpointSource, participantSources, readerVisiblePath, participants, barrier, + temporary -> { }); + } + + ArchiveReaderPublicationGate(HistoryCommitStore history, + ProgressSource checkpointSource, Map participantSources, + Path readerVisiblePath, List participants, ArchiveStateBarrier barrier, + ArchiveProgressFile.FaultHook faultHook) { + this.history = Objects.requireNonNull(history, "history"); + this.checkpointSource = Objects.requireNonNull(checkpointSource, "checkpointSource"); + this.participants = validateParticipants(participants); + TreeMap sorted = new TreeMap<>( + Objects.requireNonNull(participantSources, "participantSources")); + if (!new ArrayList<>(sorted.keySet()).equals(this.participants) + || sorted.containsValue(null)) { + throw new IllegalArgumentException("Archive publication participant source set mismatch"); + } + this.participantSources = Collections.unmodifiableMap(new LinkedHashMap<>(sorted)); + this.readerVisiblePath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); + this.readerVisibleFile = new ArchiveProgressFile(readerVisiblePath, + new ArchiveProgressEnvelopeCodec()); + this.publisher = new ArchiveReaderHeadPublisher(history, readerVisiblePath, this.participants, + Objects.requireNonNull(faultHook, "faultHook")); + this.barrier = Objects.requireNonNull(barrier, "barrier"); + } + + public static ArchiveReaderPublicationGate forFiles(HistoryCommitStore history, + Path checkpointPath, Map participantPaths, Path readerVisiblePath, + List participants, ArchiveStateBarrier barrier) { + Objects.requireNonNull(checkpointPath, "checkpointPath"); + TreeMap sorted = new TreeMap<>( + Objects.requireNonNull(participantPaths, "participantPaths")); + if (sorted.containsValue(null)) { + throw new IllegalArgumentException("Archive publication participant path is missing"); + } + Map sources = new LinkedHashMap<>(); + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + sorted.forEach((participant, path) -> sources.put(participant, + () -> new ArchiveProgressFile(path, codec).load())); + return new ArchiveReaderPublicationGate(history, + () -> new ArchiveProgressFile(checkpointPath, codec).load(), sources, + readerVisiblePath, participants, barrier); + } + + public void publish(long targetEpoch) throws IOException { + if (targetEpoch < 0) { + throw new IllegalArgumentException("Reader publication target must be non-negative"); + } + barrier.run(() -> publishInsideBarrier(targetEpoch)); + } + + private void publishInsideBarrier(long targetEpoch) throws IOException { + HistoryCommitMarker target = requireMarker(targetEpoch); + validateCurrentReader(targetEpoch); + validateAuthorities(target); + validateAuthorities(target); + HistoryCommitMarker reloaded = requireMarker(targetEpoch); + if (!sameIdentity(target, reloaded)) { + throw new ArchivePersistenceException( + "Committed history identity drifted during reader publication"); + } + publisher.publish(targetEpoch); + } + + private HistoryCommitMarker requireMarker(long targetEpoch) { + HistoryCommitMarker marker = history.get(targetEpoch); + if (marker == null || marker.getMeta().getEpoch() != targetEpoch + || !marker.getDatabases().equals(participants)) { + throw new ArchivePersistenceException( + "Missing or mismatched committed publication target: " + targetEpoch); + } + return marker; + } + + private void validateCurrentReader(long targetEpoch) throws IOException { + if (!Files.exists(readerVisiblePath)) { + return; + } + ArchiveProgressEnvelope current = readerVisibleFile.load(); + HistoryCommitMarker marker = requireMarker(current.getEpoch()); + requireIdentity(current, Kind.READER_VISIBLE, null, marker); + if (current.getEpoch() > targetEpoch) { + throw new ArchivePersistenceException("Reader-visible authority cannot move backwards"); + } + } + + private void validateAuthorities(HistoryCommitMarker target) throws IOException { + ArchiveProgressEnvelope checkpoint = load(checkpointSource, "archive apply checkpoint"); + requireIdentity(checkpoint, Kind.APPLY_CHECKPOINT, null, target); + for (Map.Entry entry : participantSources.entrySet()) { + ArchiveProgressEnvelope progress = load(entry.getValue(), + "archive participant progress: " + entry.getKey()); + requireIdentity(progress, Kind.PARTICIPANT_PROGRESS, entry.getKey(), target); + } + } + + private ArchiveProgressEnvelope load(ProgressSource source, String name) throws IOException { + ArchiveProgressEnvelope envelope = source.load(); + if (envelope == null) { + throw new ArchivePersistenceException("Missing " + name); + } + return envelope; + } + + private void requireIdentity(ArchiveProgressEnvelope envelope, Kind kind, String participant, + HistoryCommitMarker marker) { + envelope.requireIdentity(kind, participant, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), participants); + } + + private static boolean sameIdentity(HistoryCommitMarker left, HistoryCommitMarker right) { + return left.getMeta().getEpoch() == right.getMeta().getEpoch() + && Arrays.equals(left.getMeta().getBlockHash(), right.getMeta().getBlockHash()) + && Arrays.equals(left.getBatchId(), right.getBatchId()) + && Arrays.equals(left.getHistoryLocation().getBodyDigest(), + right.getHistoryLocation().getBodyDigest()) + && left.getDatabases().equals(right.getDatabases()); + } + + private static List validateParticipants(List participants) { + List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); + if (copy.isEmpty()) { + throw new IllegalArgumentException("Archive publication participant set must not be empty"); + } + String previous = null; + for (String participant : copy) { + if (participant == null || participant.isEmpty() + || previous != null && previous.compareTo(participant) >= 0) { + throw new IllegalArgumentException( + "Archive publication participants must be non-empty, unique, and sorted"); + } + previous = participant; + } + return Collections.unmodifiableList(copy); + } + + @FunctionalInterface + public interface ProgressSource { + ArchiveProgressEnvelope load() throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java new file mode 100644 index 00000000000..8fbaeaff8fc --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java @@ -0,0 +1,168 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; + +/** File-backed prototype authority adapters for one fresh validating recovery scan. */ +public final class ArchiveRecoveryAuthorityScanner { + + private final HistoryCommitStore history; + private final Path checkpointPath; + private final Map participantSources; + private final Path readerVisiblePath; + private final List participants; + private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); + + public ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpointPath, + Map participantPaths, Path readerVisiblePath, + List participants) { + this.history = Objects.requireNonNull(history, "history"); + this.checkpointPath = Objects.requireNonNull(checkpointPath, "checkpointPath"); + this.readerVisiblePath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); + this.participants = validateParticipants(participants); + TreeMap sortedPaths = new TreeMap<>( + Objects.requireNonNull(participantPaths, "participantPaths")); + if (!new ArrayList<>(sortedPaths.keySet()).equals(this.participants) + || sortedPaths.containsValue(null)) { + throw new IllegalArgumentException("Archive participant progress path set mismatch"); + } + Map sources = new LinkedHashMap<>(); + sortedPaths.forEach((participant, path) -> + sources.put(participant, () -> progressFile(path).load())); + this.participantSources = Collections.unmodifiableMap(sources); + } + + private ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpointPath, + Map participantBatches, Path readerVisiblePath, + List participants, boolean batchAuthority) { + this.history = Objects.requireNonNull(history, "history"); + this.checkpointPath = Objects.requireNonNull(checkpointPath, "checkpointPath"); + this.readerVisiblePath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); + this.participants = validateParticipants(participants); + TreeMap sortedBatches = new TreeMap<>( + Objects.requireNonNull(participantBatches, "participantBatches")); + if (!new ArrayList<>(sortedBatches.keySet()).equals(this.participants) + || sortedBatches.containsValue(null)) { + throw new IllegalArgumentException("Archive participant batch set mismatch"); + } + Map sources = new LinkedHashMap<>(); + sortedBatches.forEach((participant, batch) -> + sources.put(participant, () -> batch.load().getProgress())); + this.participantSources = Collections.unmodifiableMap(sources); + } + + private ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpointPath, + Map participantSources, Path readerVisiblePath, + List participants, byte nativeEngineAuthority) { + this.history = Objects.requireNonNull(history, "history"); + this.checkpointPath = Objects.requireNonNull(checkpointPath, "checkpointPath"); + this.readerVisiblePath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); + this.participants = validateParticipants(participants); + TreeMap sortedSources = new TreeMap<>( + Objects.requireNonNull(participantSources, "participantSources")); + if (!new ArrayList<>(sortedSources.keySet()).equals(this.participants) + || sortedSources.containsValue(null)) { + throw new IllegalArgumentException("Archive participant source set mismatch"); + } + this.participantSources = Collections.unmodifiableMap( + new LinkedHashMap<>(sortedSources)); + } + + public static ArchiveRecoveryAuthorityScanner forParticipantBatches( + HistoryCommitStore history, Path checkpointPath, + Map participantBatches, Path readerVisiblePath, + List participants) { + return new ArchiveRecoveryAuthorityScanner(history, checkpointPath, participantBatches, + readerVisiblePath, participants, true); + } + + public static ArchiveRecoveryAuthorityScanner forRocksDbParticipants( + HistoryCommitStore history, Path checkpointPath, + Map participantEngines, Path readerVisiblePath, + List participants) { + Map sources = new LinkedHashMap<>(); + Objects.requireNonNull(participantEngines, "participantEngines") + .forEach((participant, engine) -> sources.put(participant, engine::loadProgress)); + return new ArchiveRecoveryAuthorityScanner(history, checkpointPath, sources, + readerVisiblePath, participants, (byte) 1); + } + + public RecoverySnapshot scan() throws IOException { + ArchiveRecoveryScanner.HistoryIdentitySource historySource = + new ArchiveRecoveryScanner.HistoryIdentitySource() { + @Override + public long committedHeadEpoch() { + HistoryCommitMarker head = history.head(); + if (head == null) { + throw new ArchivePersistenceException("Committed archive history is empty"); + } + return head.getMeta().getEpoch(); + } + + @Override + public HistoryCommitMarker committedMarker(long epoch) { + return history.get(epoch); + } + }; + ArchiveRecoveryScanner.ProgressIdentitySource progressSource = + new ArchiveRecoveryScanner.ProgressIdentitySource() { + @Override + public ArchiveProgressEnvelope loadCheckpoint() throws IOException { + return progressFile(checkpointPath).load(); + } + + @Override + public Map loadParticipantProgress() + throws IOException { + Map loaded = new LinkedHashMap<>(); + for (Map.Entry entry + : participantSources.entrySet()) { + loaded.put(entry.getKey(), entry.getValue().load()); + } + return loaded; + } + + @Override + public ArchiveProgressEnvelope loadReaderVisible() throws IOException { + return progressFile(readerVisiblePath).load(); + } + }; + return new ArchiveRecoveryScanner(historySource, progressSource, participants).scan(); + } + + private ArchiveProgressFile progressFile(Path path) { + return new ArchiveProgressFile(path, progressCodec); + } + + @FunctionalInterface + private interface ParticipantProgressSource { + ArchiveProgressEnvelope load() throws IOException; + } + + private static List validateParticipants(List participants) { + Objects.requireNonNull(participants, "participants"); + if (participants.isEmpty()) { + throw new IllegalArgumentException("Archive participant set must not be empty"); + } + List copy = new ArrayList<>(participants.size()); + String previous = null; + for (String participant : participants) { + if (participant == null || participant.isEmpty() + || previous != null && previous.compareTo(participant) >= 0) { + throw new IllegalArgumentException( + "Archive participants must be non-empty, unique, and sorted"); + } + copy.add(participant); + previous = participant; + } + return Collections.unmodifiableList(copy); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java b/chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java new file mode 100644 index 00000000000..7cc84d7d587 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java @@ -0,0 +1,172 @@ +package org.tron.core.db2.archive; + +import static org.fusesource.leveldbjni.JniDBFactory.factory; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.iq80.leveldb.DB; +import org.iq80.leveldb.Options; +import org.iq80.leveldb.WriteBatch; +import org.iq80.leveldb.WriteOptions; + +/** LevelDB participant whose business mutations and D[i] share one synced native WriteBatch. */ +public final class LevelDbArchiveParticipant implements Closeable { + + private static final byte BUSINESS_PREFIX = 1; + private static final byte[] PROGRESS_KEY = new byte[]{0, 'p', 'r', 'o', 'g', 'r', 'e', 's', 's'}; + + private final Path directory; + private final String participant; + private final List participants; + private final Options options = new Options().createIfMissing(true); + private final WriteOptions syncWrites = new WriteOptions().sync(true); + private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); + private final FaultHook faultHook; + private DB database; + + public LevelDbArchiveParticipant(Path directory, String participant, + List participants) throws IOException { + this(directory, participant, participants, stage -> { }); + } + + LevelDbArchiveParticipant(Path directory, String participant, List participants, + FaultHook faultHook) throws IOException { + this.directory = Objects.requireNonNull(directory, "directory"); + this.participant = Objects.requireNonNull(participant, "participant"); + this.participants = validateParticipants(participants); + if (!this.participants.contains(participant)) { + throw new IllegalArgumentException("Archive participant is outside the exact set"); + } + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + Files.createDirectories(directory); + database = open(); + } + + public synchronized void apply(List mutations, ArchiveProgressEnvelope progress) + throws IOException { + Objects.requireNonNull(mutations, "mutations"); + requireProgress(progress); + try (WriteBatch batch = database.createWriteBatch()) { + for (Mutation mutation : mutations) { + Objects.requireNonNull(mutation, "mutation"); + if (mutation.value == null) { + batch.delete(businessKey(mutation.key)); + } else { + batch.put(businessKey(mutation.key), mutation.value); + } + } + batch.put(PROGRESS_KEY, progressCodec.encode(progress)); + faultHook.atStage(Stage.BEFORE_WRITE); + database.write(batch, syncWrites); + faultHook.atStage(Stage.AFTER_WRITE); + } + } + + public synchronized byte[] get(byte[] key) { + byte[] value = database.get(businessKey(key)); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + public synchronized ArchiveProgressEnvelope loadProgress() { + byte[] encoded = database.get(PROGRESS_KEY); + if (encoded == null) { + throw new ArchivePersistenceException("Archive participant progress is missing"); + } + try { + ArchiveProgressEnvelope progress = progressCodec.decode(encoded); + requireProgress(progress); + return progress; + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Archive participant progress is corrupt", invalid); + } + } + + public synchronized void reset() throws IOException { + database.close(); + database = null; + try { + factory.destroy(directory.toFile(), options); + } finally { + database = open(); + } + } + + @Override + public synchronized void close() throws IOException { + if (database != null) { + database.close(); + database = null; + } + } + + private DB open() throws IOException { + return factory.open(directory.toFile(), options); + } + + private void requireProgress(ArchiveProgressEnvelope progress) { + Objects.requireNonNull(progress, "progress"); + if (progress.getKind() != ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS + || !participant.equals(progress.getParticipant()) + || !participants.equals(progress.getParticipants())) { + throw new IllegalArgumentException("Archive participant progress identity mismatch"); + } + } + + private static byte[] businessKey(byte[] key) { + Objects.requireNonNull(key, "key"); + return ByteBuffer.allocate(1 + key.length).put(BUSINESS_PREFIX).put(key).array(); + } + + private static List validateParticipants(List participants) { + List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); + if (copy.isEmpty()) { + throw new IllegalArgumentException("Archive participant set must not be empty"); + } + String previous = null; + for (String current : copy) { + if (current == null || current.isEmpty() + || previous != null && previous.compareTo(current) >= 0) { + throw new IllegalArgumentException( + "Archive participants must be non-empty, unique, and sorted"); + } + previous = current; + } + return Collections.unmodifiableList(copy); + } + + public static final class Mutation { + private final byte[] key; + private final byte[] value; + + private Mutation(byte[] key, byte[] value) { + this.key = Arrays.copyOf(Objects.requireNonNull(key, "key"), key.length); + this.value = value == null ? null : Arrays.copyOf(value, value.length); + } + + public static Mutation put(byte[] key, byte[] value) { + return new Mutation(key, Objects.requireNonNull(value, "value")); + } + + public static Mutation delete(byte[] key) { + return new Mutation(key, null); + } + } + + enum Stage { + BEFORE_WRITE, + AFTER_WRITE + } + + @FunctionalInterface + interface FaultHook { + void atStage(Stage stage) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java b/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java new file mode 100644 index 00000000000..40237cc4630 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java @@ -0,0 +1,178 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; + +/** RocksDB participant whose business mutations and D[i] share one synced native WriteBatch. */ +public final class RocksDbArchiveParticipant implements Closeable { + + private static final byte BUSINESS_PREFIX = 1; + private static final byte[] PROGRESS_KEY = new byte[]{0, 'p', 'r', 'o', 'g', 'r', 'e', 's', 's'}; + + static { + RocksDB.loadLibrary(); + } + + private final String participant; + private final List participants; + private final Options options = new Options().setCreateIfMissing(true); + private final WriteOptions syncWrites = new WriteOptions().setSync(true); + private final RocksDB database; + private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); + private final FaultHook faultHook; + + public RocksDbArchiveParticipant(Path directory, String participant, + List participants) throws IOException { + this(directory, participant, participants, stage -> { }); + } + + RocksDbArchiveParticipant(Path directory, String participant, List participants, + FaultHook faultHook) throws IOException { + this.participant = Objects.requireNonNull(participant, "participant"); + this.participants = validateParticipants(participants); + if (!this.participants.contains(participant)) { + throw new IllegalArgumentException("Archive participant is outside the exact set"); + } + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + try { + Files.createDirectories(directory); + } catch (IOException failure) { + options.close(); + syncWrites.close(); + throw failure; + } + try { + database = RocksDB.open(options, directory.toString()); + } catch (RocksDBException failure) { + options.close(); + syncWrites.close(); + throw new IOException("Failed to open archive participant engine", failure); + } + } + + public synchronized void apply(List mutations, ArchiveProgressEnvelope progress) + throws IOException { + Objects.requireNonNull(mutations, "mutations"); + requireProgress(progress); + try (WriteBatch batch = new WriteBatch()) { + for (Mutation mutation : mutations) { + Objects.requireNonNull(mutation, "mutation"); + if (mutation.value == null) { + batch.delete(businessKey(mutation.key)); + } else { + batch.put(businessKey(mutation.key), mutation.value); + } + } + batch.put(PROGRESS_KEY, progressCodec.encode(progress)); + faultHook.atStage(Stage.BEFORE_WRITE); + database.write(syncWrites, batch); + faultHook.atStage(Stage.AFTER_WRITE); + } catch (RocksDBException failure) { + throw new IOException("Failed to apply archive participant batch", failure); + } + } + + public synchronized byte[] get(byte[] key) throws IOException { + try { + byte[] value = database.get(businessKey(key)); + return value == null ? null : Arrays.copyOf(value, value.length); + } catch (RocksDBException failure) { + throw new IOException("Failed to read archive participant business state", failure); + } + } + + public synchronized ArchiveProgressEnvelope loadProgress() throws IOException { + try { + byte[] encoded = database.get(PROGRESS_KEY); + if (encoded == null) { + throw new ArchivePersistenceException("Archive participant progress is missing"); + } + ArchiveProgressEnvelope progress = progressCodec.decode(encoded); + requireProgress(progress); + return progress; + } catch (RocksDBException failure) { + throw new IOException("Failed to read archive participant progress", failure); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Archive participant progress is corrupt", invalid); + } + } + + @Override + public synchronized void close() { + syncWrites.close(); + database.close(); + options.close(); + } + + private void requireProgress(ArchiveProgressEnvelope progress) { + Objects.requireNonNull(progress, "progress"); + if (progress.getKind() != ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS + || !participant.equals(progress.getParticipant()) + || !participants.equals(progress.getParticipants())) { + throw new IllegalArgumentException("Archive participant progress identity mismatch"); + } + } + + private static byte[] businessKey(byte[] key) { + Objects.requireNonNull(key, "key"); + return ByteBuffer.allocate(1 + key.length).put(BUSINESS_PREFIX).put(key).array(); + } + + private static List validateParticipants(List participants) { + List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); + if (copy.isEmpty()) { + throw new IllegalArgumentException("Archive participant set must not be empty"); + } + String previous = null; + for (String current : copy) { + if (current == null || current.isEmpty() + || previous != null && previous.compareTo(current) >= 0) { + throw new IllegalArgumentException( + "Archive participants must be non-empty, unique, and sorted"); + } + previous = current; + } + return Collections.unmodifiableList(copy); + } + + public static final class Mutation { + private final byte[] key; + private final byte[] value; + + private Mutation(byte[] key, byte[] value) { + this.key = Arrays.copyOf(Objects.requireNonNull(key, "key"), key.length); + this.value = value == null ? null : Arrays.copyOf(value, value.length); + } + + public static Mutation put(byte[] key, byte[] value) { + return new Mutation(key, Objects.requireNonNull(value, "value")); + } + + public static Mutation delete(byte[] key) { + return new Mutation(key, null); + } + } + + enum Stage { + BEFORE_WRITE, + AFTER_WRITE + } + + @FunctionalInterface + interface FaultHook { + void atStage(Stage stage) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorage.java b/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorage.java new file mode 100644 index 00000000000..eb65573787c --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorage.java @@ -0,0 +1,178 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoveryStorage; +import org.tron.core.db2.archive.RocksDbArchiveParticipant.Mutation; + +/** File-history plus native RocksDB participant implementation of the H/C/D[i]/R executor. */ +public final class RocksDbArchiveRecoveryStorage implements RecoveryStorage, Closeable { + + private final Path archiveDirectory; + private final long maxSegmentSize; + private final List participants; + private final Map participantEngines; + private final ParticipantReplayer replayer; + private final HistorySegmentStore bodies; + private final HistoryIndexStore index; + private final HistoryCommitStore history; + private final ArchiveRecoveryAuthorityScanner scanner; + private final ArchiveReaderHeadPublisher readerPublisher; + + public RocksDbArchiveRecoveryStorage(Path archiveDirectory, long maxSegmentSize, + Path checkpointPath, Map participantEngines, + Path readerVisiblePath, List participants, ParticipantReplayer replayer) + throws IOException { + this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + if (maxSegmentSize <= 0) { + throw new IllegalArgumentException("maxSegmentSize must be positive"); + } + this.maxSegmentSize = maxSegmentSize; + this.participants = validateParticipants(participants); + TreeMap sorted = new TreeMap<>( + Objects.requireNonNull(participantEngines, "participantEngines")); + if (!new ArrayList<>(sorted.keySet()).equals(this.participants) + || sorted.containsValue(null)) { + throw new IllegalArgumentException("Archive participant engine set mismatch"); + } + this.participantEngines = Collections.unmodifiableMap(sorted); + this.replayer = Objects.requireNonNull(replayer, "replayer"); + new ArchiveTruncationRecovery(archiveDirectory, maxSegmentSize).recover(); + ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archiveDirectory, + new HistoryCommitMarkerCodec()); + if (checkpoint == null) { + throw new ArchivePersistenceException("Archive restart checkpoint is missing"); + } + HistorySegmentStore openedBodies = null; + HistoryIndexStore openedIndex = null; + HistoryCommitStore openedHistory = null; + try { + openedBodies = new HistorySegmentStore(archiveDirectory, new BlockHistoryCodec(), + maxSegmentSize, checkpoint); + openedIndex = new HistoryIndexStore(archiveDirectory, new HistoryIndexCodec(), checkpoint); + openedHistory = new HistoryCommitStore(archiveDirectory, new HistoryCommitMarkerCodec(), + checkpoint); + } catch (IOException | RuntimeException failure) { + close(openedIndex, failure); + close(openedBodies, failure); + close(openedHistory, failure); + throw failure; + } + this.bodies = openedBodies; + this.index = openedIndex; + this.history = openedHistory; + this.scanner = ArchiveRecoveryAuthorityScanner.forRocksDbParticipants(this.history, + checkpointPath, this.participantEngines, readerVisiblePath, this.participants); + this.readerPublisher = new ArchiveReaderHeadPublisher(this.history, readerVisiblePath, + this.participants); + } + + @Override + public RecoverySnapshot scan() throws IOException { + return scanner.scan(); + } + + @Override + public void truncateHistoryAndSync(long historyHead) throws IOException { + ArchiveTruncationIntent.prepare(archiveDirectory, history, index, bodies, historyHead, + new HistoryCommitMarkerCodec()); + new ArchiveTruncationRecovery(archiveDirectory, maxSegmentSize).recover(); + } + + @Override + public void replayParticipantAndSyncProgress(String participant, long firstEpoch, + long lastEpoch) throws IOException { + RocksDbArchiveParticipant engine = participantEngines.get(participant); + if (engine == null) { + throw new ArchivePersistenceException("Unknown archive recovery participant: " + participant); + } + HistoryCommitMarker marker = history.get(lastEpoch); + if (marker == null || firstEpoch > lastEpoch) { + throw new ArchivePersistenceException("Archive participant replay range is invalid"); + } + List mutations = Objects.requireNonNull( + replayer.replay(participant, firstEpoch, lastEpoch), "participant replay mutations"); + ArchiveProgressEnvelope progress = new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, + participant, lastEpoch, marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), participants); + engine.apply(mutations, progress); + } + + @Override + public void publishReaderHeadAndSync(long readerVisibleHead) throws IOException { + readerPublisher.publish(readerVisibleHead); + } + + @Override + public void close() throws IOException { + IOException failure = null; + try { + index.close(); + } catch (IOException closeFailure) { + failure = closeFailure; + } + try { + bodies.close(); + } catch (IOException closeFailure) { + failure = add(failure, closeFailure); + } + try { + history.close(); + } catch (IOException closeFailure) { + failure = add(failure, closeFailure); + } + if (failure != null) { + throw failure; + } + } + + private static List validateParticipants(List participants) { + List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); + if (copy.isEmpty()) { + throw new IllegalArgumentException("Archive participant set must not be empty"); + } + String previous = null; + for (String participant : copy) { + if (participant == null || participant.isEmpty() + || previous != null && previous.compareTo(participant) >= 0) { + throw new IllegalArgumentException( + "Archive participants must be non-empty, unique, and sorted"); + } + previous = participant; + } + return Collections.unmodifiableList(copy); + } + + private static IOException add(IOException current, IOException addition) { + if (current == null) { + return addition; + } + current.addSuppressed(addition); + return current; + } + + private static void close(Closeable resource, Exception failure) { + if (resource == null) { + return; + } + try { + resource.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + + @FunctionalInterface + public interface ParticipantReplayer { + List replay(String participant, long firstEpoch, long lastEpoch) throws IOException; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java new file mode 100644 index 00000000000..e6b077c8133 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java @@ -0,0 +1,260 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveReaderPublicationGate.ProgressSource; +import org.tron.core.db2.core.SnapshotManager; + +public class ArchiveReaderPublicationGateTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void publishesExactAuthoritiesWhileMergeAndFlushAreBlocked() throws Exception { + try (Fixture fixture = fixture()) { + SnapshotManager manager = new SnapshotManager(""); + manager.enable(); + ISession parent = manager.buildSession(); + ISession child = manager.buildSession(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicBoolean paused = new AtomicBoolean(); + Map sources = fixture.sources(); + String first = fixture.participants.get(0); + ProgressSource original = sources.get(first); + sources.put(first, () -> { + ArchiveProgressEnvelope loaded = original.load(); + if (paused.compareAndSet(false, true)) { + entered.countDown(); + try { + if (!release.await(5, TimeUnit.SECONDS)) { + throw new IOException("Timed out waiting to release publication gate"); + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted inside publication gate", interrupted); + } + } + return loaded; + }); + ArchiveReaderPublicationGate gate = new ArchiveReaderPublicationGate(fixture.history, + fixture.checkpointSource(), sources, fixture.readerPath, fixture.participants, + manager::withArchiveStateBarrier); + ExecutorService executor = Executors.newFixedThreadPool(3); + try { + Future publication = executor.submit(() -> { + gate.publish(1); + return null; + }); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + Future merge = executor.submit(child::merge); + Future flush = executor.submit(manager::flush); + assertThrows(TimeoutException.class, + () -> merge.get(100, TimeUnit.MILLISECONDS)); + assertThrows(TimeoutException.class, + () -> flush.get(100, TimeUnit.MILLISECONDS)); + + release.countDown(); + publication.get(5, TimeUnit.SECONDS); + merge.get(5, TimeUnit.SECONDS); + flush.get(5, TimeUnit.SECONDS); + assertEquals(1, fixture.reader().getEpoch()); + } finally { + release.countDown(); + child.close(); + parent.close(); + executor.shutdownNow(); + } + } + } + + @Test + public void missingMismatchedOrNullParticipantNeverAdvancesReader() throws Exception { + try (Fixture missing = fixture()) { + Files.delete(missing.participantPaths.get(missing.participants.get(0))); + assertThrows(ArchivePersistenceException.class, + () -> missing.fileGate().publish(1)); + assertEquals(0, missing.reader().getEpoch()); + } + + try (Fixture mismatch = fixture()) { + String participant = mismatch.participants.get(0); + mismatch.store(mismatch.participantPaths.get(participant), + mismatch.envelope(Kind.PARTICIPANT_PROGRESS, participant, 0)); + assertThrows(ArchivePersistenceException.class, + () -> mismatch.fileGate().publish(1)); + assertEquals(0, mismatch.reader().getEpoch()); + } + + try (Fixture absentLevelDb = fixture()) { + Map sources = absentLevelDb.sources(); + sources.put("account", () -> null); + ArchiveReaderPublicationGate gate = new ArchiveReaderPublicationGate(absentLevelDb.history, + absentLevelDb.checkpointSource(), sources, absentLevelDb.readerPath, + absentLevelDb.participants, action -> action.run()); + assertThrows(ArchivePersistenceException.class, () -> gate.publish(1)); + assertEquals(0, absentLevelDb.reader().getEpoch()); + } + } + + @Test + public void secondScanDriftAndRegressionPreserveCurrentReader() throws Exception { + try (Fixture drift = fixture()) { + Map sources = drift.sources(); + String participant = drift.participants.get(0); + ProgressSource stable = sources.get(participant); + AtomicInteger reads = new AtomicInteger(); + sources.put(participant, () -> reads.getAndIncrement() == 0 + ? stable.load() : drift.envelope(Kind.PARTICIPANT_PROGRESS, participant, 0)); + ArchiveReaderPublicationGate gate = new ArchiveReaderPublicationGate(drift.history, + drift.checkpointSource(), sources, drift.readerPath, drift.participants, + action -> action.run()); + assertThrows(ArchivePersistenceException.class, () -> gate.publish(1)); + assertEquals(0, drift.reader().getEpoch()); + } + + try (Fixture regression = fixture()) { + regression.fileGate().publish(1); + regression.writeAuthorities(0); + assertThrows(ArchivePersistenceException.class, + () -> regression.fileGate().publish(0)); + assertEquals(1, regression.reader().getEpoch()); + } + } + + @Test + public void publicationFaultKeepsOldReaderAndRetryPublishesOnce() throws Exception { + try (Fixture fixture = fixture()) { + ArchiveReaderPublicationGate failing = new ArchiveReaderPublicationGate(fixture.history, + fixture.checkpointSource(), fixture.sources(), fixture.readerPath, + fixture.participants, action -> action.run(), temporary -> { + throw new IOException("injected after reader temporary force"); + }); + assertThrows(IOException.class, () -> failing.publish(1)); + assertEquals(0, fixture.reader().getEpoch()); + + fixture.fileGate().publish(1); + assertEquals(1, fixture.reader().getEpoch()); + } + } + + private Fixture fixture() throws Exception { + return new Fixture(temporaryFolder.newFolder().toPath()); + } + + private static final class Fixture implements AutoCloseable { + private final List participants; + private final HistoryCommitStore history; + private final Path checkpointPath; + private final Map participantPaths = new LinkedHashMap<>(); + private final Path readerPath; + private final ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + + private Fixture(Path directory) throws Exception { + participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + java.util.Collections.sort(participants); + history = new HistoryCommitStore(directory, new HistoryCommitMarkerCodec()); + history.commitAll(Arrays.asList(marker(0), marker(1))); + checkpointPath = directory.resolve("progress/checkpoint.progress"); + for (String participant : participants) { + participantPaths.put(participant, + directory.resolve("progress/participants/" + participant + ".progress")); + } + readerPath = directory.resolve("progress/reader.progress"); + writeAuthorities(1); + store(readerPath, envelope(Kind.READER_VISIBLE, null, 0)); + } + + private ArchiveReaderPublicationGate fileGate() { + return ArchiveReaderPublicationGate.forFiles(history, checkpointPath, participantPaths, + readerPath, participants, action -> action.run()); + } + + private ProgressSource checkpointSource() { + return () -> new ArchiveProgressFile(checkpointPath, codec).load(); + } + + private Map sources() { + Map sources = new TreeMap<>(); + participantPaths.forEach((participant, path) -> sources.put(participant, + () -> new ArchiveProgressFile(path, codec).load())); + return sources; + } + + private void writeAuthorities(int epoch) throws IOException { + store(checkpointPath, envelope(Kind.APPLY_CHECKPOINT, null, epoch)); + for (Map.Entry entry : participantPaths.entrySet()) { + store(entry.getValue(), + envelope(Kind.PARTICIPANT_PROGRESS, entry.getKey(), epoch)); + } + } + + private ArchiveProgressEnvelope reader() throws IOException { + return new ArchiveProgressFile(readerPath, codec).load(); + } + + private ArchiveProgressEnvelope envelope(Kind kind, String participant, int epoch) { + HistoryCommitMarker marker = history.get(epoch); + return new ArchiveProgressEnvelope(kind, participant, epoch, + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), participants); + } + + private void store(Path path, ArchiveProgressEnvelope envelope) throws IOException { + new ArchiveProgressFile(path, codec).store(envelope); + } + + private HistoryCommitMarker marker(int epoch) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), + epoch * 1_000L); + HistoryLocation body = new HistoryLocation(0, epoch * 100L, 100, epoch, + bytes(32, epoch + 20)); + HistoryIndexLocation index = new HistoryIndexLocation(epoch * 50L, 50, + bytes(32, epoch + 30)); + return new HistoryCommitMarker(meta, epoch - 1L, body, index, + bytes(16, epoch + 40), participants); + } + + @Override + public void close() throws IOException { + history.close(); + } + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScannerTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScannerTest.java new file mode 100644 index 00000000000..bde40c304a1 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScannerTest.java @@ -0,0 +1,260 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoveryStorage; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; + +public class ArchiveRecoveryAuthorityScannerTest { + + private static final List PARTICIPANTS = Arrays.asList("account", "account-asset"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void composesFreshFileAuthoritiesWithExecutorScan() throws Exception { + try (Fixture fixture = fixture()) { + RecordingStorage storage = new RecordingStorage(fixture.scanner); + RecoveryPlan plan = new ArchiveRecoveryExecutor(storage).recover(); + + assertEquals(Arrays.asList("truncate:10", "replay:account-asset:9-10", "publish:10"), + storage.actions); + assertEquals(3, plan.getActions().size()); + assertEquals(8, plan.getSafeHeadBeforeRecovery()); + } + } + + @Test + public void corruptEnvelopeFailsBeforeFirstRecoveryAction() throws Exception { + try (Fixture fixture = fixture()) { + byte[] corrupt = Files.readAllBytes(fixture.participantPaths.get("account-asset")); + corrupt[corrupt.length - 1] ^= 1; + Files.write(fixture.participantPaths.get("account-asset"), corrupt); + RecordingStorage storage = new RecordingStorage(fixture.scanner); + + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveRecoveryExecutor(storage).recover()); + assertEquals(0, storage.actions.size()); + } + } + + @Test + public void missingOrMismatchedIdentityFailsBeforeFirstRecoveryAction() throws Exception { + try (Fixture missing = fixture()) { + Files.delete(missing.checkpointPath); + RecordingStorage storage = new RecordingStorage(missing.scanner); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveRecoveryExecutor(storage).recover()); + assertEquals(0, storage.actions.size()); + } + + try (Fixture mismatch = fixture()) { + HistoryCommitMarker marker = mismatch.history.get(8); + new ArchiveProgressFile(mismatch.participantPaths.get("account-asset"), + new ArchiveProgressEnvelopeCodec()).store(new ArchiveProgressEnvelope( + Kind.PARTICIPANT_PROGRESS, "account-asset", 8, + marker.getMeta().getBlockHash(), bytes(16, 99), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS)); + RecordingStorage storage = new RecordingStorage(mismatch.scanner); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveRecoveryExecutor(storage).recover()); + assertEquals(0, storage.actions.size()); + } + } + + @Test + public void readerPublishCrashPreservesOldAuthorityAndSecondRecoveryResumes() + throws Exception { + try (Fixture fixture = fixture()) { + ArchiveReaderHeadPublisher failingPublisher = new ArchiveReaderHeadPublisher( + fixture.history, fixture.readerVisiblePath, PARTICIPANTS, temporary -> { + throw new IOException("injected after reader temporary force"); + }); + DurableStorage first = new DurableStorage(fixture, failingPublisher); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveRecoveryExecutor(first).recover()); + assertEquals(Arrays.asList("truncate:10", "replay:account-asset:9-10"), first.actions); + assertEquals(8, new ArchiveProgressFile(fixture.readerVisiblePath, + new ArchiveProgressEnvelopeCodec()).load().getEpoch()); + + RecoverySnapshot afterCrash = fixture.scanner.scan(); + assertEquals(10, afterCrash.getHistoryHead()); + assertEquals(Long.valueOf(10), + afterCrash.getParticipantHeads().get("account-asset")); + assertEquals(8, afterCrash.getReaderVisibleHead()); + + ArchiveReaderHeadPublisher publisher = new ArchiveReaderHeadPublisher( + fixture.history, fixture.readerVisiblePath, PARTICIPANTS); + DurableStorage second = new DurableStorage(fixture, publisher); + new ArchiveRecoveryExecutor(second).recover(); + assertEquals(Arrays.asList("publish:10"), second.actions); + assertEquals(10, fixture.scanner.scan().getReaderVisibleHead()); + + DurableStorage third = new DurableStorage(fixture, publisher); + assertEquals(0, new ArchiveRecoveryExecutor(third).recover().getActions().size()); + assertEquals(0, third.actions.size()); + } + } + + private Fixture fixture() throws Exception { + Path directory = temporaryFolder.newFolder().toPath(); + HistoryCommitStore history = new HistoryCommitStore(directory, + new HistoryCommitMarkerCodec()); + List markers = new ArrayList<>(); + for (long epoch = 8; epoch <= 12; epoch++) { + markers.add(marker(epoch)); + } + history.commitAll(markers); + + Path checkpointPath = directory.resolve("progress/checkpoint.progress"); + Map participantPaths = new LinkedHashMap<>(); + participantPaths.put("account", directory.resolve("progress/account.progress")); + participantPaths.put("account-asset", + directory.resolve("progress/account-asset.progress")); + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + new ArchiveProgressFile(checkpointPath, codec).store( + envelope(Kind.APPLY_CHECKPOINT, null, history.get(10))); + new ArchiveProgressFile(participantPaths.get("account"), codec).store( + envelope(Kind.PARTICIPANT_PROGRESS, "account", history.get(10))); + new ArchiveProgressFile(participantPaths.get("account-asset"), codec).store( + envelope(Kind.PARTICIPANT_PROGRESS, "account-asset", history.get(8))); + Path readerVisiblePath = directory.resolve("progress/reader-visible.progress"); + new ArchiveProgressFile(readerVisiblePath, codec).store( + envelope(Kind.READER_VISIBLE, null, history.get(8))); + ArchiveRecoveryAuthorityScanner scanner = new ArchiveRecoveryAuthorityScanner(history, + checkpointPath, participantPaths, readerVisiblePath, PARTICIPANTS); + return new Fixture(history, scanner, checkpointPath, participantPaths, readerVisiblePath); + } + + private static ArchiveProgressEnvelope envelope(Kind kind, String participant, + HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); + } + + private static HistoryCommitMarker marker(long epoch) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, bytes(32, (int) epoch), + bytes(32, (int) epoch - 1), epoch * 1_000); + HistoryLocation body = new HistoryLocation(0, epoch * 100, 100, (int) epoch, + bytes(32, (int) epoch + 20)); + HistoryIndexLocation index = new HistoryIndexLocation(epoch * 50, 50, + bytes(32, (int) epoch + 30)); + return new HistoryCommitMarker(meta, epoch - 1, body, index, + bytes(16, (int) epoch + 40), PARTICIPANTS); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } + + private static final class RecordingStorage implements RecoveryStorage { + private final ArchiveRecoveryAuthorityScanner scanner; + private final List actions = new ArrayList<>(); + + private RecordingStorage(ArchiveRecoveryAuthorityScanner scanner) { + this.scanner = scanner; + } + + @Override + public RecoverySnapshot scan() throws IOException { + return scanner.scan(); + } + + @Override + public void truncateHistoryAndSync(long historyHead) { + actions.add("truncate:" + historyHead); + } + + @Override + public void replayParticipantAndSyncProgress(String participant, long firstEpoch, + long lastEpoch) { + actions.add("replay:" + participant + ":" + firstEpoch + "-" + lastEpoch); + } + + @Override + public void publishReaderHeadAndSync(long readerVisibleHead) { + actions.add("publish:" + readerVisibleHead); + } + } + + private static final class DurableStorage implements RecoveryStorage { + private final Fixture fixture; + private final ArchiveReaderHeadPublisher publisher; + private final List actions = new ArrayList<>(); + + private DurableStorage(Fixture fixture, ArchiveReaderHeadPublisher publisher) { + this.fixture = fixture; + this.publisher = publisher; + } + + @Override + public RecoverySnapshot scan() throws IOException { + return fixture.scanner.scan(); + } + + @Override + public void truncateHistoryAndSync(long historyHead) throws IOException { + HistoryCommitMarker head = fixture.history.head(); + while (head != null && head.getMeta().getEpoch() > historyHead) { + fixture.history.removeHead(head.getMeta()); + head = fixture.history.head(); + } + actions.add("truncate:" + historyHead); + } + + @Override + public void replayParticipantAndSyncProgress(String participant, long firstEpoch, + long lastEpoch) throws IOException { + new ArchiveProgressFile(fixture.participantPaths.get(participant), + new ArchiveProgressEnvelopeCodec()).store(envelope( + Kind.PARTICIPANT_PROGRESS, participant, fixture.history.get(lastEpoch))); + actions.add("replay:" + participant + ":" + firstEpoch + "-" + lastEpoch); + } + + @Override + public void publishReaderHeadAndSync(long readerVisibleHead) throws IOException { + publisher.publish(readerVisibleHead); + actions.add("publish:" + readerVisibleHead); + } + } + + private static final class Fixture implements AutoCloseable { + private final HistoryCommitStore history; + private final ArchiveRecoveryAuthorityScanner scanner; + private final Path checkpointPath; + private final Map participantPaths; + private final Path readerVisiblePath; + + private Fixture(HistoryCommitStore history, ArchiveRecoveryAuthorityScanner scanner, + Path checkpointPath, Map participantPaths, Path readerVisiblePath) { + this.history = history; + this.scanner = scanner; + this.checkpointPath = checkpointPath; + this.participantPaths = participantPaths; + this.readerVisiblePath = readerVisiblePath; + } + + @Override + public void close() throws IOException { + history.close(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java b/framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java new file mode 100644 index 00000000000..cdb0fcd1171 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java @@ -0,0 +1,125 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.LevelDbArchiveParticipant.Mutation; +import org.tron.core.db2.archive.LevelDbArchiveParticipant.Stage; + +public class LevelDbArchiveParticipantTest { + + private static final List PARTICIPANTS = + Arrays.asList("account", "account-asset"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void nativeBatchExposesOnlyOldOldOrNewNewAcrossFailureBoundaries() throws Exception { + for (Stage failedStage : Stage.values()) { + Path directory = temporaryFolder.newFolder("native-" + failedStage).toPath(); + try (LevelDbArchiveParticipant participant = new LevelDbArchiveParticipant( + directory, "account", PARTICIPANTS)) { + participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("old"))), + progress(1)); + } + + try (LevelDbArchiveParticipant failing = new LevelDbArchiveParticipant( + directory, "account", PARTICIPANTS, stage -> failAt(failedStage, stage))) { + assertThrows(IOException.class, () -> failing.apply( + Collections.singletonList(Mutation.put(bytes("key"), bytes("new"))), progress(2))); + } + + try (LevelDbArchiveParticipant reopened = new LevelDbArchiveParticipant( + directory, "account", PARTICIPANTS)) { + long expectedEpoch = failedStage == Stage.BEFORE_WRITE ? 1 : 2; + byte[] expectedValue = failedStage == Stage.BEFORE_WRITE ? bytes("old") : bytes("new"); + assertEquals(expectedEpoch, reopened.loadProgress().getEpoch()); + assertArrayEquals(expectedValue, reopened.get(bytes("key"))); + } + } + } + + @Test + public void deleteAndProgressShareTheSameNativeBatch() throws Exception { + Path directory = temporaryFolder.newFolder("native-delete").toPath(); + try (LevelDbArchiveParticipant participant = new LevelDbArchiveParticipant( + directory, "account", PARTICIPANTS)) { + participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("value"))), + progress(1)); + participant.apply(Collections.singletonList(Mutation.delete(bytes("key"))), progress(2)); + assertNull(participant.get(bytes("key"))); + assertEquals(2, participant.loadProgress().getEpoch()); + } + } + + @Test + public void resetClearsBusinessAndProgressBeforeAConsistentReapply() throws Exception { + Path directory = temporaryFolder.newFolder("native-reset").toPath(); + try (LevelDbArchiveParticipant participant = new LevelDbArchiveParticipant( + directory, "account", PARTICIPANTS)) { + participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("old"))), + progress(1)); + participant.reset(); + assertNull(participant.get(bytes("key"))); + assertThrows(ArchivePersistenceException.class, participant::loadProgress); + + participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("new"))), + progress(2)); + } + + try (LevelDbArchiveParticipant reopened = new LevelDbArchiveParticipant( + directory, "account", PARTICIPANTS)) { + assertArrayEquals(bytes("new"), reopened.get(bytes("key"))); + assertEquals(2, reopened.loadProgress().getEpoch()); + } + } + + @Test + public void rejectsProgressForAnotherParticipantBeforeNativeWrite() throws Exception { + Path directory = temporaryFolder.newFolder("native-identity").toPath(); + try (LevelDbArchiveParticipant participant = new LevelDbArchiveParticipant( + directory, "account", PARTICIPANTS)) { + ArchiveProgressEnvelope wrong = new ArchiveProgressEnvelope( + ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, "account-asset", 1, + bytes(32, 1), bytes(16, 2), bytes(32, 3), PARTICIPANTS); + assertThrows(IllegalArgumentException.class, () -> participant.apply( + Collections.singletonList(Mutation.put(bytes("key"), bytes("value"))), wrong)); + assertNull(participant.get(bytes("key"))); + assertThrows(ArchivePersistenceException.class, participant::loadProgress); + } + } + + private static ArchiveProgressEnvelope progress(long epoch) { + return new ArchiveProgressEnvelope(ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, + "account", epoch, bytes(32, (int) epoch), bytes(16, (int) epoch + 10), + bytes(32, (int) epoch + 20), PARTICIPANTS); + } + + private static void failAt(Stage failedStage, Stage currentStage) throws IOException { + if (currentStage == failedStage) { + throw new IOException("injected at " + currentStage); + } + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java b/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java new file mode 100644 index 00000000000..c1d07661cb6 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java @@ -0,0 +1,163 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.RocksDbArchiveParticipant.Mutation; +import org.tron.core.db2.archive.RocksDbArchiveParticipant.Stage; + +public class RocksDbArchiveParticipantTest { + + private static final List PARTICIPANTS = + Arrays.asList("account", "account-asset"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void nativeBatchExposesOnlyOldOldOrNewNewAcrossFailureBoundaries() throws Exception { + for (Stage failedStage : Stage.values()) { + Path directory = temporaryFolder.newFolder("native-" + failedStage).toPath(); + try (RocksDbArchiveParticipant participant = new RocksDbArchiveParticipant( + directory, "account", PARTICIPANTS)) { + participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("old"))), + progress(1)); + } + + try (RocksDbArchiveParticipant failing = new RocksDbArchiveParticipant( + directory, "account", PARTICIPANTS, stage -> failAt(failedStage, stage))) { + assertThrows(IOException.class, () -> failing.apply( + Collections.singletonList(Mutation.put(bytes("key"), bytes("new"))), progress(2))); + } + + try (RocksDbArchiveParticipant reopened = new RocksDbArchiveParticipant( + directory, "account", PARTICIPANTS)) { + long expectedEpoch = failedStage == Stage.BEFORE_WRITE ? 1 : 2; + byte[] expectedValue = failedStage == Stage.BEFORE_WRITE ? bytes("old") : bytes("new"); + assertEquals(expectedEpoch, reopened.loadProgress().getEpoch()); + assertArrayEquals(expectedValue, reopened.get(bytes("key"))); + } + } + } + + @Test + public void deleteAndProgressShareTheSameNativeBatch() throws Exception { + Path directory = temporaryFolder.newFolder("native-delete").toPath(); + try (RocksDbArchiveParticipant participant = new RocksDbArchiveParticipant( + directory, "account", PARTICIPANTS)) { + participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("value"))), + progress(1)); + participant.apply(Collections.singletonList(Mutation.delete(bytes("key"))), progress(2)); + assertNull(participant.get(bytes("key"))); + assertEquals(2, participant.loadProgress().getEpoch()); + } + } + + @Test + public void rejectsProgressForAnotherParticipantBeforeNativeWrite() throws Exception { + Path directory = temporaryFolder.newFolder("native-identity").toPath(); + try (RocksDbArchiveParticipant participant = new RocksDbArchiveParticipant( + directory, "account", PARTICIPANTS)) { + ArchiveProgressEnvelope wrong = new ArchiveProgressEnvelope( + ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, "account-asset", 1, + bytes(32, 1), bytes(16, 2), bytes(32, 3), PARTICIPANTS); + assertThrows(IllegalArgumentException.class, () -> participant.apply( + Collections.singletonList(Mutation.put(bytes("key"), bytes("value"))), wrong)); + assertNull(participant.get(bytes("key"))); + assertThrows(ArchivePersistenceException.class, participant::loadProgress); + } + } + + @Test + public void recoveryScannerReadsParticipantProgressFromNativeEngines() throws Exception { + Path directory = temporaryFolder.newFolder("native-scanner").toPath(); + Path checkpointPath = directory.resolve("progress/checkpoint.progress"); + Path readerPath = directory.resolve("progress/reader.progress"); + try (HistoryCommitStore history = new HistoryCommitStore(directory, + new HistoryCommitMarkerCodec()); + RocksDbArchiveParticipant account = new RocksDbArchiveParticipant( + directory.resolve("account-engine"), "account", PARTICIPANTS); + RocksDbArchiveParticipant asset = new RocksDbArchiveParticipant( + directory.resolve("asset-engine"), "account-asset", PARTICIPANTS)) { + HistoryCommitMarker first = marker(1); + HistoryCommitMarker second = marker(2); + history.commitAll(Arrays.asList(first, second)); + new ArchiveProgressFile(checkpointPath, new ArchiveProgressEnvelopeCodec()) + .store(globalProgress(ArchiveProgressEnvelope.Kind.APPLY_CHECKPOINT, second)); + new ArchiveProgressFile(readerPath, new ArchiveProgressEnvelopeCodec()) + .store(globalProgress(ArchiveProgressEnvelope.Kind.READER_VISIBLE, first)); + account.apply(Collections.emptyList(), participantProgress("account", second)); + asset.apply(Collections.emptyList(), participantProgress("account-asset", first)); + Map engines = new LinkedHashMap<>(); + engines.put("account", account); + engines.put("account-asset", asset); + + ArchiveRecoveryExecutor.RecoverySnapshot snapshot = + ArchiveRecoveryAuthorityScanner.forRocksDbParticipants(history, checkpointPath, + engines, readerPath, PARTICIPANTS).scan(); + assertEquals(2, snapshot.getHistoryHead()); + assertEquals(2, snapshot.getCheckpointHead()); + assertEquals(Long.valueOf(2), snapshot.getParticipantHeads().get("account")); + assertEquals(Long.valueOf(1), snapshot.getParticipantHeads().get("account-asset")); + assertEquals(1, snapshot.getReaderVisibleHead()); + } + } + + private static ArchiveProgressEnvelope progress(long epoch) { + return new ArchiveProgressEnvelope(ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, + "account", epoch, bytes(32, (int) epoch), bytes(16, (int) epoch + 10), + bytes(32, (int) epoch + 20), PARTICIPANTS); + } + + private static HistoryCommitMarker marker(long epoch) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, bytes(32, (int) epoch), + bytes(32, (int) epoch - 1), epoch * 1_000); + return new HistoryCommitMarker(meta, epoch - 1, + new HistoryLocation(0, epoch * 100, 80, (int) epoch, bytes(32, (int) epoch + 20)), + new HistoryIndexLocation(epoch * 50, 50, bytes(32, (int) epoch + 30)), + bytes(16, (int) epoch + 40), PARTICIPANTS); + } + + private static ArchiveProgressEnvelope participantProgress(String participant, + HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, + participant, marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), + marker.getBatchId(), marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private static ArchiveProgressEnvelope globalProgress(ArchiveProgressEnvelope.Kind kind, + HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private static void failAt(Stage failedStage, Stage currentStage) throws IOException { + if (currentStage == failedStage) { + throw new IOException("injected at " + currentStage); + } + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorageTest.java b/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorageTest.java new file mode 100644 index 00000000000..0026dd77f27 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorageTest.java @@ -0,0 +1,138 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.RocksDbArchiveParticipant.Mutation; + +public class RocksDbArchiveRecoveryStorageTest { + + private static final List PARTICIPANTS = + Arrays.asList("account", "account-asset"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void executorConvergesHistoryNativeParticipantsAndReaderHead() throws Exception { + Path archive = temporaryFolder.newFolder("native-recovery").toPath(); + Path checkpointPath = archive.resolve("progress/checkpoint.progress"); + Path readerPath = archive.resolve("progress/reader.progress"); + List markers = initializeHistory(archive, 3); + new ArchiveProgressFile(checkpointPath, new ArchiveProgressEnvelopeCodec()) + .store(global(Kind.APPLY_CHECKPOINT, markers.get(1))); + new ArchiveProgressFile(readerPath, new ArchiveProgressEnvelopeCodec()) + .store(global(Kind.READER_VISIBLE, markers.get(0))); + + try (RocksDbArchiveParticipant account = new RocksDbArchiveParticipant( + archive.resolve("participants/account"), "account", PARTICIPANTS); + RocksDbArchiveParticipant asset = new RocksDbArchiveParticipant( + archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS)) { + account.apply(Collections.emptyList(), participant("account", markers.get(1))); + asset.apply(Collections.emptyList(), participant("account-asset", markers.get(0))); + Map engines = engines(account, asset); + + try (RocksDbArchiveRecoveryStorage storage = new RocksDbArchiveRecoveryStorage( + archive, 4096, checkpointPath, engines, readerPath, PARTICIPANTS, + (name, first, last) -> Collections.singletonList( + Mutation.put(bytes("replayed"), bytes(name + ":" + first + "-" + last))))) { + assertEquals(3, new ArchiveRecoveryExecutor(storage).recover().getActions().size()); + } + + assertEquals(2, asset.loadProgress().getEpoch()); + assertArrayEquals(bytes("account-asset:2-2"), asset.get(bytes("replayed"))); + assertEquals(2, new ArchiveProgressFile(readerPath, + new ArchiveProgressEnvelopeCodec()).load().getEpoch()); + assertEquals(2, ArchiveRestartCheckpoint.load(archive, + new HistoryCommitMarkerCodec()).getMarker().getMeta().getEpoch()); + assertFalse(Files.exists(archive.resolve("truncation.intent"))); + + try (RocksDbArchiveRecoveryStorage reopened = new RocksDbArchiveRecoveryStorage( + archive, 4096, checkpointPath, engines, readerPath, PARTICIPANTS, + (name, first, last) -> { + throw new AssertionError("fixed-point recovery must not replay"); + })) { + assertEquals(0, new ArchiveRecoveryExecutor(reopened).recover().getActions().size()); + } + } + } + + private static List initializeHistory(Path archive, int lastEpoch) + throws Exception { + List markers = new ArrayList<>(); + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096); + HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); + HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + for (int epoch = 1; epoch <= lastEpoch; epoch++) { + BlockReverseDiff diff = new BlockReverseDiff( + new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L), + Collections.singletonList(new BlockReverseDiff.DbGroup("account", + Collections.singletonList(new BlockReverseDiff.Entry(bytes("key-" + epoch), + OldValue.present(bytes("old-" + epoch))))))); + HistoryLocation body = bodies.append(diff); + HistoryIndexLocation location = index.append(HistoryIndexRecord.from(diff, body)); + markers.add(new HistoryCommitMarker(diff.getMeta(), epoch - 1L, body, location, + bytes(16, epoch + 40), PARTICIPANTS)); + } + bodies.sync(); + index.sync(); + commits.commitAll(markers); + ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), + commits.getRecordLength(), commits.head(), new HistoryCommitMarkerCodec()); + } + return markers; + } + + private static Map engines( + RocksDbArchiveParticipant account, RocksDbArchiveParticipant asset) { + Map engines = new LinkedHashMap<>(); + engines.put("account", account); + engines.put("account-asset", asset); + return engines; + } + + private static ArchiveProgressEnvelope participant(String name, + HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, name, + marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } +} From ba17d263815d683db0b04380c5ab24473b3c06eb Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 12:01:37 +0800 Subject: [PATCH 013/161] feat(chainbase): coordinate archive recovery Unify LevelDB and RocksDB participants behind one recovery contract. Persist canonical mutation plans and bind checkpoint, participant, and reader progress to the same digest. --- .../core/db2/archive/ArchiveParticipant.java | 11 + .../archive/ArchiveParticipantMutation.java | 32 ++ .../ArchiveParticipantMutationBatch.java | 94 ++++++ .../ArchiveParticipantProgressSource.java | 10 + .../ArchiveParticipantRecoveryStorage.java | 273 +++++++++++++++++ .../db2/archive/ArchiveProgressEnvelope.java | 22 ++ .../archive/ArchiveProgressEnvelopeCodec.java | 16 +- .../archive/ArchiveReaderHeadPublisher.java | 6 +- .../archive/ArchiveReaderPublicationGate.java | 51 +++- .../ArchiveRecoveryAuthorityScanner.java | 29 +- .../db2/archive/ArchiveRecoveryExecutor.java | 4 + .../db2/archive/ArchiveRecoveryScanner.java | 13 + .../ArchiveTargetApplyCoordinator.java | 194 ++++++++++++ .../archive/ArchiveTargetMutationPlan.java | 80 +++++ .../ArchiveTargetMutationPlanBuilder.java | 69 +++++ .../ArchiveTargetMutationPlanCodec.java | 156 ++++++++++ .../ArchiveTargetMutationPlanFile.java | 97 ++++++ .../archive/LevelDbArchiveParticipant.java | 34 +-- .../archive/RocksDbArchiveParticipant.java | 34 +-- .../RocksDbArchiveRecoveryStorage.java | 178 ----------- .../ArchiveMixedEngineProgressSourceTest.java | 195 +++++++++++++ ...ArchiveParticipantRecoveryStorageTest.java | 243 +++++++++++++++ .../archive/ArchiveProgressEnvelopeTest.java | 14 + .../ArchiveReaderPublicationGateTest.java | 56 +++- .../ArchiveTargetApplyCoordinatorTest.java | 276 ++++++++++++++++++ .../ArchiveTargetMutationPlanBuilderTest.java | 193 ++++++++++++ .../ArchiveTargetMutationPlanFileTest.java | 134 +++++++++ .../LevelDbArchiveParticipantTest.java | 22 +- .../RocksDbArchiveParticipantTest.java | 18 +- .../RocksDbArchiveRecoveryStorageTest.java | 138 --------- 30 files changed, 2267 insertions(+), 425 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipant.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutation.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantProgressSource.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlan.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanCodec.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFile.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorage.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveMixedEngineProgressSourceTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorageTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipant.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipant.java new file mode 100644 index 00000000000..8c00eefbadc --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipant.java @@ -0,0 +1,11 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.util.List; + +/** Engine-neutral archive participant with one atomic business+D apply boundary. */ +public interface ArchiveParticipant extends ArchiveParticipantProgressSource { + + void apply(List mutations, ArchiveProgressEnvelope progress) + throws IOException; +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutation.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutation.java new file mode 100644 index 00000000000..615819f0f32 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutation.java @@ -0,0 +1,32 @@ +package org.tron.core.db2.archive; + +import java.util.Arrays; +import java.util.Objects; + +/** Immutable engine-neutral business mutation for one archive participant batch. */ +public final class ArchiveParticipantMutation { + + private final byte[] key; + private final byte[] value; + + private ArchiveParticipantMutation(byte[] key, byte[] value) { + this.key = Arrays.copyOf(Objects.requireNonNull(key, "key"), key.length); + this.value = value == null ? null : Arrays.copyOf(value, value.length); + } + + public static ArchiveParticipantMutation put(byte[] key, byte[] value) { + return new ArchiveParticipantMutation(key, Objects.requireNonNull(value, "value")); + } + + public static ArchiveParticipantMutation delete(byte[] key) { + return new ArchiveParticipantMutation(key, null); + } + + byte[] getKey() { + return Arrays.copyOf(key, key.length); + } + + byte[] getValue() { + return value == null ? null : Arrays.copyOf(value, value.length); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java new file mode 100644 index 00000000000..d875d3af941 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java @@ -0,0 +1,94 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** Immutable producer payload for one committed target's exact physical participant mutations. */ +public final class ArchiveParticipantMutationBatch { + + private final long targetEpoch; + private final byte[] blockHash; + private final byte[] batchId; + private final byte[] historyPayloadDigest; + private final List participants; + private final List mutations; + + public ArchiveParticipantMutationBatch(HistoryCommitMarker target, List mutations) { + HistoryCommitMarker checkedTarget = Objects.requireNonNull(target, "target"); + targetEpoch = checkedTarget.getMeta().getEpoch(); + blockHash = checkedTarget.getMeta().getBlockHash(); + batchId = checkedTarget.getBatchId(); + historyPayloadDigest = checkedTarget.getHistoryLocation().getBodyDigest(); + participants = Collections.unmodifiableList( + new ArrayList<>(checkedTarget.getDatabases())); + List copy = new ArrayList<>(Objects.requireNonNull(mutations, "mutations")); + if (copy.contains(null)) { + throw new IllegalArgumentException("Participant mutation batch contains null mutation"); + } + this.mutations = Collections.unmodifiableList(copy); + } + + public long getTargetEpoch() { + return targetEpoch; + } + + byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + byte[] getBatchId() { + return Arrays.copyOf(batchId, batchId.length); + } + + byte[] getHistoryPayloadDigest() { + return Arrays.copyOf(historyPayloadDigest, historyPayloadDigest.length); + } + + List getParticipants() { + return participants; + } + + List getMutations() { + return mutations; + } + + /** One immutable put/delete against an exact participant physical key. */ + public static final class Mutation { + private final String dbName; + private final byte[] physicalRawKey; + private final byte[] value; + + private Mutation(String dbName, byte[] physicalRawKey, byte[] value) { + if (dbName == null || dbName.isEmpty()) { + throw new IllegalArgumentException("Participant mutation dbName must not be empty"); + } + this.dbName = dbName; + this.physicalRawKey = Arrays.copyOf( + Objects.requireNonNull(physicalRawKey, "physicalRawKey"), physicalRawKey.length); + this.value = value == null ? null : Arrays.copyOf(value, value.length); + } + + public static Mutation put(String dbName, byte[] physicalRawKey, byte[] value) { + return new Mutation(dbName, physicalRawKey, Objects.requireNonNull(value, "value")); + } + + public static Mutation delete(String dbName, byte[] physicalRawKey) { + return new Mutation(dbName, physicalRawKey, null); + } + + String getDbName() { + return dbName; + } + + byte[] getPhysicalRawKey() { + return Arrays.copyOf(physicalRawKey, physicalRawKey.length); + } + + byte[] getValue() { + return value == null ? null : Arrays.copyOf(value, value.length); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantProgressSource.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantProgressSource.java new file mode 100644 index 00000000000..7692ccad814 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantProgressSource.java @@ -0,0 +1,10 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; + +/** Engine-neutral durable participant D authority source. */ +@FunctionalInterface +public interface ArchiveParticipantProgressSource { + + ArchiveProgressEnvelope loadProgress() throws IOException; +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java new file mode 100644 index 00000000000..37537a40dd6 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java @@ -0,0 +1,273 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoveryStorage; + +/** File history plus mixed native participants implementing the H/C/D[i]/R executor. */ +public final class ArchiveParticipantRecoveryStorage implements RecoveryStorage, Closeable { + + private final Path archiveDirectory; + private final long maxSegmentSize; + private final List participants; + private final Map participantEngines; + private final ArchiveProgressFile checkpointFile; + private final ArchiveTargetMutationPlanFile mutationPlanFile; + private final HistorySegmentStore bodies; + private final HistoryIndexStore index; + private final HistoryCommitStore history; + private final ArchiveRecoveryAuthorityScanner scanner; + private final ArchiveReaderPublicationGate publicationGate; + private final ArchiveStateBarrier.ArchiveStateAction refresh; + + public ArchiveParticipantRecoveryStorage(Path archiveDirectory, long maxSegmentSize, + Path checkpointPath, Map participantEngines, + Path readerVisiblePath, List participants) + throws IOException { + this(archiveDirectory, maxSegmentSize, checkpointPath, participantEngines, + readerVisiblePath, participants, action -> action.run(), () -> { }); + } + + public ArchiveParticipantRecoveryStorage(Path archiveDirectory, long maxSegmentSize, + Path checkpointPath, Map participantEngines, + Path readerVisiblePath, List participants, ArchiveStateBarrier barrier, + ArchiveStateBarrier.ArchiveStateAction refresh) + throws IOException { + this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + if (maxSegmentSize <= 0) { + throw new IllegalArgumentException("maxSegmentSize must be positive"); + } + this.maxSegmentSize = maxSegmentSize; + this.participants = validateParticipants(participants); + TreeMap sorted = new TreeMap<>( + Objects.requireNonNull(participantEngines, "participantEngines")); + if (!new ArrayList<>(sorted.keySet()).equals(this.participants) + || sorted.containsValue(null)) { + throw new IllegalArgumentException("Archive participant engine set mismatch"); + } + this.participantEngines = Collections.unmodifiableMap(sorted); + ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); + this.checkpointFile = new ArchiveProgressFile(checkpointPath, progressCodec); + this.mutationPlanFile = new ArchiveTargetMutationPlanFile(checkpointPath); + new ArchiveTruncationRecovery(archiveDirectory, maxSegmentSize).recover(); + ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archiveDirectory, + new HistoryCommitMarkerCodec()); + if (checkpoint == null) { + throw new ArchivePersistenceException("Archive restart checkpoint is missing"); + } + HistorySegmentStore openedBodies = null; + HistoryIndexStore openedIndex = null; + HistoryCommitStore openedHistory = null; + try { + openedBodies = new HistorySegmentStore(archiveDirectory, new BlockHistoryCodec(), + maxSegmentSize, checkpoint); + openedIndex = new HistoryIndexStore(archiveDirectory, new HistoryIndexCodec(), checkpoint); + openedHistory = new HistoryCommitStore(archiveDirectory, new HistoryCommitMarkerCodec(), + checkpoint); + } catch (IOException | RuntimeException failure) { + close(openedIndex, failure); + close(openedBodies, failure); + close(openedHistory, failure); + throw failure; + } + this.bodies = openedBodies; + this.index = openedIndex; + this.history = openedHistory; + this.scanner = ArchiveRecoveryAuthorityScanner.forParticipants(this.history, + checkpointPath, this.participantEngines, readerVisiblePath, this.participants); + this.publicationGate = new ArchiveReaderPublicationGate(this.history, + checkpointFile::load, + this.participantEngines, readerVisiblePath, this.participants, + Objects.requireNonNull(barrier, "barrier")); + this.refresh = Objects.requireNonNull(refresh, "refresh"); + } + + @Override + public RecoverySnapshot scan() throws IOException { + RecoverySnapshot snapshot = scanner.scan(); + ArchiveTargetMutationPlan plan = mutationPlanFile.loadIfPresent(); + boolean fixed = isFixed(snapshot); + if (plan == null) { + if (!authoritiesAtCheckpoint(snapshot)) { + throw new ArchivePersistenceException("Archive recovery mutation plan is missing"); + } + return snapshot; + } + long planEpoch = plan.getTarget().getEpoch(); + HistoryCommitMarker marker = history.get(planEpoch); + if (marker != null) { + plan.requireIdentity(marker, participants); + } + long checkpoint = snapshot.getCheckpointHead(); + boolean preparedOnly = planEpoch == checkpoint + 1 && authoritiesAtCheckpoint(snapshot); + if (planEpoch != checkpoint && !preparedOnly || marker == null && !preparedOnly) { + throw new ArchivePersistenceException("Mutation plan does not match recovery checkpoint"); + } + if (!preparedOnly) { + requirePlanDigest(plan, checkpointFile.load()); + } + return snapshot; + } + + @Override + public void truncateHistoryAndSync(long historyHead) throws IOException { + ArchiveTruncationIntent.prepare(archiveDirectory, history, index, bodies, historyHead, + new HistoryCommitMarkerCodec()); + new ArchiveTruncationRecovery(archiveDirectory, maxSegmentSize).recover(); + history.truncateAfter(historyHead); + } + + @Override + public void replayParticipantAndSyncProgress(String participant, long firstEpoch, + long lastEpoch) throws IOException { + ArchiveParticipant engine = participantEngines.get(participant); + if (engine == null) { + throw new ArchivePersistenceException("Unknown archive recovery participant: " + participant); + } + HistoryCommitMarker marker = history.get(lastEpoch); + if (marker == null || firstEpoch > lastEpoch) { + throw new ArchivePersistenceException("Archive participant replay range is invalid"); + } + ArchiveTargetMutationPlan plan = mutationPlanFile.loadRequired(); + plan.requireIdentity(marker, participants); + if (plan.getTarget().getEpoch() != lastEpoch) { + throw new ArchivePersistenceException("Mutation plan does not cover replay range"); + } + byte[] mutationPlanDigest = requirePlanDigest(plan, checkpointFile.load()); + List mutations = plan.getMutations(participant); + ArchiveProgressEnvelope progress = new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, + participant, lastEpoch, marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, participants); + engine.apply(mutations, progress); + } + + @Override + public void publishReaderHeadAndSync(long readerVisibleHead) throws IOException { + publicationGate.publishAfterRefresh(readerVisibleHead, refresh); + } + + @Override + public void recoveryComplete() throws IOException { + RecoverySnapshot snapshot = scanner.scan(); + if (!isFixed(snapshot)) { + throw new ArchivePersistenceException("Archive recovery did not reach a fixed point"); + } + ArchiveTargetMutationPlan plan = mutationPlanFile.loadIfPresent(); + if (plan != null) { + long epoch = plan.getTarget().getEpoch(); + HistoryCommitMarker marker = history.get(epoch); + if (marker != null) { + plan.requireIdentity(marker, participants); + } + if (epoch != snapshot.getCheckpointHead() && epoch != snapshot.getCheckpointHead() + 1) { + throw new ArchivePersistenceException("Completed recovery has an unrelated mutation plan"); + } + if (epoch == snapshot.getCheckpointHead()) { + requirePlanDigest(plan, checkpointFile.load()); + } + } + mutationPlanFile.retire(); + } + + @Override + public void close() throws IOException { + IOException failure = null; + try { + index.close(); + } catch (IOException closeFailure) { + failure = closeFailure; + } + try { + bodies.close(); + } catch (IOException closeFailure) { + failure = add(failure, closeFailure); + } + try { + history.close(); + } catch (IOException closeFailure) { + failure = add(failure, closeFailure); + } + if (failure != null) { + throw failure; + } + } + + private static List validateParticipants(List participants) { + List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); + if (copy.isEmpty()) { + throw new IllegalArgumentException("Archive participant set must not be empty"); + } + String previous = null; + for (String participant : copy) { + if (participant == null || participant.isEmpty() + || previous != null && previous.compareTo(participant) >= 0) { + throw new IllegalArgumentException( + "Archive participants must be non-empty, unique, and sorted"); + } + previous = participant; + } + return Collections.unmodifiableList(copy); + } + + private static IOException add(IOException current, IOException addition) { + if (current == null) { + return addition; + } + current.addSuppressed(addition); + return current; + } + + private static boolean isFixed(RecoverySnapshot snapshot) { + long checkpoint = snapshot.getCheckpointHead(); + if (snapshot.getHistoryHead() != checkpoint + || !authoritiesAtCheckpoint(snapshot)) { + return false; + } + return true; + } + + private static boolean authoritiesAtCheckpoint(RecoverySnapshot snapshot) { + long checkpoint = snapshot.getCheckpointHead(); + if (snapshot.getReaderVisibleHead() != checkpoint) { + return false; + } + for (long participant : snapshot.getParticipantHeads().values()) { + if (participant != checkpoint) { + return false; + } + } + return true; + } + + private static byte[] requirePlanDigest(ArchiveTargetMutationPlan plan, + ArchiveProgressEnvelope checkpoint) { + byte[] actual = plan.digest(); + if (!Arrays.equals(actual, checkpoint.getMutationPlanDigest())) { + throw new ArchivePersistenceException( + "Archive checkpoint mutation-plan digest mismatch"); + } + return actual; + } + + private static void close(Closeable resource, Exception failure) { + if (resource == null) { + return; + } + try { + resource.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java index d94647c5889..92514ff3700 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java @@ -21,10 +21,17 @@ public enum Kind { private final byte[] blockHash; private final byte[] batchId; private final byte[] payloadDigest; + private final byte[] mutationPlanDigest; private final List participants; public ArchiveProgressEnvelope(Kind kind, String participant, long epoch, byte[] blockHash, byte[] batchId, byte[] payloadDigest, List participants) { + this(kind, participant, epoch, blockHash, batchId, payloadDigest, null, participants); + } + + public ArchiveProgressEnvelope(Kind kind, String participant, long epoch, byte[] blockHash, + byte[] batchId, byte[] payloadDigest, byte[] mutationPlanDigest, + List participants) { this.kind = Objects.requireNonNull(kind, "kind"); if (epoch < 0) { throw new IllegalArgumentException("Archive progress epoch must be non-negative"); @@ -33,6 +40,8 @@ public ArchiveProgressEnvelope(Kind kind, String participant, long epoch, byte[] this.blockHash = exactBytes(blockHash, 32, "blockHash"); this.batchId = exactBytes(batchId, 16, "batchId"); this.payloadDigest = exactBytes(payloadDigest, 32, "payloadDigest"); + this.mutationPlanDigest = mutationPlanDigest == null ? null + : exactBytes(mutationPlanDigest, 32, "mutationPlanDigest"); this.participants = validateParticipants(participants); if (kind != Kind.PARTICIPANT_PROGRESS) { if (participant != null) { @@ -72,6 +81,11 @@ public byte[] getPayloadDigest() { return Arrays.copyOf(payloadDigest, payloadDigest.length); } + public byte[] getMutationPlanDigest() { + return mutationPlanDigest == null ? null + : Arrays.copyOf(mutationPlanDigest, mutationPlanDigest.length); + } + public List getParticipants() { return participants; } @@ -79,10 +93,18 @@ public List getParticipants() { public void requireIdentity(Kind expectedKind, String expectedParticipant, long expectedEpoch, byte[] expectedBlockHash, byte[] expectedBatchId, byte[] expectedPayloadDigest, List expectedParticipants) { + requireIdentity(expectedKind, expectedParticipant, expectedEpoch, expectedBlockHash, + expectedBatchId, expectedPayloadDigest, mutationPlanDigest, expectedParticipants); + } + + public void requireIdentity(Kind expectedKind, String expectedParticipant, long expectedEpoch, + byte[] expectedBlockHash, byte[] expectedBatchId, byte[] expectedPayloadDigest, + byte[] expectedMutationPlanDigest, List expectedParticipants) { if (kind != expectedKind || !Objects.equals(participant, expectedParticipant) || epoch != expectedEpoch || !Arrays.equals(blockHash, expectedBlockHash) || !Arrays.equals(batchId, expectedBatchId) || !Arrays.equals(payloadDigest, expectedPayloadDigest) + || !Arrays.equals(mutationPlanDigest, expectedMutationPlanDigest) || !participants.equals(expectedParticipants)) { throw new ArchivePersistenceException("Archive progress identity mismatch"); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java index d6123a0555a..535f901d1be 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java @@ -18,7 +18,8 @@ public final class ArchiveProgressEnvelopeCodec { private static final int MAGIC = 0x54415047; // TAPG - private static final short VERSION = 1; + private static final short VERSION_1 = 1; + private static final short VERSION_2 = 2; private static final int HEADER_LENGTH = 12; private static final int MAX_FIELD_LENGTH = 1024; private static final int MAX_PARTICIPANTS = 1024; @@ -29,7 +30,8 @@ public byte[] encode(ArchiveProgressEnvelope envelope) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(bytes); output.writeInt(MAGIC); - output.writeShort(VERSION); + byte[] mutationPlanDigest = envelope.getMutationPlanDigest(); + output.writeShort(mutationPlanDigest == null ? VERSION_1 : VERSION_2); output.writeByte(kindCode(envelope.getKind())); output.writeByte(0); output.writeInt(0); @@ -37,6 +39,9 @@ public byte[] encode(ArchiveProgressEnvelope envelope) { output.write(envelope.getBlockHash()); output.write(envelope.getBatchId()); output.write(envelope.getPayloadDigest()); + if (mutationPlanDigest != null) { + output.write(mutationPlanDigest); + } writeString(output, envelope.getParticipant() == null ? "" : envelope.getParticipant()); output.writeInt(envelope.getParticipants().size()); for (String participant : envelope.getParticipants()) { @@ -73,7 +78,9 @@ public ArchiveProgressEnvelope decode(byte[] encoded) { } try { DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); - if (input.readInt() != MAGIC || input.readShort() != VERSION) { + int magic = input.readInt(); + short version = input.readShort(); + if (magic != MAGIC || version != VERSION_1 && version != VERSION_2) { throw new IllegalArgumentException("Unsupported archive progress envelope header"); } Kind kind = decodeKind(input.readUnsignedByte()); @@ -84,6 +91,7 @@ public ArchiveProgressEnvelope decode(byte[] encoded) { byte[] blockHash = readExact(input, 32); byte[] batchId = readExact(input, 16); byte[] payloadDigest = readExact(input, 32); + byte[] mutationPlanDigest = version == VERSION_2 ? readExact(input, 32) : null; String participant = readString(input, true); int count = input.readInt(); if (count <= 0 || count > MAX_PARTICIPANTS) { @@ -97,7 +105,7 @@ public ArchiveProgressEnvelope decode(byte[] encoded) { throw new IllegalArgumentException("Archive progress envelope payload mismatch"); } return new ArchiveProgressEnvelope(kind, participant.isEmpty() ? null : participant, epoch, - blockHash, batchId, payloadDigest, participants); + blockHash, batchId, payloadDigest, mutationPlanDigest, participants); } catch (EOFException truncated) { throw new IllegalArgumentException("Archive progress envelope is truncated", truncated); } catch (IOException invalid) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java index d986f04f6a6..542857d9320 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java @@ -29,6 +29,10 @@ public ArchiveReaderHeadPublisher(HistoryCommitStore history, Path path, } public void publish(long epoch) throws IOException { + publish(epoch, null); + } + + public void publish(long epoch, byte[] mutationPlanDigest) throws IOException { HistoryCommitMarker marker = history.get(epoch); if (marker == null || marker.getMeta().getEpoch() != epoch || !marker.getDatabases().equals(participants)) { @@ -37,7 +41,7 @@ public void publish(long epoch) throws IOException { } progressFile.store(new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, epoch, marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), participants)); + marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, participants)); } private static List validateParticipants(List participants) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java index ef57ec843c0..f3c44621077 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java @@ -18,7 +18,7 @@ public final class ArchiveReaderPublicationGate { private final HistoryCommitStore history; private final ProgressSource checkpointSource; - private final Map participantSources; + private final Map participantSources; private final Path readerVisiblePath; private final ArchiveProgressFile readerVisibleFile; private final ArchiveReaderHeadPublisher publisher; @@ -26,20 +26,22 @@ public final class ArchiveReaderPublicationGate { private final ArchiveStateBarrier barrier; public ArchiveReaderPublicationGate(HistoryCommitStore history, - ProgressSource checkpointSource, Map participantSources, + ProgressSource checkpointSource, + Map participantSources, Path readerVisiblePath, List participants, ArchiveStateBarrier barrier) { this(history, checkpointSource, participantSources, readerVisiblePath, participants, barrier, temporary -> { }); } ArchiveReaderPublicationGate(HistoryCommitStore history, - ProgressSource checkpointSource, Map participantSources, + ProgressSource checkpointSource, + Map participantSources, Path readerVisiblePath, List participants, ArchiveStateBarrier barrier, ArchiveProgressFile.FaultHook faultHook) { this.history = Objects.requireNonNull(history, "history"); this.checkpointSource = Objects.requireNonNull(checkpointSource, "checkpointSource"); this.participants = validateParticipants(participants); - TreeMap sorted = new TreeMap<>( + TreeMap sorted = new TreeMap<>( Objects.requireNonNull(participantSources, "participantSources")); if (!new ArrayList<>(sorted.keySet()).equals(this.participants) || sorted.containsValue(null)) { @@ -63,7 +65,7 @@ public static ArchiveReaderPublicationGate forFiles(HistoryCommitStore history, if (sorted.containsValue(null)) { throw new IllegalArgumentException("Archive publication participant path is missing"); } - Map sources = new LinkedHashMap<>(); + Map sources = new LinkedHashMap<>(); ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); sorted.forEach((participant, path) -> sources.put(participant, () -> new ArchiveProgressFile(path, codec).load())); @@ -73,23 +75,36 @@ public static ArchiveReaderPublicationGate forFiles(HistoryCommitStore history, } public void publish(long targetEpoch) throws IOException { + publishAfterRefresh(targetEpoch, () -> { }); + } + + public void publishAfterRefresh(long targetEpoch, + ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { if (targetEpoch < 0) { throw new IllegalArgumentException("Reader publication target must be non-negative"); } - barrier.run(() -> publishInsideBarrier(targetEpoch)); + Objects.requireNonNull(refresh, "refresh"); + barrier.run(() -> { + refresh.run(); + publishInsideBarrier(targetEpoch); + }); } private void publishInsideBarrier(long targetEpoch) throws IOException { HistoryCommitMarker target = requireMarker(targetEpoch); validateCurrentReader(targetEpoch); - validateAuthorities(target); - validateAuthorities(target); + byte[] firstDigest = validateAuthorities(target); + byte[] secondDigest = validateAuthorities(target); + if (!Arrays.equals(firstDigest, secondDigest)) { + throw new ArchivePersistenceException( + "Archive mutation-plan authority drifted during reader publication"); + } HistoryCommitMarker reloaded = requireMarker(targetEpoch); if (!sameIdentity(target, reloaded)) { throw new ArchivePersistenceException( "Committed history identity drifted during reader publication"); } - publisher.publish(targetEpoch); + publisher.publish(targetEpoch, secondDigest); } private HistoryCommitMarker requireMarker(long targetEpoch) { @@ -114,14 +129,24 @@ private void validateCurrentReader(long targetEpoch) throws IOException { } } - private void validateAuthorities(HistoryCommitMarker target) throws IOException { + private byte[] validateAuthorities(HistoryCommitMarker target) throws IOException { ArchiveProgressEnvelope checkpoint = load(checkpointSource, "archive apply checkpoint"); requireIdentity(checkpoint, Kind.APPLY_CHECKPOINT, null, target); - for (Map.Entry entry : participantSources.entrySet()) { - ArchiveProgressEnvelope progress = load(entry.getValue(), - "archive participant progress: " + entry.getKey()); + byte[] mutationPlanDigest = checkpoint.getMutationPlanDigest(); + for (Map.Entry entry + : participantSources.entrySet()) { + ArchiveProgressEnvelope progress = entry.getValue().loadProgress(); + if (progress == null) { + throw new ArchivePersistenceException( + "Missing archive participant progress: " + entry.getKey()); + } requireIdentity(progress, Kind.PARTICIPANT_PROGRESS, entry.getKey(), target); + if (!Arrays.equals(mutationPlanDigest, progress.getMutationPlanDigest())) { + throw new ArchivePersistenceException( + "Archive participant mutation-plan digest mismatch: " + entry.getKey()); + } } + return mutationPlanDigest; } private ArchiveProgressEnvelope load(ProgressSource source, String name) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java index 8fbaeaff8fc..d34c28af95b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java @@ -16,7 +16,7 @@ public final class ArchiveRecoveryAuthorityScanner { private final HistoryCommitStore history; private final Path checkpointPath; - private final Map participantSources; + private final Map participantSources; private final Path readerVisiblePath; private final List participants; private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); @@ -34,7 +34,7 @@ public ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpoi || sortedPaths.containsValue(null)) { throw new IllegalArgumentException("Archive participant progress path set mismatch"); } - Map sources = new LinkedHashMap<>(); + Map sources = new LinkedHashMap<>(); sortedPaths.forEach((participant, path) -> sources.put(participant, () -> progressFile(path).load())); this.participantSources = Collections.unmodifiableMap(sources); @@ -53,20 +53,21 @@ private ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpo || sortedBatches.containsValue(null)) { throw new IllegalArgumentException("Archive participant batch set mismatch"); } - Map sources = new LinkedHashMap<>(); + Map sources = new LinkedHashMap<>(); sortedBatches.forEach((participant, batch) -> sources.put(participant, () -> batch.load().getProgress())); this.participantSources = Collections.unmodifiableMap(sources); } private ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpointPath, - Map participantSources, Path readerVisiblePath, + Map participantSources, + Path readerVisiblePath, List participants, byte nativeEngineAuthority) { this.history = Objects.requireNonNull(history, "history"); this.checkpointPath = Objects.requireNonNull(checkpointPath, "checkpointPath"); this.readerVisiblePath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); this.participants = validateParticipants(participants); - TreeMap sortedSources = new TreeMap<>( + TreeMap sortedSources = new TreeMap<>( Objects.requireNonNull(participantSources, "participantSources")); if (!new ArrayList<>(sortedSources.keySet()).equals(this.participants) || sortedSources.containsValue(null)) { @@ -84,13 +85,14 @@ public static ArchiveRecoveryAuthorityScanner forParticipantBatches( readerVisiblePath, participants, true); } - public static ArchiveRecoveryAuthorityScanner forRocksDbParticipants( + public static ArchiveRecoveryAuthorityScanner forParticipants( HistoryCommitStore history, Path checkpointPath, - Map participantEngines, Path readerVisiblePath, + Map participantEngines, + Path readerVisiblePath, List participants) { - Map sources = new LinkedHashMap<>(); + Map sources = new LinkedHashMap<>(); Objects.requireNonNull(participantEngines, "participantEngines") - .forEach((participant, engine) -> sources.put(participant, engine::loadProgress)); + .forEach(sources::put); return new ArchiveRecoveryAuthorityScanner(history, checkpointPath, sources, readerVisiblePath, participants, (byte) 1); } @@ -123,9 +125,9 @@ public ArchiveProgressEnvelope loadCheckpoint() throws IOException { public Map loadParticipantProgress() throws IOException { Map loaded = new LinkedHashMap<>(); - for (Map.Entry entry + for (Map.Entry entry : participantSources.entrySet()) { - loaded.put(entry.getKey(), entry.getValue().load()); + loaded.put(entry.getKey(), entry.getValue().loadProgress()); } return loaded; } @@ -142,11 +144,6 @@ private ArchiveProgressFile progressFile(Path path) { return new ArchiveProgressFile(path, progressCodec); } - @FunctionalInterface - private interface ParticipantProgressSource { - ArchiveProgressEnvelope load() throws IOException; - } - private static List validateParticipants(List participants) { Objects.requireNonNull(participants, "participants"); if (participants.isEmpty()) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java index fee9c953864..cdd7df4125d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java @@ -35,6 +35,7 @@ public RecoveryPlan recover() { execute(action); faultHook.afterDurableAction(action); } + storage.recoveryComplete(); return plan; } catch (IOException failure) { throw new ArchivePersistenceException("Archive recovery action failed", failure); @@ -75,6 +76,9 @@ void replayParticipantAndSyncProgress(String participant, long firstEpoch, long throws IOException; void publishReaderHeadAndSync(long readerVisibleHead) throws IOException; + + default void recoveryComplete() throws IOException { + } } /** Immutable result of one fresh durable H/C/D[i]/R scan. */ diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java index 00b6443ca0e..bcddcb1d002 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -49,6 +50,12 @@ public RecoverySnapshot scan() throws IOException { "Missing archive participant progress: " + participant); } validateEnvelope(envelope, Kind.PARTICIPANT_PROGRESS, participant); + if (envelope.getEpoch() == checkpoint.getEpoch() + && !Arrays.equals(envelope.getMutationPlanDigest(), + checkpoint.getMutationPlanDigest())) { + throw new ArchivePersistenceException( + "Archive participant mutation-plan digest mismatch: " + participant); + } participantHeads.put(participant, envelope.getEpoch()); } ArchiveProgressEnvelope readerVisible = progress.loadReaderVisible(); @@ -56,6 +63,12 @@ public RecoverySnapshot scan() throws IOException { throw new ArchivePersistenceException("Missing archive reader-visible progress"); } validateEnvelope(readerVisible, Kind.READER_VISIBLE, null); + if (readerVisible.getEpoch() == checkpoint.getEpoch() + && !Arrays.equals(readerVisible.getMutationPlanDigest(), + checkpoint.getMutationPlanDigest())) { + throw new ArchivePersistenceException( + "Archive reader mutation-plan digest mismatch"); + } return new RecoverySnapshot(history.committedHeadEpoch(), checkpoint.getEpoch(), participantHeads, readerVisible.getEpoch()); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java new file mode 100644 index 00000000000..1c189665164 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java @@ -0,0 +1,194 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; + +/** Advances one standalone normal target through C, mixed D, latest refresh, and R. */ +public final class ArchiveTargetApplyCoordinator { + + private final HistoryCommitStore history; + private final ArchiveProgressFile checkpointFile; + private final ArchiveTargetMutationPlanFile mutationPlanFile; + private final Map participantEngines; + private final List participants; + private final ArchiveRecoveryAuthorityScanner scanner; + private final ArchiveReaderPublicationGate publicationGate; + private final FaultHook faultHook; + + public ArchiveTargetApplyCoordinator(HistoryCommitStore history, Path checkpointPath, + Map participantEngines, Path readerVisiblePath, + List participants, ArchiveStateBarrier barrier) { + this(history, checkpointPath, participantEngines, readerVisiblePath, participants, barrier, + (stage, participant) -> { }, temporary -> { }, (stage, path) -> { }); + } + + ArchiveTargetApplyCoordinator(HistoryCommitStore history, Path checkpointPath, + Map participantEngines, Path readerVisiblePath, + List participants, ArchiveStateBarrier barrier, FaultHook faultHook, + ArchiveProgressFile.FaultHook publicationFaultHook) { + this(history, checkpointPath, participantEngines, readerVisiblePath, participants, barrier, + faultHook, publicationFaultHook, (stage, path) -> { }); + } + + ArchiveTargetApplyCoordinator(HistoryCommitStore history, Path checkpointPath, + Map participantEngines, Path readerVisiblePath, + List participants, ArchiveStateBarrier barrier, FaultHook faultHook, + ArchiveProgressFile.FaultHook publicationFaultHook, + ArchiveTargetMutationPlanFile.FaultHook planFaultHook) { + this.history = Objects.requireNonNull(history, "history"); + Path checkedCheckpointPath = Objects.requireNonNull(checkpointPath, "checkpointPath"); + Path checkedReaderPath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); + this.participants = validateParticipants(participants); + TreeMap sorted = new TreeMap<>( + Objects.requireNonNull(participantEngines, "participantEngines")); + if (!new ArrayList<>(sorted.keySet()).equals(this.participants) + || sorted.containsValue(null)) { + throw new IllegalArgumentException("Archive target participant engine set mismatch"); + } + this.participantEngines = Collections.unmodifiableMap(new LinkedHashMap<>(sorted)); + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + this.checkpointFile = new ArchiveProgressFile(checkedCheckpointPath, codec); + this.mutationPlanFile = new ArchiveTargetMutationPlanFile(checkedCheckpointPath, + Objects.requireNonNull(planFaultHook, "planFaultHook")); + this.scanner = ArchiveRecoveryAuthorityScanner.forParticipants(history, + checkedCheckpointPath, this.participantEngines, checkedReaderPath, this.participants); + this.publicationGate = new ArchiveReaderPublicationGate(history, checkpointFile::load, + this.participantEngines, checkedReaderPath, this.participants, + Objects.requireNonNull(barrier, "barrier"), + Objects.requireNonNull(publicationFaultHook, "publicationFaultHook")); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + public void apply(long targetEpoch, + Map> mutationPlans, + ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { + HistoryCommitMarker target = validateTarget(targetEpoch); + Map> plans = validatePlans(mutationPlans); + ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlan( + progress(Kind.APPLY_CHECKPOINT, null, target, null), plans); + apply(target, plan, refresh); + } + + public void apply(ArchiveParticipantMutationBatch batch, + ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { + ArchiveParticipantMutationBatch input = Objects.requireNonNull(batch, "batch"); + HistoryCommitMarker target = validateTarget(input.getTargetEpoch()); + apply(target, new ArchiveTargetMutationPlanBuilder().build(target, input), refresh); + } + + private void apply(HistoryCommitMarker target, ArchiveTargetMutationPlan plan, + ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { + long targetEpoch = target.getMeta().getEpoch(); + plan.requireIdentity(target, participants); + requireFixedPointBeforeTarget(targetEpoch); + byte[] mutationPlanDigest = plan.digest(); + mutationPlanFile.store(plan); + faultHook.afterDurableStage(Stage.AFTER_PLAN, null); + ArchiveProgressEnvelope checkpoint = progress(Kind.APPLY_CHECKPOINT, null, target, + mutationPlanDigest); + checkpointFile.store(checkpoint); + faultHook.afterDurableStage(Stage.AFTER_CHECKPOINT, null); + for (String participant : participants) { + participantEngines.get(participant).apply(plan.getMutations(participant), + progress(Kind.PARTICIPANT_PROGRESS, participant, target, mutationPlanDigest)); + faultHook.afterDurableStage(Stage.AFTER_PARTICIPANT, participant); + } + publicationGate.publishAfterRefresh(targetEpoch, + Objects.requireNonNull(refresh, "refresh")); + faultHook.afterDurableStage(Stage.AFTER_READER, null); + mutationPlanFile.retire(); + } + + private HistoryCommitMarker validateTarget(long targetEpoch) { + if (targetEpoch < 0) { + throw new IllegalArgumentException("Archive apply target must be non-negative"); + } + HistoryCommitMarker target = history.get(targetEpoch); + if (target == null || target.getMeta().getEpoch() != targetEpoch + || !target.getDatabases().equals(participants)) { + throw new ArchivePersistenceException("Missing or mismatched archive apply target"); + } + return target; + } + + private Map> validatePlans( + Map> mutationPlans) { + TreeMap> sorted = new TreeMap<>( + Objects.requireNonNull(mutationPlans, "mutationPlans")); + if (!new ArrayList<>(sorted.keySet()).equals(participants) + || sorted.containsValue(null)) { + throw new IllegalArgumentException("Archive target mutation plan set mismatch"); + } + Map> copy = new LinkedHashMap<>(); + sorted.forEach((participant, mutations) -> { + List mutationCopy = new ArrayList<>(mutations); + if (mutationCopy.contains(null)) { + throw new IllegalArgumentException("Archive target mutation plan contains null"); + } + copy.put(participant, Collections.unmodifiableList(mutationCopy)); + }); + return Collections.unmodifiableMap(copy); + } + + private void requireFixedPointBeforeTarget(long targetEpoch) throws IOException { + RecoverySnapshot current = scanner.scan(); + long checkpoint = current.getCheckpointHead(); + if (mutationPlanFile.loadIfPresent() != null) { + throw new ArchivePersistenceException("Archive mutation plan requires recovery before apply"); + } + if (current.getHistoryHead() < targetEpoch || checkpoint + 1 != targetEpoch + || current.getReaderVisibleHead() != checkpoint) { + throw new ArchivePersistenceException("Archive apply source is not a safe fixed point"); + } + for (long participantHead : current.getParticipantHeads().values()) { + if (participantHead != checkpoint) { + throw new ArchivePersistenceException("Archive participant requires recovery before apply"); + } + } + } + + private ArchiveProgressEnvelope progress(Kind kind, String participant, + HistoryCommitMarker marker, byte[] mutationPlanDigest) { + return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, participants); + } + + private static List validateParticipants(List participants) { + List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); + if (copy.isEmpty()) { + throw new IllegalArgumentException("Archive target participant set must not be empty"); + } + String previous = null; + for (String participant : copy) { + if (participant == null || participant.isEmpty() + || previous != null && previous.compareTo(participant) >= 0) { + throw new IllegalArgumentException( + "Archive target participants must be non-empty, unique, and sorted"); + } + previous = participant; + } + return Collections.unmodifiableList(copy); + } + + enum Stage { + AFTER_PLAN, + AFTER_CHECKPOINT, + AFTER_PARTICIPANT, + AFTER_READER + } + + @FunctionalInterface + interface FaultHook { + void afterDurableStage(Stage stage, String participant) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlan.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlan.java new file mode 100644 index 00000000000..0ee4436263b --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlan.java @@ -0,0 +1,80 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; + +/** Immutable target H identity plus exact per-participant business mutations. */ +final class ArchiveTargetMutationPlan { + + private final ArchiveProgressEnvelope target; + private final Map> mutations; + + ArchiveTargetMutationPlan(ArchiveProgressEnvelope target, + Map> mutations) { + this.target = Objects.requireNonNull(target, "target"); + if (target.getKind() != Kind.APPLY_CHECKPOINT || target.getParticipant() != null) { + throw new IllegalArgumentException("Mutation plan target must be a global checkpoint"); + } + if (target.getMutationPlanDigest() != null) { + throw new IllegalArgumentException("Mutation plan target must not contain its own digest"); + } + TreeMap> checked = new TreeMap<>( + Objects.requireNonNull(mutations, "mutations")); + if (!new ArrayList<>(checked.keySet()).equals(target.getParticipants()) + || checked.containsValue(null)) { + throw new IllegalArgumentException("Mutation plan participant set mismatch"); + } + Map> copy = new LinkedHashMap<>(); + for (String participant : target.getParticipants()) { + List values = new ArrayList<>( + Objects.requireNonNull(checked.get(participant), "participant mutations")); + if (values.contains(null)) { + throw new IllegalArgumentException("Mutation plan contains null mutation"); + } + values.sort((left, right) -> BlockReverseDiff.compareUnsigned( + left.getKey(), right.getKey())); + byte[] previous = null; + for (ArchiveParticipantMutation mutation : values) { + byte[] key = mutation.getKey(); + if (previous != null && BlockReverseDiff.compareUnsigned(previous, key) == 0) { + throw new IllegalArgumentException("Mutation plan contains duplicate physical key"); + } + previous = key; + } + copy.put(participant, Collections.unmodifiableList(values)); + } + this.mutations = Collections.unmodifiableMap(copy); + } + + ArchiveProgressEnvelope getTarget() { + return target; + } + + List getMutations(String participant) { + List values = mutations.get(participant); + if (values == null) { + throw new ArchivePersistenceException("Unknown mutation-plan participant: " + participant); + } + return values; + } + + Map> getMutations() { + return mutations; + } + + byte[] digest() { + return new ArchiveTargetMutationPlanCodec().digest(this); + } + + void requireIdentity(HistoryCommitMarker marker, List participants) { + target.requireIdentity(Kind.APPLY_CHECKPOINT, null, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), participants); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java new file mode 100644 index 00000000000..15fe91fd712 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java @@ -0,0 +1,69 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.tron.core.db2.archive.ArchiveParticipantMutationBatch.Mutation; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; + +/** Builds one canonical target plan from an immutable exact physical participant batch. */ +final class ArchiveTargetMutationPlanBuilder { + + private final List participants; + + ArchiveTargetMutationPlanBuilder() { + List expected = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(expected); + participants = Collections.unmodifiableList(expected); + } + + ArchiveTargetMutationPlan build(HistoryCommitMarker committedTarget, + ArchiveParticipantMutationBatch batch) { + HistoryCommitMarker target = Objects.requireNonNull(committedTarget, "committedTarget"); + ArchiveParticipantMutationBatch input = Objects.requireNonNull(batch, "batch"); + requireTargetIdentity(target, input); + if (!target.getDatabases().equals(participants) + || !input.getParticipants().equals(participants)) { + throw new ArchivePersistenceException( + "Participant mutation batch does not contain the exact VERSIONED_STATE set"); + } + Map> grouped = new LinkedHashMap<>(); + for (String participant : participants) { + grouped.put(participant, new ArrayList<>()); + } + for (Mutation mutation : input.getMutations()) { + String dbName = mutation.getDbName(); + List participantMutations = grouped.get(dbName); + if (participantMutations == null || !ArchiveStoreScope.isStateDatabase(dbName)) { + throw new ArchivePersistenceException( + "Unknown or derived archive participant mutation: " + dbName); + } + byte[] value = mutation.getValue(); + participantMutations.add(value == null + ? ArchiveParticipantMutation.delete(mutation.getPhysicalRawKey()) + : ArchiveParticipantMutation.put(mutation.getPhysicalRawKey(), value)); + } + ArchiveProgressEnvelope targetEnvelope = new ArchiveProgressEnvelope( + Kind.APPLY_CHECKPOINT, null, target.getMeta().getEpoch(), + target.getMeta().getBlockHash(), target.getBatchId(), + target.getHistoryLocation().getBodyDigest(), participants); + return new ArchiveTargetMutationPlan(targetEnvelope, grouped); + } + + private void requireTargetIdentity(HistoryCommitMarker target, + ArchiveParticipantMutationBatch batch) { + if (target.getMeta().getEpoch() != batch.getTargetEpoch() + || !Arrays.equals(target.getMeta().getBlockHash(), batch.getBlockHash()) + || !Arrays.equals(target.getBatchId(), batch.getBatchId()) + || !Arrays.equals(target.getHistoryLocation().getBodyDigest(), + batch.getHistoryPayloadDigest()) + || !target.getDatabases().equals(batch.getParticipants())) { + throw new ArchivePersistenceException( + "Participant mutation batch target identity mismatch"); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanCodec.java new file mode 100644 index 00000000000..4c9a7bcda84 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanCodec.java @@ -0,0 +1,156 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Checksummed bounded codec for one durable target mutation plan. */ +final class ArchiveTargetMutationPlanCodec { + + private static final int MAGIC = 0x54414d50; // TAMP + private static final short VERSION = 1; + private static final int HEADER_LENGTH = 12; + private static final int MAX_PARTICIPANTS = 1024; + private static final int MAX_MUTATIONS = 1_000_000; + private static final int MAX_FIELD_LENGTH = 64 * 1024 * 1024; + static final int MAX_ENCODED_LENGTH = 128 * 1024 * 1024; + private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); + + byte[] encode(ArchiveTargetMutationPlan plan) { + try { + byte[] target = progressCodec.encode(plan.getTarget()); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(0); + writeBytes(output, target); + output.writeInt(plan.getTarget().getParticipants().size()); + for (String participant : plan.getTarget().getParticipants()) { + List mutations = plan.getMutations(participant); + if (mutations.size() > MAX_MUTATIONS) { + throw new IllegalArgumentException("Mutation plan contains too many mutations"); + } + output.writeInt(mutations.size()); + for (ArchiveParticipantMutation mutation : mutations) { + writeBytes(output, mutation.getKey()); + byte[] value = mutation.getValue(); + output.writeInt(value == null ? -1 : value.length); + if (value != null) { + output.write(value); + } + } + } + output.flush(); + byte[] payload = bytes.toByteArray(); + int length = Math.addExact(payload.length, Integer.BYTES); + if (length > MAX_ENCODED_LENGTH) { + throw new IllegalArgumentException("Mutation plan is too large"); + } + ByteBuffer.wrap(payload).putInt(8, length); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(payload); + output.writeInt(crc32c(payload)); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected mutation-plan encoding failure", impossible); + } + } + + ArchiveTargetMutationPlan decode(byte[] encoded) { + if (encoded == null || encoded.length < HEADER_LENGTH + Integer.BYTES + || encoded.length > MAX_ENCODED_LENGTH) { + throw new IllegalArgumentException("Mutation-plan length is invalid"); + } + int checksum = ByteBuffer.wrap(encoded, encoded.length - Integer.BYTES, + Integer.BYTES).getInt(); + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + if (checksum != crc32c(payload)) { + throw new IllegalArgumentException("Mutation-plan checksum mismatch"); + } + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != encoded.length) { + throw new IllegalArgumentException("Unsupported mutation-plan header"); + } + ArchiveProgressEnvelope target = progressCodec.decode(readBytes(input)); + int participantCount = input.readInt(); + if (participantCount <= 0 || participantCount > MAX_PARTICIPANTS + || participantCount != target.getParticipants().size()) { + throw new IllegalArgumentException("Mutation-plan participant count mismatch"); + } + Map> mutations = new LinkedHashMap<>(); + for (String participant : target.getParticipants()) { + int count = input.readInt(); + if (count < 0 || count > MAX_MUTATIONS) { + throw new IllegalArgumentException("Mutation-plan mutation count is invalid"); + } + List values = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + byte[] key = readBytes(input); + int valueLength = input.readInt(); + if (valueLength < -1 || valueLength > MAX_FIELD_LENGTH + || valueLength > input.available() - Integer.BYTES) { + throw new IllegalArgumentException("Mutation-plan value length is invalid"); + } + values.add(valueLength == -1 ? ArchiveParticipantMutation.delete(key) + : ArchiveParticipantMutation.put(key, readExact(input, valueLength))); + } + mutations.put(participant, values); + } + if (input.available() != Integer.BYTES) { + throw new IllegalArgumentException("Mutation-plan payload mismatch"); + } + return new ArchiveTargetMutationPlan(target, mutations); + } catch (EOFException truncated) { + throw new IllegalArgumentException("Mutation plan is truncated", truncated); + } catch (IOException invalid) { + throw new IllegalArgumentException("Invalid mutation plan", invalid); + } + } + + byte[] digest(ArchiveTargetMutationPlan plan) { + return Hashing.sha256().hashBytes(encode(plan)).asBytes(); + } + + private static void writeBytes(DataOutputStream output, byte[] value) throws IOException { + if (value.length > MAX_FIELD_LENGTH) { + throw new IllegalArgumentException("Mutation-plan field is too large"); + } + output.writeInt(value.length); + output.write(value); + } + + private static byte[] readBytes(DataInputStream input) throws IOException { + int length = input.readInt(); + if (length < 0 || length > MAX_FIELD_LENGTH + || length > input.available() - Integer.BYTES) { + throw new IllegalArgumentException("Mutation-plan field length is invalid"); + } + return readExact(input, length); + } + + private static byte[] readExact(DataInputStream input, int length) throws IOException { + byte[] value = new byte[length]; + input.readFully(value); + return value; + } + + private static int crc32c(byte[] value) { + return Hashing.crc32c().hashBytes(value).asInt(); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFile.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFile.java new file mode 100644 index 00000000000..b3cd417d984 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFile.java @@ -0,0 +1,97 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Objects; + +/** Atomic durable authority for one in-flight target mutation plan. */ +final class ArchiveTargetMutationPlanFile { + + private static final String FILE_NAME = "target.mutation-plan"; + private final Path path; + private final Path temporary; + private final ArchiveTargetMutationPlanCodec codec = new ArchiveTargetMutationPlanCodec(); + private final FaultHook faultHook; + + ArchiveTargetMutationPlanFile(Path checkpointPath) { + this(checkpointPath, (stage, path) -> { }); + } + + ArchiveTargetMutationPlanFile(Path checkpointPath, FaultHook faultHook) { + Path directory = Objects.requireNonNull(checkpointPath, "checkpointPath").getParent(); + if (directory == null) { + throw new IllegalArgumentException("Checkpoint path must have a parent"); + } + this.path = directory.resolve(FILE_NAME); + this.temporary = directory.resolve(FILE_NAME + ".tmp"); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + void store(ArchiveTargetMutationPlan plan) throws IOException { + byte[] encoded = codec.encode(plan); + Files.createDirectories(path.getParent()); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(encoded); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + faultHook.after(Stage.AFTER_TEMPORARY_FORCE, temporary); + try { + Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new ArchivePersistenceException( + "Archive filesystem does not support atomic mutation-plan replacement", unsupported); + } + HistorySegmentStore.syncDirectory(path.getParent()); + faultHook.after(Stage.AFTER_REPLACE, path); + } + + ArchiveTargetMutationPlan loadRequired() throws IOException { + if (!Files.exists(path)) { + throw new ArchivePersistenceException("Archive target mutation plan is missing"); + } + long size = Files.size(path); + if (size <= 0 || size > ArchiveTargetMutationPlanCodec.MAX_ENCODED_LENGTH) { + throw new ArchivePersistenceException("Archive target mutation-plan length is invalid"); + } + try { + return codec.decode(Files.readAllBytes(path)); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Archive target mutation plan is corrupt", invalid); + } + } + + ArchiveTargetMutationPlan loadIfPresent() throws IOException { + return Files.exists(path) ? loadRequired() : null; + } + + void retire() throws IOException { + Files.deleteIfExists(path); + Files.deleteIfExists(temporary); + HistorySegmentStore.syncDirectory(path.getParent()); + } + + Path getPath() { + return path; + } + + enum Stage { + AFTER_TEMPORARY_FORCE, + AFTER_REPLACE + } + + @FunctionalInterface + interface FaultHook { + void after(Stage stage, Path path) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java b/chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java index 7cc84d7d587..11c75e04e0d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java @@ -18,7 +18,7 @@ import org.iq80.leveldb.WriteOptions; /** LevelDB participant whose business mutations and D[i] share one synced native WriteBatch. */ -public final class LevelDbArchiveParticipant implements Closeable { +public final class LevelDbArchiveParticipant implements Closeable, ArchiveParticipant { private static final byte BUSINESS_PREFIX = 1; private static final byte[] PROGRESS_KEY = new byte[]{0, 'p', 'r', 'o', 'g', 'r', 'e', 's', 's'}; @@ -50,17 +50,20 @@ public LevelDbArchiveParticipant(Path directory, String participant, database = open(); } - public synchronized void apply(List mutations, ArchiveProgressEnvelope progress) + @Override + public synchronized void apply(List mutations, + ArchiveProgressEnvelope progress) throws IOException { Objects.requireNonNull(mutations, "mutations"); requireProgress(progress); try (WriteBatch batch = database.createWriteBatch()) { - for (Mutation mutation : mutations) { + for (ArchiveParticipantMutation mutation : mutations) { Objects.requireNonNull(mutation, "mutation"); - if (mutation.value == null) { - batch.delete(businessKey(mutation.key)); + byte[] value = mutation.getValue(); + if (value == null) { + batch.delete(businessKey(mutation.getKey())); } else { - batch.put(businessKey(mutation.key), mutation.value); + batch.put(businessKey(mutation.getKey()), value); } } batch.put(PROGRESS_KEY, progressCodec.encode(progress)); @@ -75,6 +78,7 @@ public synchronized byte[] get(byte[] key) { return value == null ? null : Arrays.copyOf(value, value.length); } + @Override public synchronized ArchiveProgressEnvelope loadProgress() { byte[] encoded = database.get(PROGRESS_KEY); if (encoded == null) { @@ -142,24 +146,6 @@ private static List validateParticipants(List participants) { return Collections.unmodifiableList(copy); } - public static final class Mutation { - private final byte[] key; - private final byte[] value; - - private Mutation(byte[] key, byte[] value) { - this.key = Arrays.copyOf(Objects.requireNonNull(key, "key"), key.length); - this.value = value == null ? null : Arrays.copyOf(value, value.length); - } - - public static Mutation put(byte[] key, byte[] value) { - return new Mutation(key, Objects.requireNonNull(value, "value")); - } - - public static Mutation delete(byte[] key) { - return new Mutation(key, null); - } - } - enum Stage { BEFORE_WRITE, AFTER_WRITE diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java b/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java index 40237cc4630..1c5cbea10f7 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java @@ -17,7 +17,7 @@ import org.rocksdb.WriteOptions; /** RocksDB participant whose business mutations and D[i] share one synced native WriteBatch. */ -public final class RocksDbArchiveParticipant implements Closeable { +public final class RocksDbArchiveParticipant implements Closeable, ArchiveParticipant { private static final byte BUSINESS_PREFIX = 1; private static final byte[] PROGRESS_KEY = new byte[]{0, 'p', 'r', 'o', 'g', 'r', 'e', 's', 's'}; @@ -63,17 +63,20 @@ public RocksDbArchiveParticipant(Path directory, String participant, } } - public synchronized void apply(List mutations, ArchiveProgressEnvelope progress) + @Override + public synchronized void apply(List mutations, + ArchiveProgressEnvelope progress) throws IOException { Objects.requireNonNull(mutations, "mutations"); requireProgress(progress); try (WriteBatch batch = new WriteBatch()) { - for (Mutation mutation : mutations) { + for (ArchiveParticipantMutation mutation : mutations) { Objects.requireNonNull(mutation, "mutation"); - if (mutation.value == null) { - batch.delete(businessKey(mutation.key)); + byte[] value = mutation.getValue(); + if (value == null) { + batch.delete(businessKey(mutation.getKey())); } else { - batch.put(businessKey(mutation.key), mutation.value); + batch.put(businessKey(mutation.getKey()), value); } } batch.put(PROGRESS_KEY, progressCodec.encode(progress)); @@ -94,6 +97,7 @@ public synchronized byte[] get(byte[] key) throws IOException { } } + @Override public synchronized ArchiveProgressEnvelope loadProgress() throws IOException { try { byte[] encoded = database.get(PROGRESS_KEY); @@ -148,24 +152,6 @@ private static List validateParticipants(List participants) { return Collections.unmodifiableList(copy); } - public static final class Mutation { - private final byte[] key; - private final byte[] value; - - private Mutation(byte[] key, byte[] value) { - this.key = Arrays.copyOf(Objects.requireNonNull(key, "key"), key.length); - this.value = value == null ? null : Arrays.copyOf(value, value.length); - } - - public static Mutation put(byte[] key, byte[] value) { - return new Mutation(key, Objects.requireNonNull(value, "value")); - } - - public static Mutation delete(byte[] key) { - return new Mutation(key, null); - } - } - enum Stage { BEFORE_WRITE, AFTER_WRITE diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorage.java b/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorage.java deleted file mode 100644 index eb65573787c..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorage.java +++ /dev/null @@ -1,178 +0,0 @@ -package org.tron.core.db2.archive; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.TreeMap; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoveryStorage; -import org.tron.core.db2.archive.RocksDbArchiveParticipant.Mutation; - -/** File-history plus native RocksDB participant implementation of the H/C/D[i]/R executor. */ -public final class RocksDbArchiveRecoveryStorage implements RecoveryStorage, Closeable { - - private final Path archiveDirectory; - private final long maxSegmentSize; - private final List participants; - private final Map participantEngines; - private final ParticipantReplayer replayer; - private final HistorySegmentStore bodies; - private final HistoryIndexStore index; - private final HistoryCommitStore history; - private final ArchiveRecoveryAuthorityScanner scanner; - private final ArchiveReaderHeadPublisher readerPublisher; - - public RocksDbArchiveRecoveryStorage(Path archiveDirectory, long maxSegmentSize, - Path checkpointPath, Map participantEngines, - Path readerVisiblePath, List participants, ParticipantReplayer replayer) - throws IOException { - this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); - if (maxSegmentSize <= 0) { - throw new IllegalArgumentException("maxSegmentSize must be positive"); - } - this.maxSegmentSize = maxSegmentSize; - this.participants = validateParticipants(participants); - TreeMap sorted = new TreeMap<>( - Objects.requireNonNull(participantEngines, "participantEngines")); - if (!new ArrayList<>(sorted.keySet()).equals(this.participants) - || sorted.containsValue(null)) { - throw new IllegalArgumentException("Archive participant engine set mismatch"); - } - this.participantEngines = Collections.unmodifiableMap(sorted); - this.replayer = Objects.requireNonNull(replayer, "replayer"); - new ArchiveTruncationRecovery(archiveDirectory, maxSegmentSize).recover(); - ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archiveDirectory, - new HistoryCommitMarkerCodec()); - if (checkpoint == null) { - throw new ArchivePersistenceException("Archive restart checkpoint is missing"); - } - HistorySegmentStore openedBodies = null; - HistoryIndexStore openedIndex = null; - HistoryCommitStore openedHistory = null; - try { - openedBodies = new HistorySegmentStore(archiveDirectory, new BlockHistoryCodec(), - maxSegmentSize, checkpoint); - openedIndex = new HistoryIndexStore(archiveDirectory, new HistoryIndexCodec(), checkpoint); - openedHistory = new HistoryCommitStore(archiveDirectory, new HistoryCommitMarkerCodec(), - checkpoint); - } catch (IOException | RuntimeException failure) { - close(openedIndex, failure); - close(openedBodies, failure); - close(openedHistory, failure); - throw failure; - } - this.bodies = openedBodies; - this.index = openedIndex; - this.history = openedHistory; - this.scanner = ArchiveRecoveryAuthorityScanner.forRocksDbParticipants(this.history, - checkpointPath, this.participantEngines, readerVisiblePath, this.participants); - this.readerPublisher = new ArchiveReaderHeadPublisher(this.history, readerVisiblePath, - this.participants); - } - - @Override - public RecoverySnapshot scan() throws IOException { - return scanner.scan(); - } - - @Override - public void truncateHistoryAndSync(long historyHead) throws IOException { - ArchiveTruncationIntent.prepare(archiveDirectory, history, index, bodies, historyHead, - new HistoryCommitMarkerCodec()); - new ArchiveTruncationRecovery(archiveDirectory, maxSegmentSize).recover(); - } - - @Override - public void replayParticipantAndSyncProgress(String participant, long firstEpoch, - long lastEpoch) throws IOException { - RocksDbArchiveParticipant engine = participantEngines.get(participant); - if (engine == null) { - throw new ArchivePersistenceException("Unknown archive recovery participant: " + participant); - } - HistoryCommitMarker marker = history.get(lastEpoch); - if (marker == null || firstEpoch > lastEpoch) { - throw new ArchivePersistenceException("Archive participant replay range is invalid"); - } - List mutations = Objects.requireNonNull( - replayer.replay(participant, firstEpoch, lastEpoch), "participant replay mutations"); - ArchiveProgressEnvelope progress = new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, - participant, lastEpoch, marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), participants); - engine.apply(mutations, progress); - } - - @Override - public void publishReaderHeadAndSync(long readerVisibleHead) throws IOException { - readerPublisher.publish(readerVisibleHead); - } - - @Override - public void close() throws IOException { - IOException failure = null; - try { - index.close(); - } catch (IOException closeFailure) { - failure = closeFailure; - } - try { - bodies.close(); - } catch (IOException closeFailure) { - failure = add(failure, closeFailure); - } - try { - history.close(); - } catch (IOException closeFailure) { - failure = add(failure, closeFailure); - } - if (failure != null) { - throw failure; - } - } - - private static List validateParticipants(List participants) { - List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); - if (copy.isEmpty()) { - throw new IllegalArgumentException("Archive participant set must not be empty"); - } - String previous = null; - for (String participant : copy) { - if (participant == null || participant.isEmpty() - || previous != null && previous.compareTo(participant) >= 0) { - throw new IllegalArgumentException( - "Archive participants must be non-empty, unique, and sorted"); - } - previous = participant; - } - return Collections.unmodifiableList(copy); - } - - private static IOException add(IOException current, IOException addition) { - if (current == null) { - return addition; - } - current.addSuppressed(addition); - return current; - } - - private static void close(Closeable resource, Exception failure) { - if (resource == null) { - return; - } - try { - resource.close(); - } catch (IOException closeFailure) { - failure.addSuppressed(closeFailure); - } - } - - @FunctionalInterface - public interface ParticipantReplayer { - List replay(String participant, long firstEpoch, long lastEpoch) throws IOException; - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveMixedEngineProgressSourceTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveMixedEngineProgressSourceTest.java new file mode 100644 index 00000000000..a5843628ccd --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveMixedEngineProgressSourceTest.java @@ -0,0 +1,195 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; + +public class ArchiveMixedEngineProgressSourceTest { + + private static final List PARTICIPANTS = + Arrays.asList("account", "account-asset"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void mixedEnginesDriveFreshRecoveryAndReaderPublication() throws Exception { + try (Fixture fixture = fixture()) { + fixture.apply(fixture.marker(1)); + RecoverySnapshot snapshot = fixture.scanner(fixture.sources()).scan(); + assertEquals(1, snapshot.getHistoryHead()); + assertEquals(1, snapshot.getCheckpointHead()); + assertEquals(Long.valueOf(1), snapshot.getParticipantHeads().get("account")); + assertEquals(Long.valueOf(1), snapshot.getParticipantHeads().get("account-asset")); + assertEquals(0, snapshot.getReaderVisibleHead()); + + fixture.gate(fixture.sources()).publish(1); + assertEquals(1, fixture.reader().getEpoch()); + } + } + + @Test + public void sourceSetMismatchFailsBeforeReadingEitherEngine() throws Exception { + try (Fixture fixture = fixture()) { + AtomicInteger reads = new AtomicInteger(); + Map missing = new LinkedHashMap<>(); + missing.put("account", () -> { + reads.incrementAndGet(); + return fixture.progress("account", fixture.marker(1)); + }); + + assertThrows(IllegalArgumentException.class, () -> fixture.scanner(missing)); + assertThrows(IllegalArgumentException.class, () -> fixture.gate(missing)); + assertEquals(0, reads.get()); + assertEquals(0, fixture.reader().getEpoch()); + } + } + + @Test + public void identityAndPartialReadFailureNeverAdvanceReader() throws Exception { + try (Fixture fixture = fixture()) { + HistoryCommitMarker target = fixture.marker(1); + ArchiveProgressEnvelope wrongHash = new ArchiveProgressEnvelope( + Kind.PARTICIPANT_PROGRESS, "account", 1, bytes(32, 99), target.getBatchId(), + target.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + fixture.level.apply(Collections.emptyList(), wrongHash); + fixture.rocks.apply(Collections.emptyList(), fixture.progress("account-asset", target)); + assertThrows(ArchivePersistenceException.class, + () -> fixture.scanner(fixture.sources()).scan()); + assertThrows(ArchivePersistenceException.class, + () -> fixture.gate(fixture.sources()).publish(1)); + assertEquals(0, fixture.reader().getEpoch()); + + fixture.apply(target); + Map partial = fixture.sources(); + partial.put("account-asset", () -> { + throw new IOException("injected mixed-engine progress read failure"); + }); + assertThrows(IOException.class, () -> fixture.scanner(partial).scan()); + assertThrows(IOException.class, () -> fixture.gate(partial).publish(1)); + assertEquals(0, fixture.reader().getEpoch()); + } + } + + private Fixture fixture() throws Exception { + return new Fixture(temporaryFolder.newFolder().toPath()); + } + + private static final class Fixture implements AutoCloseable { + private final HistoryCommitStore history; + private final Path checkpointPath; + private final Path readerPath; + private final LevelDbArchiveParticipant level; + private final RocksDbArchiveParticipant rocks; + private final ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + + private Fixture(Path directory) throws Exception { + history = new HistoryCommitStore(directory, new HistoryCommitMarkerCodec()); + history.commitAll(Arrays.asList(marker(0), marker(1))); + checkpointPath = directory.resolve("progress/checkpoint.progress"); + readerPath = directory.resolve("progress/reader.progress"); + new ArchiveProgressFile(checkpointPath, codec).store( + progress(Kind.APPLY_CHECKPOINT, null, marker(1))); + new ArchiveProgressFile(readerPath, codec).store( + progress(Kind.READER_VISIBLE, null, marker(0))); + level = new LevelDbArchiveParticipant( + directory.resolve("account-level"), "account", PARTICIPANTS); + rocks = new RocksDbArchiveParticipant( + directory.resolve("asset-rocks"), "account-asset", PARTICIPANTS); + } + + private void apply(HistoryCommitMarker marker) throws IOException { + level.apply(Collections.emptyList(), progress("account", marker)); + rocks.apply(Collections.emptyList(), progress("account-asset", marker)); + } + + private Map sources() { + Map sources = new LinkedHashMap<>(); + sources.put("account", level); + sources.put("account-asset", rocks); + return sources; + } + + private ArchiveRecoveryAuthorityScanner scanner( + Map sources) { + return ArchiveRecoveryAuthorityScanner.forParticipants(history, checkpointPath, sources, + readerPath, PARTICIPANTS); + } + + private ArchiveReaderPublicationGate gate( + Map sources) { + return new ArchiveReaderPublicationGate(history, + () -> new ArchiveProgressFile(checkpointPath, codec).load(), sources, + readerPath, PARTICIPANTS, action -> action.run()); + } + + private ArchiveProgressEnvelope reader() throws IOException { + return new ArchiveProgressFile(readerPath, codec).load(); + } + + private ArchiveProgressEnvelope progress(String participant, HistoryCommitMarker marker) { + return progress(Kind.PARTICIPANT_PROGRESS, participant, marker); + } + + private ArchiveProgressEnvelope progress(Kind kind, String participant, + HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private HistoryCommitMarker marker(long epoch) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, bytes(32, (int) epoch), + bytes(32, (int) epoch - 1), epoch * 1_000); + HistoryLocation body = new HistoryLocation(0, epoch * 100, 100, (int) epoch, + bytes(32, (int) epoch + 20)); + HistoryIndexLocation index = new HistoryIndexLocation(epoch * 50, 50, + bytes(32, (int) epoch + 30)); + return new HistoryCommitMarker(meta, epoch - 1, body, index, + bytes(16, (int) epoch + 40), new ArrayList<>(PARTICIPANTS)); + } + + @Override + public void close() throws IOException { + IOException failure = null; + try { + rocks.close(); + } catch (RuntimeException closeFailure) { + failure = new IOException("Failed to close RocksDB participant", closeFailure); + } + try { + level.close(); + } catch (IOException closeFailure) { + if (failure == null) { + failure = closeFailure; + } else { + failure.addSuppressed(closeFailure); + } + } + history.close(); + if (failure != null) { + throw failure; + } + } + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java new file mode 100644 index 00000000000..c9cba1e83cf --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java @@ -0,0 +1,243 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; + +public class ArchiveParticipantRecoveryStorageTest { + + private static final List PARTICIPANTS = + Arrays.asList("account", "account-asset"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void secondRestartFinishesOnlyRemainingMixedEngineParticipant() throws Exception { + Path archive = temporaryFolder.newFolder("mixed-native-recovery").toPath(); + Path checkpointPath = archive.resolve("progress/checkpoint.progress"); + Path readerPath = archive.resolve("progress/reader.progress"); + List markers = initializeHistory(archive, 3); + new ArchiveProgressFile(checkpointPath, new ArchiveProgressEnvelopeCodec()) + .store(global(Kind.APPLY_CHECKPOINT, markers.get(1))); + new ArchiveProgressFile(readerPath, new ArchiveProgressEnvelopeCodec()) + .store(global(Kind.READER_VISIBLE, markers.get(0))); + + try (LevelDbArchiveParticipant account = new LevelDbArchiveParticipant( + archive.resolve("participants/account"), "account", PARTICIPANTS); + RocksDbArchiveParticipant asset = new RocksDbArchiveParticipant( + archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS)) { + account.apply(Collections.emptyList(), participant("account", markers.get(0))); + asset.apply(Collections.emptyList(), participant("account-asset", markers.get(0))); + Map engines = engines(account, asset); + ArchiveTargetMutationPlan activePlan = storePlan(checkpointPath, markers.get(1)); + new ArchiveProgressFile(checkpointPath, new ArchiveProgressEnvelopeCodec()) + .store(global(Kind.APPLY_CHECKPOINT, markers.get(1), activePlan.digest())); + + try (ArchiveParticipantRecoveryStorage first = new ArchiveParticipantRecoveryStorage( + archive, 4096, checkpointPath, failingEngines(account, asset), readerPath, + PARTICIPANTS)) { + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveRecoveryExecutor(first).recover()); + } + + assertEquals(2, account.loadProgress().getEpoch()); + assertArrayEquals(bytes("account:2-2"), account.get(bytes("replayed"))); + assertEquals(1, asset.loadProgress().getEpoch()); + assertNull(asset.get(bytes("replayed"))); + assertEquals(1, reader(readerPath).getEpoch()); + assertEquals(2, ArchiveRestartCheckpoint.load(archive, + new HistoryCommitMarkerCodec()).getMarker().getMeta().getEpoch()); + assertFalse(Files.exists(archive.resolve("truncation.intent"))); + + ArchiveTargetMutationPlanFile planFile = new ArchiveTargetMutationPlanFile(checkpointPath); + byte[] validPlan = Files.readAllBytes(planFile.getPath()); + Files.delete(planFile.getPath()); + assertRecoveryFails(archive, checkpointPath, engines, readerPath); + Files.write(planFile.getPath(), validPlan); + byte[] corruptPlan = Arrays.copyOf(validPlan, validPlan.length); + corruptPlan[corruptPlan.length - 1] ^= 1; + Files.write(planFile.getPath(), corruptPlan); + assertRecoveryFails(archive, checkpointPath, engines, readerPath); + storeSubstitutedPlan(checkpointPath, markers.get(1)); + assertRecoveryFails(archive, checkpointPath, engines, readerPath); + storePlan(checkpointPath, markers.get(0)); + assertRecoveryFails(archive, checkpointPath, engines, readerPath); + Files.write(planFile.getPath(), validPlan); + account.apply(Collections.emptyList(), participant("account", markers.get(1), + bytes(32, 99))); + assertRecoveryFails(archive, checkpointPath, engines, readerPath); + account.apply(Collections.emptyList(), participant("account", markers.get(1), + activePlan.digest())); + + try (ArchiveParticipantRecoveryStorage second = new ArchiveParticipantRecoveryStorage( + archive, 4096, checkpointPath, engines, readerPath, PARTICIPANTS)) { + assertEquals(2, new ArchiveRecoveryExecutor(second).recover().getActions().size()); + } + + assertEquals(2, account.loadProgress().getEpoch()); + assertEquals(2, asset.loadProgress().getEpoch()); + assertArrayEquals(bytes("account-asset:2-2"), asset.get(bytes("replayed"))); + assertEquals(2, reader(readerPath).getEpoch()); + assertFalse(Files.exists(planFile.getPath())); + + try (ArchiveParticipantRecoveryStorage third = new ArchiveParticipantRecoveryStorage( + archive, 4096, checkpointPath, engines, readerPath, PARTICIPANTS)) { + assertEquals(0, new ArchiveRecoveryExecutor(third).recover().getActions().size()); + } + } + } + + private static List mutation(String participant, long firstEpoch, + long lastEpoch) { + return Collections.singletonList(ArchiveParticipantMutation.put(bytes("replayed"), + bytes(participant + ":" + firstEpoch + "-" + lastEpoch))); + } + + private static ArchiveTargetMutationPlan storePlan(Path checkpointPath, + HistoryCommitMarker marker) + throws IOException { + Map> mutations = new LinkedHashMap<>(); + mutations.put("account", mutation("account", 2, 2)); + mutations.put("account-asset", mutation("account-asset", 2, 2)); + ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlan( + global(Kind.APPLY_CHECKPOINT, marker), mutations); + new ArchiveTargetMutationPlanFile(checkpointPath).store(plan); + return plan; + } + + private static void storeSubstitutedPlan(Path checkpointPath, HistoryCommitMarker marker) + throws IOException { + Map> mutations = new LinkedHashMap<>(); + mutations.put("account", Collections.singletonList( + ArchiveParticipantMutation.put(bytes("replayed"), bytes("substituted-account")))); + mutations.put("account-asset", Collections.singletonList( + ArchiveParticipantMutation.put(bytes("replayed"), bytes("substituted-asset")))); + new ArchiveTargetMutationPlanFile(checkpointPath).store(new ArchiveTargetMutationPlan( + global(Kind.APPLY_CHECKPOINT, marker), mutations)); + } + + private static void assertRecoveryFails(Path archive, Path checkpointPath, + Map engines, Path readerPath) throws IOException { + try (ArchiveParticipantRecoveryStorage storage = new ArchiveParticipantRecoveryStorage( + archive, 4096, checkpointPath, engines, readerPath, PARTICIPANTS)) { + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveRecoveryExecutor(storage).recover()); + } + } + + private static Map failingEngines( + ArchiveParticipant account, ArchiveParticipant asset) { + Map engines = new LinkedHashMap<>(); + engines.put("account", account); + engines.put("account-asset", new ArchiveParticipant() { + @Override + public void apply(List mutations, + ArchiveProgressEnvelope progress) throws IOException { + throw new IOException("injected second participant replay failure"); + } + + @Override + public ArchiveProgressEnvelope loadProgress() throws IOException { + return asset.loadProgress(); + } + }); + return engines; + } + + private static ArchiveProgressEnvelope reader(Path readerPath) throws IOException { + return new ArchiveProgressFile(readerPath, new ArchiveProgressEnvelopeCodec()).load(); + } + + private static List initializeHistory(Path archive, int lastEpoch) + throws Exception { + List markers = new ArrayList<>(); + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096); + HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); + HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + for (int epoch = 1; epoch <= lastEpoch; epoch++) { + BlockReverseDiff diff = new BlockReverseDiff( + new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L), + Collections.singletonList(new BlockReverseDiff.DbGroup("account", + Collections.singletonList(new BlockReverseDiff.Entry(bytes("key-" + epoch), + OldValue.present(bytes("old-" + epoch))))))); + HistoryLocation body = bodies.append(diff); + HistoryIndexLocation location = index.append(HistoryIndexRecord.from(diff, body)); + markers.add(new HistoryCommitMarker(diff.getMeta(), epoch - 1L, body, location, + bytes(16, epoch + 40), PARTICIPANTS)); + } + bodies.sync(); + index.sync(); + commits.commitAll(markers); + ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), + commits.getRecordLength(), commits.head(), new HistoryCommitMarkerCodec()); + } + return markers; + } + + private static Map engines( + ArchiveParticipant account, ArchiveParticipant asset) { + Map engines = new LinkedHashMap<>(); + engines.put("account", account); + engines.put("account-asset", asset); + return engines; + } + + private static ArchiveProgressEnvelope participant(String name, + HistoryCommitMarker marker) { + return participant(name, marker, null); + } + + private static ArchiveProgressEnvelope participant(String name, + HistoryCommitMarker marker, byte[] mutationPlanDigest) { + return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, name, + marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, PARTICIPANTS); + } + + private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { + return global(kind, marker, null); + } + + private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker, + byte[] mutationPlanDigest) { + return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, PARTICIPANTS); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java index 252fc2307f4..51bd110ad74 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java @@ -36,6 +36,10 @@ public void deterministicallyRoundTripsCheckpointParticipantAndReaderProgress() ArchiveProgressEnvelope reader = reader(10, 1); assertEnvelope(reader, codec.decode(codec.encode(reader))); + + ArchiveProgressEnvelope bound = new ArchiveProgressEnvelope(Kind.APPLY_CHECKPOINT, null, + 10, bytes(32, 1), bytes(16, 2), bytes(32, 3), bytes(32, 4), PARTICIPANTS); + assertEnvelope(bound, codec.decode(codec.encode(bound))); } @Test @@ -80,6 +84,15 @@ public void rejectsEveryExpectedIdentityMismatch() { () -> progress.requireIdentity(Kind.PARTICIPANT_PROGRESS, "account-asset", 10, bytes(32, 1), bytes(16, 2), bytes(32, 3), Arrays.asList("account", "account-asset"))); + + ArchiveProgressEnvelope bound = new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, + "account-asset", 10, bytes(32, 1), bytes(16, 2), bytes(32, 3), bytes(32, 4), + PARTICIPANTS); + bound.requireIdentity(Kind.PARTICIPANT_PROGRESS, "account-asset", 10, + bytes(32, 1), bytes(16, 2), bytes(32, 3), bytes(32, 4), PARTICIPANTS); + assertThrows(ArchivePersistenceException.class, + () -> bound.requireIdentity(Kind.PARTICIPANT_PROGRESS, "account-asset", 10, + bytes(32, 1), bytes(16, 2), bytes(32, 3), bytes(32, 5), PARTICIPANTS)); } @Test @@ -134,6 +147,7 @@ private static void assertEnvelope(ArchiveProgressEnvelope expected, assertArrayEquals(expected.getBlockHash(), actual.getBlockHash()); assertArrayEquals(expected.getBatchId(), actual.getBatchId()); assertArrayEquals(expected.getPayloadDigest(), actual.getPayloadDigest()); + assertArrayEquals(expected.getMutationPlanDigest(), actual.getMutationPlanDigest()); assertEquals(expected.getParticipants(), actual.getParticipants()); } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java index e6b077c8133..e039a5d455b 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java @@ -1,5 +1,6 @@ package org.tron.core.db2.archive; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -44,11 +45,11 @@ public void publishesExactAuthoritiesWhileMergeAndFlushAreBlocked() throws Excep CountDownLatch entered = new CountDownLatch(1); CountDownLatch release = new CountDownLatch(1); AtomicBoolean paused = new AtomicBoolean(); - Map sources = fixture.sources(); + Map sources = fixture.sources(); String first = fixture.participants.get(0); - ProgressSource original = sources.get(first); + ArchiveParticipantProgressSource original = sources.get(first); sources.put(first, () -> { - ArchiveProgressEnvelope loaded = original.load(); + ArchiveProgressEnvelope loaded = original.loadProgress(); if (paused.compareAndSet(false, true)) { entered.countDown(); try { @@ -112,7 +113,7 @@ public void missingMismatchedOrNullParticipantNeverAdvancesReader() throws Excep } try (Fixture absentLevelDb = fixture()) { - Map sources = absentLevelDb.sources(); + Map sources = absentLevelDb.sources(); sources.put("account", () -> null); ArchiveReaderPublicationGate gate = new ArchiveReaderPublicationGate(absentLevelDb.history, absentLevelDb.checkpointSource(), sources, absentLevelDb.readerPath, @@ -125,12 +126,12 @@ public void missingMismatchedOrNullParticipantNeverAdvancesReader() throws Excep @Test public void secondScanDriftAndRegressionPreserveCurrentReader() throws Exception { try (Fixture drift = fixture()) { - Map sources = drift.sources(); + Map sources = drift.sources(); String participant = drift.participants.get(0); - ProgressSource stable = sources.get(participant); + ArchiveParticipantProgressSource stable = sources.get(participant); AtomicInteger reads = new AtomicInteger(); sources.put(participant, () -> reads.getAndIncrement() == 0 - ? stable.load() : drift.envelope(Kind.PARTICIPANT_PROGRESS, participant, 0)); + ? stable.loadProgress() : drift.envelope(Kind.PARTICIPANT_PROGRESS, participant, 0)); ArchiveReaderPublicationGate gate = new ArchiveReaderPublicationGate(drift.history, drift.checkpointSource(), sources, drift.readerPath, drift.participants, action -> action.run()); @@ -163,6 +164,26 @@ public void publicationFaultKeepsOldReaderAndRetryPublishesOnce() throws Excepti } } + @Test + public void inheritsExactPlanDigestAndRejectsParticipantMismatch() throws Exception { + byte[] digest = bytes(32, 77); + try (Fixture inherited = fixture()) { + inherited.writeAuthorities(1, digest); + inherited.fileGate().publish(1); + assertArrayEquals(digest, inherited.reader().getMutationPlanDigest()); + } + + try (Fixture mismatch = fixture()) { + mismatch.writeAuthorities(1, digest); + String participant = mismatch.participants.get(0); + mismatch.store(mismatch.participantPaths.get(participant), + mismatch.envelope(Kind.PARTICIPANT_PROGRESS, participant, 1, bytes(32, 78))); + assertThrows(ArchivePersistenceException.class, + () -> mismatch.fileGate().publish(1)); + assertEquals(0, mismatch.reader().getEpoch()); + } + } + private Fixture fixture() throws Exception { return new Fixture(temporaryFolder.newFolder().toPath()); } @@ -199,18 +220,24 @@ private ProgressSource checkpointSource() { return () -> new ArchiveProgressFile(checkpointPath, codec).load(); } - private Map sources() { - Map sources = new TreeMap<>(); + private Map sources() { + Map sources = new TreeMap<>(); participantPaths.forEach((participant, path) -> sources.put(participant, () -> new ArchiveProgressFile(path, codec).load())); return sources; } private void writeAuthorities(int epoch) throws IOException { - store(checkpointPath, envelope(Kind.APPLY_CHECKPOINT, null, epoch)); + writeAuthorities(epoch, null); + } + + private void writeAuthorities(int epoch, byte[] mutationPlanDigest) throws IOException { + store(checkpointPath, + envelope(Kind.APPLY_CHECKPOINT, null, epoch, mutationPlanDigest)); for (Map.Entry entry : participantPaths.entrySet()) { store(entry.getValue(), - envelope(Kind.PARTICIPANT_PROGRESS, entry.getKey(), epoch)); + envelope(Kind.PARTICIPANT_PROGRESS, entry.getKey(), epoch, + mutationPlanDigest)); } } @@ -219,10 +246,15 @@ private ArchiveProgressEnvelope reader() throws IOException { } private ArchiveProgressEnvelope envelope(Kind kind, String participant, int epoch) { + return envelope(kind, participant, epoch, null); + } + + private ArchiveProgressEnvelope envelope(Kind kind, String participant, int epoch, + byte[] mutationPlanDigest) { HistoryCommitMarker marker = history.get(epoch); return new ArchiveProgressEnvelope(kind, participant, epoch, marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), participants); + marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, participants); } private void store(Path path, ArchiveProgressEnvelope envelope) throws IOException { diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java new file mode 100644 index 00000000000..d9ac7e931de --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java @@ -0,0 +1,276 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveTargetApplyCoordinator.Stage; + +public class ArchiveTargetApplyCoordinatorTest { + + private static final List PARTICIPANTS = + Arrays.asList("account", "account-asset"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void appliesCheckpointParticipantsRefreshAndReaderInOrder() throws Exception { + try (Fixture fixture = fixture("normal")) { + AtomicBoolean insideBarrier = new AtomicBoolean(); + ArchiveStateBarrier barrier = action -> { + assertTrue(insideBarrier.compareAndSet(false, true)); + try { + action.run(); + } finally { + insideBarrier.set(false); + } + }; + try (HistoryCommitStore history = fixture.openHistory()) { + ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, barrier); + coordinator.apply(1, plans(), () -> { + assertTrue(insideBarrier.get()); + assertEquals(1, fixture.account.loadProgress().getEpoch()); + assertEquals(1, fixture.asset.loadProgress().getEpoch()); + }); + } + + ArchiveProgressEnvelope checkpoint = fixture.checkpoint(); + ArchiveProgressEnvelope reader = fixture.reader(); + byte[] mutationPlanDigest = checkpoint.getMutationPlanDigest(); + assertEquals(1, checkpoint.getEpoch()); + assertEquals(1, reader.getEpoch()); + assertTrue(mutationPlanDigest != null); + assertArrayEquals(mutationPlanDigest, + fixture.account.loadProgress().getMutationPlanDigest()); + assertArrayEquals(mutationPlanDigest, + fixture.asset.loadProgress().getMutationPlanDigest()); + assertArrayEquals(mutationPlanDigest, reader.getMutationPlanDigest()); + assertArrayEquals(bytes("account"), fixture.account.get(bytes("normal"))); + assertArrayEquals(bytes("account-asset"), fixture.asset.get(bytes("normal"))); + assertFalse(Files.exists( + new ArchiveTargetMutationPlanFile(fixture.checkpointPath).getPath())); + } + } + + @Test + public void everyDurableStageFailureConvergesThroughFreshRecovery() throws Exception { + for (FailurePoint point : FailurePoint.values()) { + try (Fixture fixture = fixture(point.name().toLowerCase())) { + AtomicInteger refreshes = new AtomicInteger(); + try (HistoryCommitStore history = fixture.openHistory()) { + ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, + action -> action.run(), + (stage, participant) -> failAfterStage(point, stage, participant), temporary -> { + if (point == FailurePoint.DURING_PUBLICATION) { + throw new IOException("injected during publication"); + } + }, (stage, path) -> failPlanStage(point, stage)); + assertThrows(IOException.class, () -> coordinator.apply(1, plans(), () -> { + refreshes.incrementAndGet(); + if (point == FailurePoint.DURING_REFRESH) { + throw new IOException("injected during refresh"); + } + })); + } + + try (ArchiveParticipantRecoveryStorage recovery = + new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, + action -> action.run(), refreshes::incrementAndGet)) { + new ArchiveRecoveryExecutor(recovery).recover(); + } + + long expected = point.isPlanFailure() ? 0 : 1; + assertEquals(expected, fixture.checkpoint().getEpoch()); + assertEquals(expected, fixture.account.loadProgress().getEpoch()); + assertEquals(expected, fixture.asset.loadProgress().getEpoch()); + assertEquals(expected, fixture.reader().getEpoch()); + assertEquals(point.isPlanFailure() ? 0 : 1, + Math.min(refreshes.get(), 1)); + assertFalse(Files.exists( + new ArchiveTargetMutationPlanFile(fixture.checkpointPath).getPath())); + + try (ArchiveParticipantRecoveryStorage fixed = + new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { + assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); + } + } + } + } + + private Fixture fixture(String name) throws Exception { + return new Fixture(temporaryFolder.newFolder(name).toPath()); + } + + private static Map> plans() { + Map> plans = new LinkedHashMap<>(); + plans.put("account", Collections.singletonList( + ArchiveParticipantMutation.put(bytes("normal"), bytes("account")))); + plans.put("account-asset", Collections.singletonList( + ArchiveParticipantMutation.put(bytes("normal"), bytes("account-asset")))); + return plans; + } + + private static void failAfterStage(FailurePoint point, Stage stage, String participant) + throws IOException { + if (point == FailurePoint.AFTER_CHECKPOINT && stage == Stage.AFTER_CHECKPOINT + || point == FailurePoint.AFTER_FIRST_PARTICIPANT + && stage == Stage.AFTER_PARTICIPANT && "account".equals(participant) + || point == FailurePoint.AFTER_READER && stage == Stage.AFTER_READER) { + throw new IOException("injected at " + point); + } + } + + private static void failPlanStage(FailurePoint point, + ArchiveTargetMutationPlanFile.Stage stage) throws IOException { + if (point == FailurePoint.AFTER_PLAN_TEMPORARY_FORCE + && stage == ArchiveTargetMutationPlanFile.Stage.AFTER_TEMPORARY_FORCE + || point == FailurePoint.AFTER_PLAN_REPLACE + && stage == ArchiveTargetMutationPlanFile.Stage.AFTER_REPLACE) { + throw new IOException("injected at " + point); + } + } + + private enum FailurePoint { + AFTER_PLAN_TEMPORARY_FORCE, + AFTER_PLAN_REPLACE, + AFTER_CHECKPOINT, + AFTER_FIRST_PARTICIPANT, + DURING_REFRESH, + DURING_PUBLICATION, + AFTER_READER; + + private boolean isPlanFailure() { + return this == AFTER_PLAN_TEMPORARY_FORCE || this == AFTER_PLAN_REPLACE; + } + } + + private static final class Fixture implements AutoCloseable { + private final Path archive; + private final Path checkpointPath; + private final Path readerPath; + private final LevelDbArchiveParticipant account; + private final RocksDbArchiveParticipant asset; + private final ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + + private Fixture(Path archive) throws Exception { + this.archive = archive; + List markers = initializeHistory(archive); + checkpointPath = archive.resolve("progress/checkpoint.progress"); + readerPath = archive.resolve("progress/reader.progress"); + new ArchiveProgressFile(checkpointPath, codec).store( + global(Kind.APPLY_CHECKPOINT, markers.get(0))); + new ArchiveProgressFile(readerPath, codec).store( + global(Kind.READER_VISIBLE, markers.get(0))); + account = new LevelDbArchiveParticipant( + archive.resolve("participants/account"), "account", PARTICIPANTS); + asset = new RocksDbArchiveParticipant( + archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS); + account.apply(Collections.emptyList(), participant("account", markers.get(0))); + asset.apply(Collections.emptyList(), participant("account-asset", markers.get(0))); + } + + private HistoryCommitStore openHistory() throws IOException { + return new HistoryCommitStore(archive, new HistoryCommitMarkerCodec()); + } + + private Map engines() { + Map engines = new LinkedHashMap<>(); + engines.put("account", account); + engines.put("account-asset", asset); + return engines; + } + + private ArchiveProgressEnvelope checkpoint() throws IOException { + return new ArchiveProgressFile(checkpointPath, codec).load(); + } + + private ArchiveProgressEnvelope reader() throws IOException { + return new ArchiveProgressFile(readerPath, codec).load(); + } + + @Override + public void close() throws IOException { + asset.close(); + account.close(); + } + } + + private static List initializeHistory(Path archive) throws Exception { + List markers = new ArrayList<>(); + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096); + HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); + HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + for (int epoch = 0; epoch <= 1; epoch++) { + BlockReverseDiff diff = new BlockReverseDiff( + new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L), + Collections.singletonList(new BlockReverseDiff.DbGroup("account", + Collections.singletonList(new BlockReverseDiff.Entry(bytes("key-" + epoch), + OldValue.present(bytes("old-" + epoch))))))); + HistoryLocation body = bodies.append(diff); + HistoryIndexLocation location = index.append(HistoryIndexRecord.from(diff, body)); + markers.add(new HistoryCommitMarker(diff.getMeta(), epoch - 1L, body, location, + bytes(16, epoch + 40), PARTICIPANTS)); + } + bodies.sync(); + index.sync(); + commits.commitAll(markers); + ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), + commits.getRecordLength(), commits.head(), new HistoryCommitMarkerCodec()); + } + return markers; + } + + private static ArchiveProgressEnvelope participant(String participant, + HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, participant, + marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java new file mode 100644 index 00000000000..6fb6db7cee5 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java @@ -0,0 +1,193 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveParticipantMutationBatch.Mutation; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; + +public class ArchiveTargetMutationPlanBuilderTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void canonicalizesExactPhysicalMutationsAndOwnsInputBytes() { + HistoryCommitMarker target = marker(1, participants()); + byte[] key = bytes(3, 3); + byte[] value = bytes(2, 7); + ArchiveParticipantMutationBatch first = new ArchiveParticipantMutationBatch(target, + Arrays.asList(Mutation.delete("storage-row", bytes(3, 2)), + Mutation.put("account", key, value), + Mutation.put("account", bytes(3, 1), new byte[0]))); + key[0] = 99; + value[0] = 99; + ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(target, first); + + assertArrayEquals(bytes(3, 1), plan.getMutations("account").get(0).getKey()); + assertArrayEquals(new byte[0], plan.getMutations("account").get(0).getValue()); + assertArrayEquals(bytes(3, 3), plan.getMutations("account").get(1).getKey()); + assertArrayEquals(bytes(2, 7), plan.getMutations("account").get(1).getValue()); + assertNull(plan.getMutations("storage-row").get(0).getValue()); + assertEquals(participants(), new ArrayList<>(plan.getMutations().keySet())); + + ArchiveParticipantMutationBatch reordered = new ArchiveParticipantMutationBatch(target, + Arrays.asList(Mutation.put("account", bytes(3, 1), new byte[0]), + Mutation.put("account", bytes(3, 3), bytes(2, 7)), + Mutation.delete("storage-row", bytes(3, 2)))); + assertArrayEquals(plan.digest(), + new ArchiveTargetMutationPlanBuilder().build(target, reordered).digest()); + } + + @Test + public void rejectsUnknownDerivedAndDuplicatePhysicalKeys() { + HistoryCommitMarker target = marker(1, participants()); + assertBuildFails(target, Collections.singletonList( + Mutation.put("unknown-db", bytes(1, 1), bytes(1, 2)))); + assertBuildFails(target, Collections.singletonList( + Mutation.delete("accountTrie", bytes(1, 1)))); + assertThrows(IllegalArgumentException.class, + () -> new ArchiveTargetMutationPlanBuilder().build(target, + new ArchiveParticipantMutationBatch(target, Arrays.asList( + Mutation.put("account", bytes(1, 1), bytes(1, 2)), + Mutation.delete("account", bytes(1, 1)))))); + } + + @Test + public void rejectsTargetIdentityAndExactParticipantSetMismatch() { + HistoryCommitMarker target = marker(1, participants()); + ArchiveParticipantMutationBatch batch = new ArchiveParticipantMutationBatch(target, + Collections.emptyList()); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveTargetMutationPlanBuilder().build(marker(2, participants()), batch)); + + List incomplete = Arrays.asList("account", "account-asset"); + HistoryCommitMarker incompleteTarget = marker(1, incomplete); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveTargetMutationPlanBuilder().build(incompleteTarget, + new ArchiveParticipantMutationBatch(incompleteTarget, Collections.emptyList()))); + } + + @Test + public void coordinatorConsumesImmutableBatchAndPublishesExactDigest() throws Exception { + Path archive = temporaryFolder.newFolder("coordinator-producer").toPath(); + List participants = participants(); + HistoryCommitMarker zero = marker(0, participants); + HistoryCommitMarker one = marker(1, participants); + Path checkpointPath = archive.resolve("progress/checkpoint.progress"); + Path readerPath = archive.resolve("progress/reader.progress"); + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + new ArchiveProgressFile(checkpointPath, codec).store(global(Kind.APPLY_CHECKPOINT, zero)); + new ArchiveProgressFile(readerPath, codec).store(global(Kind.READER_VISIBLE, zero)); + Map recording = new LinkedHashMap<>(); + Map engines = new LinkedHashMap<>(); + for (String participant : participants) { + RecordingParticipant engine = new RecordingParticipant( + progress(participant, zero)); + recording.put(participant, engine); + engines.put(participant, engine); + } + try (HistoryCommitStore history = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + history.commitAll(Arrays.asList(zero, one)); + ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, + checkpointPath, engines, readerPath, participants, action -> action.run()); + coordinator.apply(new ArchiveParticipantMutationBatch(one, Arrays.asList( + Mutation.put("account", bytes(2, 1), new byte[0]), + Mutation.delete("storage-row", bytes(2, 2)))), () -> { }); + } + + ArchiveProgressEnvelope checkpoint = new ArchiveProgressFile(checkpointPath, codec).load(); + ArchiveProgressEnvelope reader = new ArchiveProgressFile(readerPath, codec).load(); + assertArrayEquals(checkpoint.getMutationPlanDigest(), reader.getMutationPlanDigest()); + assertArrayEquals(checkpoint.getMutationPlanDigest(), + recording.get("account").progress.getMutationPlanDigest()); + assertEquals(1, recording.get("account").mutations.size()); + assertArrayEquals(new byte[0], recording.get("account").mutations.get(0).getValue()); + assertNull(recording.get("storage-row").mutations.get(0).getValue()); + assertEquals(0, recording.get("witness").mutations.size()); + assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); + } + + private static void assertBuildFails(HistoryCommitMarker target, List mutations) { + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveTargetMutationPlanBuilder().build(target, + new ArchiveParticipantMutationBatch(target, mutations))); + } + + private static List participants() { + List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(participants); + return participants; + } + + private static HistoryCommitMarker marker(long epoch, List participants) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, hash((int) epoch), + hash((int) epoch - 1), epoch * 1_000L); + return new HistoryCommitMarker(meta, epoch - 1, + new HistoryLocation(0, epoch * 100, 100, (int) epoch, + bytes(32, (int) epoch + 20)), + new HistoryIndexLocation(epoch * 50, 50, bytes(32, (int) epoch + 30)), + bytes(16, (int) epoch + 40), participants); + } + + private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); + } + + private static ArchiveProgressEnvelope progress(String participant, + HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, participant, + marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); + } + + private static byte[] hash(int suffix) { + byte[] value = new byte[32]; + value[31] = (byte) suffix; + return value; + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } + + private static final class RecordingParticipant implements ArchiveParticipant { + private List mutations = Collections.emptyList(); + private ArchiveProgressEnvelope progress; + + private RecordingParticipant(ArchiveProgressEnvelope progress) { + this.progress = progress; + } + + @Override + public void apply(List mutations, + ArchiveProgressEnvelope progress) { + this.mutations = new ArrayList<>(mutations); + this.progress = progress; + } + + @Override + public ArchiveProgressEnvelope loadProgress() { + return progress; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java new file mode 100644 index 00000000000..6d66e6eee1d --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java @@ -0,0 +1,134 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveTargetMutationPlanFile.Stage; + +public class ArchiveTargetMutationPlanFileTest { + + private static final List PARTICIPANTS = + Arrays.asList("account", "account-asset"); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void codecRoundTripsExactPutDeleteAndEmptyValues() { + ArchiveTargetMutationPlan plan = plan(1); + ArchiveTargetMutationPlan decoded = new ArchiveTargetMutationPlanCodec().decode( + new ArchiveTargetMutationPlanCodec().encode(plan)); + + assertEquals(1, decoded.getTarget().getEpoch()); + assertEquals(PARTICIPANTS, decoded.getTarget().getParticipants()); + assertArrayEquals(bytes(3, 1), decoded.getMutations("account").get(0).getKey()); + assertArrayEquals(new byte[0], decoded.getMutations("account").get(0).getValue()); + assertNull(decoded.getMutations("account-asset").get(0).getValue()); + assertArrayEquals(plan.digest(), decoded.digest()); + + Map> substituted = new LinkedHashMap<>( + plan.getMutations()); + substituted.put("account", Collections.singletonList( + ArchiveParticipantMutation.put(bytes(3, 1), bytes(1, 9)))); + ArchiveTargetMutationPlan replacement = new ArchiveTargetMutationPlan( + plan.getTarget(), substituted); + assertFalse(Arrays.equals(plan.digest(), replacement.digest())); + } + + @Test + public void atomicFaultExposesOnlyOldOrNewPlan() throws Exception { + Path checkpoint = temporaryFolder.newFolder("atomic").toPath().resolve("checkpoint.progress"); + ArchiveTargetMutationPlanFile normal = new ArchiveTargetMutationPlanFile(checkpoint); + normal.store(plan(0)); + + ArchiveTargetMutationPlanFile beforeReplace = new ArchiveTargetMutationPlanFile(checkpoint, + (stage, path) -> { + if (stage == Stage.AFTER_TEMPORARY_FORCE) { + throw new IOException("injected before replace"); + } + }); + assertThrows(IOException.class, () -> beforeReplace.store(plan(1))); + assertEquals(0, normal.loadRequired().getTarget().getEpoch()); + + ArchiveTargetMutationPlanFile afterReplace = new ArchiveTargetMutationPlanFile(checkpoint, + (stage, path) -> { + if (stage == Stage.AFTER_REPLACE) { + throw new IOException("injected after replace"); + } + }); + assertThrows(IOException.class, () -> afterReplace.store(plan(1))); + assertEquals(1, normal.loadRequired().getTarget().getEpoch()); + } + + @Test + public void rejectsChecksumCorruptionAndTruncation() throws Exception { + Path checkpoint = temporaryFolder.newFolder("corrupt").toPath().resolve("checkpoint.progress"); + ArchiveTargetMutationPlanFile file = new ArchiveTargetMutationPlanFile(checkpoint); + file.store(plan(1)); + byte[] encoded = Files.readAllBytes(file.getPath()); + encoded[encoded.length - 1] ^= 1; + Files.write(file.getPath(), encoded); + assertThrows(ArchivePersistenceException.class, file::loadRequired); + + Files.write(file.getPath(), Arrays.copyOf(encoded, 10)); + assertThrows(ArchivePersistenceException.class, file::loadRequired); + } + + @Test + public void canonicalizesContainerOrderAndRejectsDuplicatePhysicalKeys() { + ArchiveProgressEnvelope target = plan(1).getTarget(); + Map> first = new LinkedHashMap<>(); + first.put("account-asset", Collections.singletonList( + ArchiveParticipantMutation.delete(bytes(3, 2)))); + first.put("account", Arrays.asList( + ArchiveParticipantMutation.put(bytes(3, 3), bytes(1, 3)), + ArchiveParticipantMutation.put(bytes(3, 1), bytes(1, 1)))); + Map> second = new LinkedHashMap<>(); + second.put("account", Arrays.asList( + ArchiveParticipantMutation.put(bytes(3, 1), bytes(1, 1)), + ArchiveParticipantMutation.put(bytes(3, 3), bytes(1, 3)))); + second.put("account-asset", Collections.singletonList( + ArchiveParticipantMutation.delete(bytes(3, 2)))); + assertArrayEquals(new ArchiveTargetMutationPlan(target, first).digest(), + new ArchiveTargetMutationPlan(target, second).digest()); + + second.put("account", Arrays.asList( + ArchiveParticipantMutation.put(bytes(3, 1), bytes(1, 1)), + ArchiveParticipantMutation.delete(bytes(3, 1)))); + assertThrows(IllegalArgumentException.class, + () -> new ArchiveTargetMutationPlan(target, second)); + } + + private static ArchiveTargetMutationPlan plan(long epoch) { + ArchiveProgressEnvelope target = new ArchiveProgressEnvelope(Kind.APPLY_CHECKPOINT, null, + epoch, bytes(32, (int) epoch), bytes(16, (int) epoch + 10), + bytes(32, (int) epoch + 20), PARTICIPANTS); + Map> mutations = new LinkedHashMap<>(); + mutations.put("account", Collections.singletonList( + ArchiveParticipantMutation.put(bytes(3, 1), new byte[0]))); + mutations.put("account-asset", Collections.singletonList( + ArchiveParticipantMutation.delete(bytes(3, 2)))); + return new ArchiveTargetMutationPlan(target, mutations); + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java b/framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java index cdb0fcd1171..1a74a5e540e 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java @@ -14,7 +14,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.LevelDbArchiveParticipant.Mutation; import org.tron.core.db2.archive.LevelDbArchiveParticipant.Stage; public class LevelDbArchiveParticipantTest { @@ -31,14 +30,16 @@ public void nativeBatchExposesOnlyOldOldOrNewNewAcrossFailureBoundaries() throws Path directory = temporaryFolder.newFolder("native-" + failedStage).toPath(); try (LevelDbArchiveParticipant participant = new LevelDbArchiveParticipant( directory, "account", PARTICIPANTS)) { - participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("old"))), + participant.apply(Collections.singletonList( + ArchiveParticipantMutation.put(bytes("key"), bytes("old"))), progress(1)); } try (LevelDbArchiveParticipant failing = new LevelDbArchiveParticipant( directory, "account", PARTICIPANTS, stage -> failAt(failedStage, stage))) { assertThrows(IOException.class, () -> failing.apply( - Collections.singletonList(Mutation.put(bytes("key"), bytes("new"))), progress(2))); + Collections.singletonList( + ArchiveParticipantMutation.put(bytes("key"), bytes("new"))), progress(2))); } try (LevelDbArchiveParticipant reopened = new LevelDbArchiveParticipant( @@ -56,9 +57,11 @@ public void deleteAndProgressShareTheSameNativeBatch() throws Exception { Path directory = temporaryFolder.newFolder("native-delete").toPath(); try (LevelDbArchiveParticipant participant = new LevelDbArchiveParticipant( directory, "account", PARTICIPANTS)) { - participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("value"))), + participant.apply(Collections.singletonList( + ArchiveParticipantMutation.put(bytes("key"), bytes("value"))), progress(1)); - participant.apply(Collections.singletonList(Mutation.delete(bytes("key"))), progress(2)); + participant.apply(Collections.singletonList( + ArchiveParticipantMutation.delete(bytes("key"))), progress(2)); assertNull(participant.get(bytes("key"))); assertEquals(2, participant.loadProgress().getEpoch()); } @@ -69,13 +72,15 @@ public void resetClearsBusinessAndProgressBeforeAConsistentReapply() throws Exce Path directory = temporaryFolder.newFolder("native-reset").toPath(); try (LevelDbArchiveParticipant participant = new LevelDbArchiveParticipant( directory, "account", PARTICIPANTS)) { - participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("old"))), + participant.apply(Collections.singletonList( + ArchiveParticipantMutation.put(bytes("key"), bytes("old"))), progress(1)); participant.reset(); assertNull(participant.get(bytes("key"))); assertThrows(ArchivePersistenceException.class, participant::loadProgress); - participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("new"))), + participant.apply(Collections.singletonList( + ArchiveParticipantMutation.put(bytes("key"), bytes("new"))), progress(2)); } @@ -95,7 +100,8 @@ public void rejectsProgressForAnotherParticipantBeforeNativeWrite() throws Excep ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, "account-asset", 1, bytes(32, 1), bytes(16, 2), bytes(32, 3), PARTICIPANTS); assertThrows(IllegalArgumentException.class, () -> participant.apply( - Collections.singletonList(Mutation.put(bytes("key"), bytes("value"))), wrong)); + Collections.singletonList( + ArchiveParticipantMutation.put(bytes("key"), bytes("value"))), wrong)); assertNull(participant.get(bytes("key"))); assertThrows(ArchivePersistenceException.class, participant::loadProgress); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java b/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java index c1d07661cb6..e66f8dcf97d 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java @@ -16,7 +16,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.RocksDbArchiveParticipant.Mutation; import org.tron.core.db2.archive.RocksDbArchiveParticipant.Stage; public class RocksDbArchiveParticipantTest { @@ -33,14 +32,16 @@ public void nativeBatchExposesOnlyOldOldOrNewNewAcrossFailureBoundaries() throws Path directory = temporaryFolder.newFolder("native-" + failedStage).toPath(); try (RocksDbArchiveParticipant participant = new RocksDbArchiveParticipant( directory, "account", PARTICIPANTS)) { - participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("old"))), + participant.apply(Collections.singletonList( + ArchiveParticipantMutation.put(bytes("key"), bytes("old"))), progress(1)); } try (RocksDbArchiveParticipant failing = new RocksDbArchiveParticipant( directory, "account", PARTICIPANTS, stage -> failAt(failedStage, stage))) { assertThrows(IOException.class, () -> failing.apply( - Collections.singletonList(Mutation.put(bytes("key"), bytes("new"))), progress(2))); + Collections.singletonList( + ArchiveParticipantMutation.put(bytes("key"), bytes("new"))), progress(2))); } try (RocksDbArchiveParticipant reopened = new RocksDbArchiveParticipant( @@ -58,9 +59,11 @@ public void deleteAndProgressShareTheSameNativeBatch() throws Exception { Path directory = temporaryFolder.newFolder("native-delete").toPath(); try (RocksDbArchiveParticipant participant = new RocksDbArchiveParticipant( directory, "account", PARTICIPANTS)) { - participant.apply(Collections.singletonList(Mutation.put(bytes("key"), bytes("value"))), + participant.apply(Collections.singletonList( + ArchiveParticipantMutation.put(bytes("key"), bytes("value"))), progress(1)); - participant.apply(Collections.singletonList(Mutation.delete(bytes("key"))), progress(2)); + participant.apply(Collections.singletonList( + ArchiveParticipantMutation.delete(bytes("key"))), progress(2)); assertNull(participant.get(bytes("key"))); assertEquals(2, participant.loadProgress().getEpoch()); } @@ -75,7 +78,8 @@ public void rejectsProgressForAnotherParticipantBeforeNativeWrite() throws Excep ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, "account-asset", 1, bytes(32, 1), bytes(16, 2), bytes(32, 3), PARTICIPANTS); assertThrows(IllegalArgumentException.class, () -> participant.apply( - Collections.singletonList(Mutation.put(bytes("key"), bytes("value"))), wrong)); + Collections.singletonList( + ArchiveParticipantMutation.put(bytes("key"), bytes("value"))), wrong)); assertNull(participant.get(bytes("key"))); assertThrows(ArchivePersistenceException.class, participant::loadProgress); } @@ -106,7 +110,7 @@ public void recoveryScannerReadsParticipantProgressFromNativeEngines() throws Ex engines.put("account-asset", asset); ArchiveRecoveryExecutor.RecoverySnapshot snapshot = - ArchiveRecoveryAuthorityScanner.forRocksDbParticipants(history, checkpointPath, + ArchiveRecoveryAuthorityScanner.forParticipants(history, checkpointPath, engines, readerPath, PARTICIPANTS).scan(); assertEquals(2, snapshot.getHistoryHead()); assertEquals(2, snapshot.getCheckpointHead()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorageTest.java b/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorageTest.java deleted file mode 100644 index 0026dd77f27..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveRecoveryStorageTest.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.RocksDbArchiveParticipant.Mutation; - -public class RocksDbArchiveRecoveryStorageTest { - - private static final List PARTICIPANTS = - Arrays.asList("account", "account-asset"); - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void executorConvergesHistoryNativeParticipantsAndReaderHead() throws Exception { - Path archive = temporaryFolder.newFolder("native-recovery").toPath(); - Path checkpointPath = archive.resolve("progress/checkpoint.progress"); - Path readerPath = archive.resolve("progress/reader.progress"); - List markers = initializeHistory(archive, 3); - new ArchiveProgressFile(checkpointPath, new ArchiveProgressEnvelopeCodec()) - .store(global(Kind.APPLY_CHECKPOINT, markers.get(1))); - new ArchiveProgressFile(readerPath, new ArchiveProgressEnvelopeCodec()) - .store(global(Kind.READER_VISIBLE, markers.get(0))); - - try (RocksDbArchiveParticipant account = new RocksDbArchiveParticipant( - archive.resolve("participants/account"), "account", PARTICIPANTS); - RocksDbArchiveParticipant asset = new RocksDbArchiveParticipant( - archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS)) { - account.apply(Collections.emptyList(), participant("account", markers.get(1))); - asset.apply(Collections.emptyList(), participant("account-asset", markers.get(0))); - Map engines = engines(account, asset); - - try (RocksDbArchiveRecoveryStorage storage = new RocksDbArchiveRecoveryStorage( - archive, 4096, checkpointPath, engines, readerPath, PARTICIPANTS, - (name, first, last) -> Collections.singletonList( - Mutation.put(bytes("replayed"), bytes(name + ":" + first + "-" + last))))) { - assertEquals(3, new ArchiveRecoveryExecutor(storage).recover().getActions().size()); - } - - assertEquals(2, asset.loadProgress().getEpoch()); - assertArrayEquals(bytes("account-asset:2-2"), asset.get(bytes("replayed"))); - assertEquals(2, new ArchiveProgressFile(readerPath, - new ArchiveProgressEnvelopeCodec()).load().getEpoch()); - assertEquals(2, ArchiveRestartCheckpoint.load(archive, - new HistoryCommitMarkerCodec()).getMarker().getMeta().getEpoch()); - assertFalse(Files.exists(archive.resolve("truncation.intent"))); - - try (RocksDbArchiveRecoveryStorage reopened = new RocksDbArchiveRecoveryStorage( - archive, 4096, checkpointPath, engines, readerPath, PARTICIPANTS, - (name, first, last) -> { - throw new AssertionError("fixed-point recovery must not replay"); - })) { - assertEquals(0, new ArchiveRecoveryExecutor(reopened).recover().getActions().size()); - } - } - } - - private static List initializeHistory(Path archive, int lastEpoch) - throws Exception { - List markers = new ArrayList<>(); - try (HistorySegmentStore bodies = new HistorySegmentStore( - archive, new BlockHistoryCodec(), 4096); - HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); - HistoryCommitStore commits = new HistoryCommitStore( - archive, new HistoryCommitMarkerCodec())) { - for (int epoch = 1; epoch <= lastEpoch; epoch++) { - BlockReverseDiff diff = new BlockReverseDiff( - new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L), - Collections.singletonList(new BlockReverseDiff.DbGroup("account", - Collections.singletonList(new BlockReverseDiff.Entry(bytes("key-" + epoch), - OldValue.present(bytes("old-" + epoch))))))); - HistoryLocation body = bodies.append(diff); - HistoryIndexLocation location = index.append(HistoryIndexRecord.from(diff, body)); - markers.add(new HistoryCommitMarker(diff.getMeta(), epoch - 1L, body, location, - bytes(16, epoch + 40), PARTICIPANTS)); - } - bodies.sync(); - index.sync(); - commits.commitAll(markers); - ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), - commits.getRecordLength(), commits.head(), new HistoryCommitMarkerCodec()); - } - return markers; - } - - private static Map engines( - RocksDbArchiveParticipant account, RocksDbArchiveParticipant asset) { - Map engines = new LinkedHashMap<>(); - engines.put("account", account); - engines.put("account-asset", asset); - return engines; - } - - private static ArchiveProgressEnvelope participant(String name, - HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, name, - marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - } - - private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - } - - private static byte[] hash(int suffix) { - byte[] hash = new byte[32]; - hash[31] = (byte) suffix; - return hash; - } - - private static byte[] bytes(String value) { - return value.getBytes(StandardCharsets.UTF_8); - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } -} From 3a7e7263fc0696c4caf7bd730de488f763c5c752 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 12:01:56 +0800 Subject: [PATCH 014/161] feat(chainbase): capture archive mutations Collect exact ordinary and AccountAsset post-state without Store scans. Enforce target identity, bounded ownership, one-shot sealing, abort, and terminal payload release. --- .../AccountAssetForwardMutationManifest.java | 181 ++++ .../AccountAssetForwardMutationRecorder.java | 243 +++++ .../archive/AccountAssetForwardProjector.java | 67 ++ .../ArchiveBlockForwardMutationCapture.java | 105 +++ .../ArchiveBlockForwardMutationLimits.java | 44 + ...hiveParticipantMutationBatchCollector.java | 112 +++ ...ParticipantMutationBatchCollectorTest.java | 869 ++++++++++++++++++ 7 files changed, 1621 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardProjector.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationLimits.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java new file mode 100644 index 00000000000..332126cdb9e --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java @@ -0,0 +1,181 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.TreeMap; +import java.util.TreeSet; +import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; +import org.tron.core.db2.archive.AccountAssetForwardProjector.Projection; +import org.tron.core.db2.archive.BlockChangeView.PostValue; + +/** Immutable one-shot account projection input bound to one exact committed target. */ +public final class AccountAssetForwardMutationManifest implements AccountAssetForwardProjector { + + private final byte[] encodedTarget; + private final TreeMap entries = new TreeMap<>(); + private final TreeSet consumed = new TreeSet<>(); + private boolean begun; + private boolean completed; + + public AccountAssetForwardMutationManifest(HistoryCommitMarker target, List entries) { + HistoryCommitMarker expectedTarget = Objects.requireNonNull(target, "target"); + if (!expectedTarget.getDatabases().equals(sortedParticipants())) { + throw new IllegalArgumentException("Manifest target must cover exact VERSIONED_STATE set"); + } + encodedTarget = new HistoryCommitMarkerCodec().encode(expectedTarget); + for (Entry entry : Objects.requireNonNull(entries, "entries")) { + if (entry == null) { + throw new IllegalArgumentException("Manifest contains null entry"); + } + Key key = new Key(entry.accountPhysicalKey); + if (this.entries.put(key, entry) != null) { + throw new IllegalArgumentException("Duplicate manifest account physical key"); + } + } + } + + @Override + public synchronized void begin(HistoryCommitMarker target, + List changedAccountPhysicalKeys) { + if (begun || completed) { + throw new ArchivePersistenceException("AccountAsset manifest is one-shot"); + } + if (!Arrays.equals(encodedTarget, + new HistoryCommitMarkerCodec().encode(Objects.requireNonNull(target, "target")))) { + throw new ArchivePersistenceException("AccountAsset manifest target identity mismatch"); + } + TreeMap changed = new TreeMap<>(); + for (byte[] key : Objects.requireNonNull(changedAccountPhysicalKeys, + "changedAccountPhysicalKeys")) { + if (changed.put(new Key(key), Boolean.TRUE) != null) { + throw new ArchivePersistenceException("Duplicate changed account physical key"); + } + } + if (!changed.keySet().equals(entries.keySet())) { + throw new ArchivePersistenceException( + "AccountAsset manifest does not exactly cover changed account keys"); + } + begun = true; + } + + @Override + public synchronized Projection project(byte[] accountPhysicalKey, + PostValue rawAccountPostValue) { + if (!begun || completed) { + throw new ArchivePersistenceException("AccountAsset manifest is not active"); + } + Entry entry = entries.get(new Key(accountPhysicalKey)); + if (entry == null) { + throw new ArchivePersistenceException("AccountAsset manifest entry is missing"); + } + Key key = new Key(accountPhysicalKey); + if (consumed.contains(key)) { + throw new ArchivePersistenceException("AccountAsset manifest entry was already consumed"); + } + if (!samePostValue(entry.rawAccountPostValue, + Objects.requireNonNull(rawAccountPostValue, "rawAccountPostValue"))) { + throw new ArchivePersistenceException("AccountAsset manifest raw account value mismatch"); + } + consumed.add(key); + return entry.projection; + } + + @Override + public synchronized void complete() { + if (!begun || completed) { + throw new ArchivePersistenceException("AccountAsset manifest is not active"); + } + if (consumed.size() != entries.size()) { + throw new ArchivePersistenceException("AccountAsset manifest contains unused entry"); + } + completed = true; + } + + private static boolean samePostValue(PostValue left, PostValue right) { + return left.isPresent() == right.isPresent() + && (!left.isPresent() || Arrays.equals(left.getValue(), right.getValue())); + } + + private static List sortedParticipants() { + List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(participants); + return participants; + } + + /** One changed account's exact raw input and canonical physical outputs. */ + public static final class Entry { + private final byte[] accountPhysicalKey; + private final PostValue rawAccountPostValue; + private final Projection projection; + + public Entry(byte[] accountPhysicalKey, PostValue rawAccountPostValue, + PostValue canonicalAccountPostValue, List assetMutations) { + this.accountPhysicalKey = Arrays.copyOf( + Objects.requireNonNull(accountPhysicalKey, "accountPhysicalKey"), + accountPhysicalKey.length); + this.rawAccountPostValue = Objects.requireNonNull(rawAccountPostValue, + "rawAccountPostValue"); + PostValue canonical = Objects.requireNonNull(canonicalAccountPostValue, + "canonicalAccountPostValue"); + if (rawAccountPostValue.isPresent() != canonical.isPresent()) { + throw new IllegalArgumentException( + "Raw and canonical account presence must match"); + } + List mutations = new ArrayList<>(Objects.requireNonNull(assetMutations, + "assetMutations")); + TreeMap assetKeys = new TreeMap<>(); + for (AssetMutation mutation : mutations) { + if (mutation == null) { + throw new IllegalArgumentException("Manifest entry contains null asset mutation"); + } + byte[] assetKey = mutation.getPhysicalRawKey(); + if (!strictlyExtends(this.accountPhysicalKey, assetKey)) { + throw new IllegalArgumentException( + "AccountAsset physical key does not belong to account"); + } + if (assetKeys.put(new Key(assetKey), Boolean.TRUE) != null) { + throw new IllegalArgumentException("Duplicate account-asset physical key"); + } + } + projection = new Projection(canonical, mutations); + } + + private static boolean strictlyExtends(byte[] prefix, byte[] value) { + if (value.length <= prefix.length) { + return false; + } + for (int i = 0; i < prefix.length; i++) { + if (prefix[i] != value[i]) { + return false; + } + } + return true; + } + } + + private static final class Key implements Comparable { + private final byte[] value; + + private Key(byte[] value) { + this.value = Arrays.copyOf(Objects.requireNonNull(value, "key"), value.length); + } + + @Override + public int compareTo(Key other) { + return BlockReverseDiff.compareUnsigned(value, other.value); + } + + @Override + public boolean equals(Object object) { + return object instanceof Key && Arrays.equals(value, ((Key) object).value); + } + + @Override + public int hashCode() { + return Arrays.hashCode(value); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java new file mode 100644 index 00000000000..43f3ac70bbf --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java @@ -0,0 +1,243 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.TreeMap; +import org.tron.core.db2.archive.AccountAssetForwardMutationManifest.Entry; +import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; +import org.tron.core.db2.archive.BlockChangeView.PostValue; + +/** Collects explicit execution-time AccountAsset events for one target without Store reads. */ +public final class AccountAssetForwardMutationRecorder { + + private final BlockSnapshotMeta targetMeta; + private final ArchiveBlockForwardMutationLimits limits; + private final TreeMap accounts = new TreeMap<>(); + private int accountCount; + private int assetMutationCount; + private long totalPayloadBytes; + private boolean sealed; + + public AccountAssetForwardMutationRecorder(BlockSnapshotMeta targetMeta, + ArchiveBlockForwardMutationLimits limits) { + this.targetMeta = Objects.requireNonNull(targetMeta, "targetMeta"); + this.limits = Objects.requireNonNull(limits, "limits"); + } + + public synchronized void recordAccount(BlockSnapshotMeta eventMeta, + byte[] accountPhysicalKey, PostValue rawAccountPostValue, + PostValue canonicalAccountPostValue) { + requireOpenMeta(eventMeta); + PostValue raw = Objects.requireNonNull(rawAccountPostValue, "rawAccountPostValue"); + PostValue canonical = Objects.requireNonNull(canonicalAccountPostValue, + "canonicalAccountPostValue"); + if (raw.isPresent() != canonical.isPresent()) { + throw new ArchivePersistenceException( + "Raw and canonical account presence must match"); + } + Key key = new Key(accountPhysicalKey); + requireKeyLength(key.value.length); + AccountEvents current = accounts.get(key); + if (current != null && current.rawAccountPostValue != null) { + throw new ArchivePersistenceException("Duplicate AccountAsset account event"); + } + long rawBytes = valueLength(raw); + long canonicalBytes = valueLength(canonical); + AccountEvents account = current == null ? new AccountEvents(key.value) : current; + reserve(current == null, false, + (current == null ? key.value.length : 0L) + rawBytes + canonicalBytes); + if (current == null) { + accounts.put(key, account); + } + account.rawAccountPostValue = raw; + account.canonicalAccountPostValue = canonical; + } + + public synchronized void recordAssetPut(BlockSnapshotMeta eventMeta, + byte[] accountPhysicalKey, byte[] assetPhysicalKey, byte[] value) { + recordAsset(eventMeta, accountPhysicalKey, assetPhysicalKey, + PostValue.present(Objects.requireNonNull(value, "value"))); + } + + public synchronized void recordAssetDelete(BlockSnapshotMeta eventMeta, + byte[] accountPhysicalKey, byte[] assetPhysicalKey) { + recordAsset(eventMeta, accountPhysicalKey, assetPhysicalKey, PostValue.absent()); + } + + public synchronized AccountAssetForwardMutationManifest seal(HistoryCommitMarker sealTarget) { + requireOpen(); + HistoryCommitMarker committedTarget = Objects.requireNonNull(sealTarget, "sealTarget"); + if (!targetMeta.equals(committedTarget.getMeta())) { + throw new ArchivePersistenceException( + "AccountAsset recorder seal target meta mismatch"); + } + List entries = new ArrayList<>(); + for (AccountEvents account : accounts.values()) { + if (account.rawAccountPostValue == null) { + throw new ArchivePersistenceException( + "AccountAsset recorder contains incomplete account"); + } + entries.add(new Entry(account.accountPhysicalKey, account.rawAccountPostValue, + account.canonicalAccountPostValue, new ArrayList<>(account.assets.values()))); + } + AccountAssetForwardMutationManifest manifest = + new AccountAssetForwardMutationManifest(committedTarget, entries); + sealed = true; + clearPayload(); + return manifest; + } + + synchronized void discard() { + requireOpen(); + sealed = true; + clearPayload(); + } + + synchronized boolean isPayloadReleased() { + return accounts.isEmpty() + && accountCount == 0 + && assetMutationCount == 0 + && totalPayloadBytes == 0; + } + + synchronized void reserveView(BlockChangeView view) { + requireOpen(); + long additionalBytes = 0; + long remaining = limits.getMaxTotalPayloadBytes() - totalPayloadBytes; + for (BlockChangeView.DatabaseChanges database : view.getDatabases()) { + for (BlockChangeView.Change change : database.getChanges()) { + byte[] key = change.getKey(); + requireKeyLength(key.length); + long entryBytes = key.length + valueLength(change.getPostValue()); + if (entryBytes > remaining - additionalBytes) { + throw new ArchivePersistenceException( + "Block forward mutation payload exceeds total limit"); + } + additionalBytes += entryBytes; + } + } + reserve(false, false, additionalBytes); + } + + private void recordAsset(BlockSnapshotMeta eventMeta, byte[] accountPhysicalKey, + byte[] assetPhysicalKey, PostValue postValue) { + requireOpenMeta(eventMeta); + Key accountKey = new Key(accountPhysicalKey); + byte[] assetKey = Arrays.copyOf(Objects.requireNonNull(assetPhysicalKey, + "assetPhysicalKey"), assetPhysicalKey.length); + requireKeyLength(accountKey.value.length); + requireKeyLength(assetKey.length); + if (!strictlyExtends(accountKey.value, assetKey)) { + throw new ArchivePersistenceException( + "AccountAsset physical key does not belong to account"); + } + AccountEvents current = accounts.get(accountKey); + Key mutationKey = new Key(assetKey); + if (current != null && current.assets.containsKey(mutationKey)) { + throw new ArchivePersistenceException("Duplicate AccountAsset asset event"); + } + long valueBytes = valueLength(postValue); + AccountEvents account = current == null ? new AccountEvents(accountKey.value) : current; + AssetMutation mutation = new AssetMutation(assetKey, postValue); + reserve(current == null, true, + (current == null ? accountKey.value.length : 0L) + assetKey.length + valueBytes); + if (current == null) { + accounts.put(accountKey, account); + } + account.assets.put(mutationKey, mutation); + } + + private long valueLength(PostValue value) { + long length = value.isPresent() ? value.getValue().length : 0L; + if (length > limits.getMaxValueBytes()) { + throw new ArchivePersistenceException("Block forward mutation value exceeds limit"); + } + return length; + } + + private void requireKeyLength(int length) { + if (length > limits.getMaxKeyBytes()) { + throw new ArchivePersistenceException("Block forward mutation key exceeds limit"); + } + } + + private void reserve(boolean newAccount, boolean newAsset, long additionalBytes) { + if (newAccount && accountCount >= limits.getMaxAccounts()) { + throw new ArchivePersistenceException("Block forward mutation account count exceeds limit"); + } + if (newAsset && assetMutationCount >= limits.getMaxAssetMutations()) { + throw new ArchivePersistenceException("Block forward mutation asset count exceeds limit"); + } + long remaining = limits.getMaxTotalPayloadBytes() - totalPayloadBytes; + if (additionalBytes < 0 || additionalBytes > remaining) { + throw new ArchivePersistenceException("Block forward mutation payload exceeds total limit"); + } + if (newAccount) { + accountCount++; + } + if (newAsset) { + assetMutationCount++; + } + totalPayloadBytes += additionalBytes; + } + + private void requireOpenMeta(BlockSnapshotMeta eventMeta) { + requireOpen(); + if (!targetMeta.equals(Objects.requireNonNull(eventMeta, "eventMeta"))) { + throw new ArchivePersistenceException("AccountAsset recorder event meta mismatch"); + } + } + + private void requireOpen() { + if (sealed) { + throw new ArchivePersistenceException("AccountAsset recorder is already sealed"); + } + } + + private void clearPayload() { + accounts.clear(); + accountCount = 0; + assetMutationCount = 0; + totalPayloadBytes = 0; + } + + private static boolean strictlyExtends(byte[] prefix, byte[] value) { + if (value.length <= prefix.length) { + return false; + } + for (int i = 0; i < prefix.length; i++) { + if (prefix[i] != value[i]) { + return false; + } + } + return true; + } + + private static final class AccountEvents { + private final byte[] accountPhysicalKey; + private final TreeMap assets = new TreeMap<>(); + private PostValue rawAccountPostValue; + private PostValue canonicalAccountPostValue; + + private AccountEvents(byte[] accountPhysicalKey) { + this.accountPhysicalKey = Arrays.copyOf( + Objects.requireNonNull(accountPhysicalKey, "accountPhysicalKey"), + accountPhysicalKey.length); + } + } + + private static final class Key implements Comparable { + private final byte[] value; + + private Key(byte[] value) { + this.value = Arrays.copyOf(Objects.requireNonNull(value, "key"), value.length); + } + + @Override + public int compareTo(Key other) { + return BlockReverseDiff.compareUnsigned(value, other.value); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardProjector.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardProjector.java new file mode 100644 index 00000000000..096b37ea10c --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardProjector.java @@ -0,0 +1,67 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.BlockChangeView.PostValue; + +/** Explicit no-scan contract for canonical account and physical account-asset post mutations. */ +@FunctionalInterface +public interface AccountAssetForwardProjector { + + /** Opens one target-bound projection pass and declares its exact changed-account keys. */ + default void begin(HistoryCommitMarker target, List changedAccountPhysicalKeys) { + } + + Projection project(byte[] accountPhysicalKey, PostValue rawAccountPostValue); + + /** Finalizes one projection pass after every declared account has been consumed. */ + default void complete() { + } + + /** One canonical account post value plus exact physical account-asset post mutations. */ + final class Projection { + private final PostValue accountPostValue; + private final List assetMutations; + + public Projection(PostValue accountPostValue, List assetMutations) { + this.accountPostValue = Objects.requireNonNull(accountPostValue, "accountPostValue"); + List copy = new ArrayList<>( + Objects.requireNonNull(assetMutations, "assetMutations")); + if (copy.contains(null)) { + throw new IllegalArgumentException("AccountAsset projection contains null mutation"); + } + this.assetMutations = Collections.unmodifiableList(copy); + } + + PostValue getAccountPostValue() { + return accountPostValue; + } + + List getAssetMutations() { + return assetMutations; + } + } + + /** Exact physical account-asset key and its present/absent post state. */ + final class AssetMutation { + private final byte[] physicalRawKey; + private final PostValue postValue; + + public AssetMutation(byte[] physicalRawKey, PostValue postValue) { + this.physicalRawKey = Arrays.copyOf( + Objects.requireNonNull(physicalRawKey, "physicalRawKey"), physicalRawKey.length); + this.postValue = Objects.requireNonNull(postValue, "postValue"); + } + + byte[] getPhysicalRawKey() { + return Arrays.copyOf(physicalRawKey, physicalRawKey.length); + } + + PostValue getPostValue() { + return postValue; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java new file mode 100644 index 00000000000..7d38550bb6b --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java @@ -0,0 +1,105 @@ +package org.tron.core.db2.archive; + +import java.util.Objects; +import org.tron.core.db2.archive.BlockChangeView.PostValue; + +/** One-shot owner of a block's explicit AccountAsset events, post-state view, and output batch. */ +public final class ArchiveBlockForwardMutationCapture { + + private final BlockSnapshotMeta targetMeta; + private final AccountAssetForwardMutationRecorder accountAssetRecorder; + private BlockChangeView view; + private State state = State.OPEN; + + public ArchiveBlockForwardMutationCapture(BlockSnapshotMeta targetMeta, + ArchiveBlockForwardMutationLimits limits) { + this.targetMeta = Objects.requireNonNull(targetMeta, "targetMeta"); + accountAssetRecorder = new AccountAssetForwardMutationRecorder(targetMeta, + Objects.requireNonNull(limits, "limits")); + } + + public synchronized void recordAccount(BlockSnapshotMeta eventMeta, + byte[] accountPhysicalKey, PostValue rawAccountPostValue, + PostValue canonicalAccountPostValue) { + requireOpen(); + accountAssetRecorder.recordAccount(eventMeta, accountPhysicalKey, rawAccountPostValue, + canonicalAccountPostValue); + } + + public synchronized void recordAssetPut(BlockSnapshotMeta eventMeta, + byte[] accountPhysicalKey, byte[] assetPhysicalKey, byte[] value) { + requireOpen(); + accountAssetRecorder.recordAssetPut(eventMeta, accountPhysicalKey, assetPhysicalKey, value); + } + + public synchronized void recordAssetDelete(BlockSnapshotMeta eventMeta, + byte[] accountPhysicalKey, byte[] assetPhysicalKey) { + requireOpen(); + accountAssetRecorder.recordAssetDelete(eventMeta, accountPhysicalKey, assetPhysicalKey); + } + + public synchronized void attach(BlockChangeView blockChangeView) { + requireOpen(); + BlockChangeView attached = Objects.requireNonNull(blockChangeView, "blockChangeView"); + if (!targetMeta.equals(attached.getMeta())) { + throw new ArchivePersistenceException("Block forward capture view meta mismatch"); + } + if (view != null) { + throw new ArchivePersistenceException("Block forward capture view is already attached"); + } + accountAssetRecorder.reserveView(attached); + view = attached; + } + + public synchronized ArchiveParticipantMutationBatch seal(HistoryCommitMarker committedTarget) { + requireOpen(); + if (view == null) { + throw new ArchivePersistenceException("Block forward capture view is missing"); + } + HistoryCommitMarker target = Objects.requireNonNull(committedTarget, "committedTarget"); + if (!targetMeta.equals(target.getMeta())) { + throw new ArchivePersistenceException("Block forward capture marker meta mismatch"); + } + AccountAssetForwardMutationManifest manifest = accountAssetRecorder.seal(target); + try { + ArchiveParticipantMutationBatch batch = + new ArchiveParticipantMutationBatchCollector(manifest).collect(target, view); + state = State.SEALED; + return batch; + } catch (RuntimeException e) { + state = State.FAILED; + throw e; + } finally { + view = null; + } + } + + public synchronized void abort() { + requireOpen(); + accountAssetRecorder.discard(); + view = null; + state = State.ABORTED; + } + + synchronized boolean hasAttachedView() { + return view != null; + } + + synchronized boolean isPayloadReleased() { + return accountAssetRecorder.isPayloadReleased(); + } + + private void requireOpen() { + if (state != State.OPEN) { + throw new ArchivePersistenceException( + "Block forward capture is terminal: " + state.name()); + } + } + + private enum State { + OPEN, + SEALED, + FAILED, + ABORTED + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationLimits.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationLimits.java new file mode 100644 index 00000000000..d1e12fde931 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationLimits.java @@ -0,0 +1,44 @@ +package org.tron.core.db2.archive; + +/** Explicit standalone bounds for one block's complete forward mutation capture. */ +public final class ArchiveBlockForwardMutationLimits { + + private final int maxAccounts; + private final int maxAssetMutations; + private final int maxKeyBytes; + private final int maxValueBytes; + private final long maxTotalPayloadBytes; + + public ArchiveBlockForwardMutationLimits(int maxAccounts, int maxAssetMutations, + int maxKeyBytes, int maxValueBytes, long maxTotalPayloadBytes) { + if (maxAccounts < 0 || maxAssetMutations < 0 || maxKeyBytes < 0 + || maxValueBytes < 0 || maxTotalPayloadBytes < 0) { + throw new IllegalArgumentException("Block forward mutation limits must not be negative"); + } + this.maxAccounts = maxAccounts; + this.maxAssetMutations = maxAssetMutations; + this.maxKeyBytes = maxKeyBytes; + this.maxValueBytes = maxValueBytes; + this.maxTotalPayloadBytes = maxTotalPayloadBytes; + } + + int getMaxAccounts() { + return maxAccounts; + } + + int getMaxAssetMutations() { + return maxAssetMutations; + } + + int getMaxKeyBytes() { + return maxKeyBytes; + } + + int getMaxValueBytes() { + return maxValueBytes; + } + + long getMaxTotalPayloadBytes() { + return maxTotalPayloadBytes; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java new file mode 100644 index 00000000000..25014f73bae --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java @@ -0,0 +1,112 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; +import org.tron.core.db2.archive.AccountAssetForwardProjector.Projection; +import org.tron.core.db2.archive.ArchiveParticipantMutationBatch.Mutation; +import org.tron.core.db2.archive.BlockChangeView.Change; +import org.tron.core.db2.archive.BlockChangeView.DatabaseChanges; +import org.tron.core.db2.archive.BlockChangeView.PostValue; + +/** Converts one immutable block post-state view into a target-bound physical mutation batch. */ +public final class ArchiveParticipantMutationBatchCollector { + + private final AccountAssetForwardProjector accountAssetProjector; + private final List participants; + + public ArchiveParticipantMutationBatchCollector() { + this(null); + } + + public ArchiveParticipantMutationBatchCollector( + AccountAssetForwardProjector accountAssetProjector) { + this.accountAssetProjector = accountAssetProjector; + List expected = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(expected); + participants = Collections.unmodifiableList(expected); + } + + public ArchiveParticipantMutationBatch collect(HistoryCommitMarker committedTarget, + BlockChangeView view) { + HistoryCommitMarker target = Objects.requireNonNull(committedTarget, "committedTarget"); + BlockChangeView input = Objects.requireNonNull(view, "view"); + if (!target.getMeta().equals(input.getMeta())) { + throw new ArchivePersistenceException("Block mutation view target identity mismatch"); + } + requireExactCoverage(target, input); + List changedAccountKeys = changedAccountKeys(input); + if (!changedAccountKeys.isEmpty() && accountAssetProjector == null) { + throw new ArchivePersistenceException( + "Account mutation requires an explicit AccountAsset forward projector"); + } + if (accountAssetProjector != null) { + accountAssetProjector.begin(target, changedAccountKeys); + } + List mutations = new ArrayList<>(); + for (DatabaseChanges database : input.getDatabases()) { + for (Change change : database.getChanges()) { + if (AccountAssetArchiveProjector.ACCOUNT_DB.equals(database.getDbName())) { + collectAccount(change, mutations); + } else { + mutations.add(toMutation(database.getDbName(), change.getKey(), + change.getPostValue())); + } + } + } + if (accountAssetProjector != null) { + accountAssetProjector.complete(); + } + return new ArchiveParticipantMutationBatch(target, mutations); + } + + private void collectAccount(Change change, List mutations) { + if (accountAssetProjector == null) { + throw new ArchivePersistenceException( + "Account mutation requires an explicit AccountAsset forward projector"); + } + byte[] accountKey = change.getKey(); + Projection projection = accountAssetProjector.project(accountKey, change.getPostValue()); + if (projection == null) { + throw new ArchivePersistenceException("AccountAsset forward projection is missing"); + } + mutations.add(toMutation(AccountAssetArchiveProjector.ACCOUNT_DB, accountKey, + projection.getAccountPostValue())); + for (AssetMutation asset : projection.getAssetMutations()) { + mutations.add(toMutation(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + asset.getPhysicalRawKey(), asset.getPostValue())); + } + } + + private void requireExactCoverage(HistoryCommitMarker target, BlockChangeView view) { + List actual = new ArrayList<>(); + for (DatabaseChanges database : view.getDatabases()) { + actual.add(database.getDbName()); + } + Collections.sort(actual); + if (!actual.equals(participants) || !target.getDatabases().equals(participants)) { + throw new ArchivePersistenceException( + "Block mutation view does not cover the exact VERSIONED_STATE set"); + } + } + + private static List changedAccountKeys(BlockChangeView view) { + List keys = new ArrayList<>(); + for (DatabaseChanges database : view.getDatabases()) { + if (AccountAssetArchiveProjector.ACCOUNT_DB.equals(database.getDbName())) { + for (Change change : database.getChanges()) { + keys.add(change.getKey()); + } + } + } + return keys; + } + + private static Mutation toMutation(String dbName, byte[] key, PostValue postValue) { + return postValue.isPresent() + ? Mutation.put(dbName, key, postValue.getValue()) + : Mutation.delete(dbName, key); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java new file mode 100644 index 00000000000..c9cb85ef17f --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java @@ -0,0 +1,869 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.tron.common.BaseMethodTest; +import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.AccountAssetForwardMutationManifest.Entry; +import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; +import org.tron.core.db2.archive.AccountAssetForwardProjector.Projection; +import org.tron.core.db2.common.DB; +import org.tron.core.db2.common.Flusher; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.core.SnapshotRoot; + +public class ArchiveParticipantMutationBatchCollectorTest extends BaseMethodTest { + + @Test + public void collectsExactPostPutDeleteAndEmptyDeterministically() { + BlockSnapshotMeta meta = meta(1); + HistoryCommitMarker marker = marker(meta, participants()); + byte[] deleted = bytes(2, 2); + try (Fixture first = new Fixture(participants()); + Fixture second = new Fixture(participants())) { + first.rootPut("storage-row", deleted, bytes(1, 8)); + second.rootPut("storage-row", deleted, bytes(1, 8)); + BlockChangeView firstView = first.capture(meta, databases -> { + databases.get("proposal").put(bytes(2, 3), bytes(1, 3)); + databases.get("abi").put(bytes(2, 1), new byte[0]); + databases.get("storage-row").delete(deleted); + }); + BlockChangeView secondView = second.capture(meta, databases -> { + databases.get("storage-row").delete(deleted); + databases.get("abi").put(bytes(2, 1), new byte[0]); + databases.get("proposal").put(bytes(2, 3), bytes(1, 3)); + }); + ArchiveParticipantMutationBatchCollector collector = + new ArchiveParticipantMutationBatchCollector(); + ArchiveTargetMutationPlan firstPlan = new ArchiveTargetMutationPlanBuilder().build(marker, + collector.collect(marker, firstView)); + ArchiveTargetMutationPlan secondPlan = new ArchiveTargetMutationPlanBuilder().build(marker, + collector.collect(marker, secondView)); + + assertArrayEquals(new byte[0], firstPlan.getMutations("abi").get(0).getValue()); + assertNull(firstPlan.getMutations("storage-row").get(0).getValue()); + assertArrayEquals(bytes(1, 3), + firstPlan.getMutations("proposal").get(0).getValue()); + assertArrayEquals(firstPlan.digest(), secondPlan.digest()); + } + } + + @Test + public void accountMutationRequiresExplicitNoScanForwardProjection() { + BlockSnapshotMeta meta = meta(1); + HistoryCommitMarker marker = marker(meta, participants()); + byte[] accountKey = bytes(3, 1); + byte[] rawAccount = bytes(3, 2); + byte[] canonicalAccount = bytes(3, 3); + byte[] assetPut = bytes(3, 4); + byte[] assetDelete = bytes(3, 5); + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").put(accountKey, rawAccount)); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveParticipantMutationBatchCollector().collect(marker, view)); + AccountAssetForwardProjector projector = (key, post) -> { + assertArrayEquals(accountKey, key); + assertArrayEquals(rawAccount, post.getValue()); + return new Projection(BlockChangeView.PostValue.present(canonicalAccount), Arrays.asList( + new AssetMutation(assetDelete, BlockChangeView.PostValue.absent()), + new AssetMutation(assetPut, BlockChangeView.PostValue.present(new byte[0])))); + }; + ArchiveParticipantMutationBatch batch = + new ArchiveParticipantMutationBatchCollector(projector).collect(marker, view); + ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, batch); + + assertArrayEquals(canonicalAccount, + plan.getMutations("account").get(0).getValue()); + assertArrayEquals(assetPut, + plan.getMutations("account-asset").get(0).getKey()); + assertEquals(0, plan.getMutations("account-asset").get(0).getValue().length); + assertArrayEquals(assetDelete, + plan.getMutations("account-asset").get(1).getKey()); + assertNull(plan.getMutations("account-asset").get(1).getValue()); + } + } + + @Test + public void rejectsViewIdentityCoverageAndMissingProjectionResult() { + BlockSnapshotMeta meta = meta(1); + HistoryCommitMarker marker = marker(meta, participants()); + try (Fixture exact = new Fixture(participants())) { + BlockChangeView view = exact.capture(meta, + databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveParticipantMutationBatchCollector().collect( + marker(meta(2), participants()), view)); + } + + try (Fixture incomplete = new Fixture(Collections.singletonList("abi"))) { + BlockChangeView view = incomplete.capture(meta, + databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveParticipantMutationBatchCollector().collect(marker, view)); + } + + try (Fixture account = new Fixture(participants())) { + BlockChangeView view = account.capture(meta, + databases -> databases.get("account").put(bytes(1, 1), bytes(1, 2))); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveParticipantMutationBatchCollector((key, post) -> null) + .collect(marker, view)); + } + } + + @Test + public void manifestCollectsAccountCreateUpdateDeleteAndExactAssetStates() { + BlockSnapshotMeta meta = meta(3); + HistoryCommitMarker marker = marker(meta, participants()); + byte[] createKey = bytes(2, 1); + byte[] updateKey = bytes(2, 2); + byte[] deleteKey = bytes(2, 3); + byte[] rawCreate = bytes(3, 11); + byte[] rawUpdate = bytes(3, 12); + byte[] canonicalCreate = bytes(3, 21); + byte[] canonicalUpdate = bytes(3, 22); + byte[] createAsset = assetKey(createKey, 1); + byte[] updateAsset = assetKey(updateKey, 1); + byte[] updateDeletedAsset = assetKey(updateKey, 2); + byte[] deleteAsset = assetKey(deleteKey, 1); + + try (Fixture fixture = new Fixture(participants())) { + fixture.rootPut("account", updateKey, bytes(3, 31)); + fixture.rootPut("account", deleteKey, bytes(3, 32)); + BlockChangeView view = fixture.capture(meta, databases -> { + databases.get("account").put(createKey, rawCreate); + databases.get("account").put(updateKey, rawUpdate); + databases.get("account").delete(deleteKey); + }); + AccountAssetForwardMutationManifest manifest = + new AccountAssetForwardMutationManifest(marker, Arrays.asList( + entry(createKey, rawCreate, canonicalCreate, + new AssetMutation(createAsset, + BlockChangeView.PostValue.present(new byte[0]))), + entry(updateKey, rawUpdate, canonicalUpdate, + new AssetMutation(updateAsset, + BlockChangeView.PostValue.present(bytes(2, 41))), + new AssetMutation(updateDeletedAsset, BlockChangeView.PostValue.absent())), + new Entry(deleteKey, BlockChangeView.PostValue.absent(), + BlockChangeView.PostValue.absent(), Collections.singletonList( + new AssetMutation(deleteAsset, BlockChangeView.PostValue.absent()))))); + + ArchiveParticipantMutationBatch batch = + new ArchiveParticipantMutationBatchCollector(manifest).collect(marker, view); + ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, batch); + + assertArrayEquals(canonicalCreate, plan.getMutations("account").get(0).getValue()); + assertArrayEquals(canonicalUpdate, plan.getMutations("account").get(1).getValue()); + assertNull(plan.getMutations("account").get(2).getValue()); + assertEquals(0, plan.getMutations("account-asset").get(0).getValue().length); + assertArrayEquals(bytes(2, 41), + plan.getMutations("account-asset").get(1).getValue()); + assertNull(plan.getMutations("account-asset").get(2).getValue()); + assertNull(plan.getMutations("account-asset").get(3).getValue()); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveParticipantMutationBatchCollector(manifest).collect(marker, view)); + } + } + + @Test + public void manifestRejectsMissingExtraTargetAndRawValueMismatch() { + BlockSnapshotMeta meta = meta(4); + HistoryCommitMarker marker = marker(meta, participants()); + byte[] accountKey = bytes(2, 1); + byte[] rawAccount = bytes(3, 2); + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").put(accountKey, rawAccount)); + AccountAssetForwardMutationManifest missing = + new AccountAssetForwardMutationManifest(marker, Collections.emptyList()); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveParticipantMutationBatchCollector(missing).collect(marker, view)); + + AccountAssetForwardMutationManifest extra = new AccountAssetForwardMutationManifest(marker, + Arrays.asList(entry(accountKey, rawAccount, rawAccount), + entry(bytes(2, 9), bytes(3, 9), bytes(3, 9)))); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveParticipantMutationBatchCollector(extra).collect(marker, view)); + + AccountAssetForwardMutationManifest wrongRaw = + new AccountAssetForwardMutationManifest(marker, + Collections.singletonList(entry(accountKey, bytes(3, 8), rawAccount))); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveParticipantMutationBatchCollector(wrongRaw).collect(marker, view)); + } + + BlockSnapshotMeta otherMeta = meta(5); + HistoryCommitMarker otherMarker = marker(otherMeta, participants()); + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView otherView = fixture.capture(otherMeta, + databases -> databases.get("account").put(accountKey, rawAccount)); + AccountAssetForwardMutationManifest wrongTarget = + new AccountAssetForwardMutationManifest(marker, + Collections.singletonList(entry(accountKey, rawAccount, rawAccount))); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveParticipantMutationBatchCollector(wrongTarget) + .collect(otherMarker, otherView)); + } + } + + @Test + public void manifestRejectsDuplicateCrossAccountAndUnusedEntries() { + HistoryCommitMarker marker = marker(meta(6), participants()); + byte[] accountKey = bytes(2, 1); + byte[] rawAccount = bytes(3, 2); + Entry entry = entry(accountKey, rawAccount, rawAccount); + assertThrows(IllegalArgumentException.class, + () -> new AccountAssetForwardMutationManifest(marker, Arrays.asList(entry, entry))); + assertThrows(IllegalArgumentException.class, + () -> new AccountAssetForwardMutationManifest(marker, + Collections.singletonList(null))); + assertThrows(IllegalArgumentException.class, + () -> entry(accountKey, rawAccount, rawAccount, + new AssetMutation(assetKey(accountKey, 1), BlockChangeView.PostValue.absent()), + new AssetMutation(assetKey(accountKey, 1), BlockChangeView.PostValue.absent()))); + assertThrows(IllegalArgumentException.class, + () -> entry(accountKey, rawAccount, rawAccount, + new AssetMutation(bytes(3, 7), BlockChangeView.PostValue.absent()))); + + AccountAssetForwardMutationManifest singleUse = + new AccountAssetForwardMutationManifest(marker, Collections.singletonList(entry)); + singleUse.begin(marker, Collections.singletonList(accountKey)); + singleUse.project(accountKey, BlockChangeView.PostValue.present(rawAccount)); + assertThrows(ArchivePersistenceException.class, + () -> singleUse.project(accountKey, BlockChangeView.PostValue.present(rawAccount))); + singleUse.complete(); + + AccountAssetForwardMutationManifest unused = + new AccountAssetForwardMutationManifest(marker, Collections.singletonList(entry)); + unused.begin(marker, Collections.singletonList(accountKey)); + assertThrows(ArchivePersistenceException.class, unused::complete); + } + + @Test + public void recorderSealsUnorderedEventsIntoExactAccountAndAssetMutations() { + BlockSnapshotMeta meta = meta(7); + byte[] updateKey = bytes(2, 1); + byte[] deleteKey = bytes(2, 2); + byte[] rawUpdate = bytes(3, 3); + byte[] canonicalUpdate = bytes(3, 4); + byte[] emptyAsset = assetKey(updateKey, 1); + byte[] deletedAsset = assetKey(updateKey, 2); + byte[] removedAccountAsset = assetKey(deleteKey, 1); + AccountAssetForwardMutationRecorder recorder = + new AccountAssetForwardMutationRecorder(meta, limits()); + + recorder.recordAssetDelete(meta, deleteKey, removedAccountAsset); + recorder.recordAssetPut(meta, updateKey, emptyAsset, new byte[0]); + recorder.recordAccount(meta, deleteKey, BlockChangeView.PostValue.absent(), + BlockChangeView.PostValue.absent()); + recorder.recordAssetDelete(meta, updateKey, deletedAsset); + recorder.recordAccount(meta, updateKey, BlockChangeView.PostValue.present(rawUpdate), + BlockChangeView.PostValue.present(canonicalUpdate)); + HistoryCommitMarker marker = marker(meta, participants()); + AccountAssetForwardMutationManifest manifest = recorder.seal(marker); + + try (Fixture fixture = new Fixture(participants())) { + fixture.rootPut("account", updateKey, bytes(3, 8)); + fixture.rootPut("account", deleteKey, bytes(3, 9)); + BlockChangeView view = fixture.capture(meta, databases -> { + databases.get("account").put(updateKey, rawUpdate); + databases.get("account").delete(deleteKey); + }); + ArchiveParticipantMutationBatch batch = + new ArchiveParticipantMutationBatchCollector(manifest).collect(marker, view); + ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, batch); + + assertArrayEquals(canonicalUpdate, plan.getMutations("account").get(0).getValue()); + assertNull(plan.getMutations("account").get(1).getValue()); + assertEquals(0, plan.getMutations("account-asset").get(0).getValue().length); + assertNull(plan.getMutations("account-asset").get(1).getValue()); + assertNull(plan.getMutations("account-asset").get(2).getValue()); + } + } + + @Test + public void recorderCanonicalizesDifferentEventOrders() { + BlockSnapshotMeta meta = meta(8); + HistoryCommitMarker marker = marker(meta, participants()); + byte[] accountKey = bytes(2, 1); + byte[] rawAccount = bytes(3, 2); + byte[] canonicalAccount = bytes(3, 3); + byte[] firstAsset = assetKey(accountKey, 1); + byte[] secondAsset = assetKey(accountKey, 2); + AccountAssetForwardMutationRecorder first = + new AccountAssetForwardMutationRecorder(meta, limits()); + AccountAssetForwardMutationRecorder second = + new AccountAssetForwardMutationRecorder(meta, limits()); + + first.recordAssetDelete(meta, accountKey, secondAsset); + first.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(canonicalAccount)); + first.recordAssetPut(meta, accountKey, firstAsset, bytes(2, 4)); + second.recordAssetPut(meta, accountKey, firstAsset, bytes(2, 4)); + second.recordAssetDelete(meta, accountKey, secondAsset); + second.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(canonicalAccount)); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").put(accountKey, rawAccount)); + ArchiveTargetMutationPlan firstPlan = new ArchiveTargetMutationPlanBuilder().build(marker, + new ArchiveParticipantMutationBatchCollector(first.seal(marker)).collect(marker, view)); + ArchiveTargetMutationPlan secondPlan = new ArchiveTargetMutationPlanBuilder().build(marker, + new ArchiveParticipantMutationBatchCollector(second.seal(marker)).collect(marker, view)); + assertArrayEquals(firstPlan.digest(), secondPlan.digest()); + } + } + + @Test + public void recorderRejectsTargetDuplicatesIncompleteAndPostSealWrites() { + BlockSnapshotMeta meta = meta(9); + BlockSnapshotMeta otherMeta = meta(10); + HistoryCommitMarker marker = marker(meta, participants()); + HistoryCommitMarker otherMarker = marker(otherMeta, participants()); + byte[] accountKey = bytes(2, 1); + byte[] rawAccount = bytes(3, 2); + byte[] assetKey = assetKey(accountKey, 1); + + AccountAssetForwardMutationRecorder wrongTarget = + new AccountAssetForwardMutationRecorder(meta, limits()); + assertThrows(ArchivePersistenceException.class, + () -> wrongTarget.recordAccount(otherMeta, accountKey, + BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(rawAccount))); + assertThrows(ArchivePersistenceException.class, () -> wrongTarget.seal(otherMarker)); + + AccountAssetForwardMutationRecorder duplicates = + new AccountAssetForwardMutationRecorder(meta, limits()); + duplicates.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(rawAccount)); + assertThrows(ArchivePersistenceException.class, + () -> duplicates.recordAccount(meta, accountKey, + BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(rawAccount))); + duplicates.recordAssetPut(meta, accountKey, assetKey, bytes(1, 3)); + assertThrows(ArchivePersistenceException.class, + () -> duplicates.recordAssetDelete(meta, accountKey, assetKey)); + assertThrows(ArchivePersistenceException.class, + () -> duplicates.recordAssetPut(meta, accountKey, bytes(3, 7), bytes(1, 3))); + + AccountAssetForwardMutationRecorder incomplete = + new AccountAssetForwardMutationRecorder(meta, limits()); + incomplete.recordAssetDelete(meta, accountKey, assetKey); + assertThrows(ArchivePersistenceException.class, () -> incomplete.seal(marker)); + + AccountAssetForwardMutationRecorder sealed = + new AccountAssetForwardMutationRecorder(meta, limits()); + sealed.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(rawAccount)); + sealed.seal(marker); + assertThrows(ArchivePersistenceException.class, () -> sealed.seal(marker)); + assertThrows(ArchivePersistenceException.class, + () -> sealed.recordAssetDelete(meta, accountKey, assetKey)); + } + + @Test + public void recorderDefensivelyTransfersPayloadBeforeCommittedMarkerExists() { + BlockSnapshotMeta meta = meta(11); + byte[] accountKey = bytes(2, 1); + byte[] rawAccount = bytes(3, 2); + byte[] canonicalAccount = bytes(3, 3); + byte[] assetKey = assetKey(accountKey, 1); + byte[] assetValue = bytes(2, 4); + byte[] expectedAccountKey = Arrays.copyOf(accountKey, accountKey.length); + byte[] expectedRaw = Arrays.copyOf(rawAccount, rawAccount.length); + byte[] expectedCanonical = Arrays.copyOf(canonicalAccount, canonicalAccount.length); + byte[] expectedAssetKey = Arrays.copyOf(assetKey, assetKey.length); + byte[] expectedAssetValue = Arrays.copyOf(assetValue, assetValue.length); + AccountAssetForwardMutationRecorder recorder = + new AccountAssetForwardMutationRecorder(meta, limits()); + + recorder.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(canonicalAccount)); + recorder.recordAssetPut(meta, accountKey, assetKey, assetValue); + Arrays.fill(accountKey, (byte) 9); + Arrays.fill(rawAccount, (byte) 9); + Arrays.fill(canonicalAccount, (byte) 9); + Arrays.fill(assetKey, (byte) 9); + Arrays.fill(assetValue, (byte) 9); + + HistoryCommitMarker marker = marker(meta, participants()); + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").put(expectedAccountKey, expectedRaw)); + ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, + new ArchiveParticipantMutationBatchCollector(recorder.seal(marker)) + .collect(marker, view)); + assertArrayEquals(expectedCanonical, plan.getMutations("account").get(0).getValue()); + assertArrayEquals(expectedAssetKey, + plan.getMutations("account-asset").get(0).getKey()); + assertArrayEquals(expectedAssetValue, + plan.getMutations("account-asset").get(0).getValue()); + } + } + + @Test + public void blockCaptureOwnsViewRecorderAndBatchAsOneShot() { + BlockSnapshotMeta meta = meta(12); + HistoryCommitMarker marker = marker(meta, participants()); + byte[] accountKey = bytes(2, 1); + byte[] rawAccount = bytes(3, 2); + byte[] canonicalAccount = bytes(3, 3); + byte[] assetKey = assetKey(accountKey, 1); + ArchiveBlockForwardMutationCapture capture = + new ArchiveBlockForwardMutationCapture(meta, limits()); + capture.recordAssetPut(meta, accountKey, assetKey, new byte[0]); + capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(canonicalAccount)); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").put(accountKey, rawAccount)); + capture.attach(view); + assertTrue(capture.hasAttachedView()); + assertFalse(capture.isPayloadReleased()); + ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, + capture.seal(marker)); + assertArrayEquals(canonicalAccount, plan.getMutations("account").get(0).getValue()); + assertEquals(0, plan.getMutations("account-asset").get(0).getValue().length); + assertFalse(capture.hasAttachedView()); + assertTrue(capture.isPayloadReleased()); + assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); + assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); + assertThrows(ArchivePersistenceException.class, + () -> capture.recordAssetDelete(meta, accountKey, assetKey)); + assertThrows(ArchivePersistenceException.class, capture::abort); + } + } + + @Test + public void blockCapturePreconditionFailuresRemainRetryableBeforeManifestConsumption() { + BlockSnapshotMeta meta = meta(13); + BlockSnapshotMeta otherMeta = meta(14); + HistoryCommitMarker marker = marker(meta, participants()); + HistoryCommitMarker otherMarker = marker(otherMeta, participants()); + ArchiveBlockForwardMutationCapture capture = + new ArchiveBlockForwardMutationCapture(meta, limits()); + assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); + + try (Fixture exact = new Fixture(participants()); + Fixture other = new Fixture(participants())) { + BlockChangeView wrongView = other.capture(otherMeta, + databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + assertThrows(ArchivePersistenceException.class, () -> capture.attach(wrongView)); + BlockChangeView view = exact.capture(meta, + databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + capture.attach(view); + assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); + assertThrows(ArchivePersistenceException.class, () -> capture.seal(otherMarker)); + ArchiveParticipantMutationBatch batch = capture.seal(marker); + assertEquals(meta.getEpoch(), batch.getTargetEpoch()); + } + } + + @Test + public void blockCaptureCoverageFailureConsumesOwnershipAndBecomesTerminal() { + BlockSnapshotMeta meta = meta(15); + HistoryCommitMarker marker = marker(meta, participants()); + byte[] accountKey = bytes(2, 1); + byte[] rawAccount = bytes(3, 2); + ArchiveBlockForwardMutationCapture capture = + new ArchiveBlockForwardMutationCapture(meta, limits()); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").put(accountKey, rawAccount)); + capture.attach(view); + assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); + assertFalse(capture.hasAttachedView()); + assertTrue(capture.isPayloadReleased()); + assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); + assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); + assertThrows(ArchivePersistenceException.class, + () -> capture.recordAccount(meta, accountKey, + BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(rawAccount))); + assertThrows(ArchivePersistenceException.class, capture::abort); + } + } + + @Test + public void blockCaptureAbortBeforeAttachReleasesPayloadAndRejectsEveryTerminalAction() { + BlockSnapshotMeta meta = meta(20); + HistoryCommitMarker marker = marker(meta, participants()); + byte[] accountKey = bytes(2, 1); + byte[] assetKey = assetKey(accountKey, 1); + ArchiveBlockForwardMutationCapture capture = + new ArchiveBlockForwardMutationCapture(meta, limits()); + capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(bytes(3, 2)), + BlockChangeView.PostValue.present(bytes(3, 3))); + capture.recordAssetPut(meta, accountKey, assetKey, bytes(3, 4)); + assertFalse(capture.hasAttachedView()); + assertFalse(capture.isPayloadReleased()); + + capture.abort(); + + assertFalse(capture.hasAttachedView()); + assertTrue(capture.isPayloadReleased()); + assertThrows(ArchivePersistenceException.class, capture::abort); + assertThrows(ArchivePersistenceException.class, + () -> capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), + BlockChangeView.PostValue.absent())); + assertThrows(ArchivePersistenceException.class, + () -> capture.recordAssetDelete(meta, accountKey, assetKey)); + assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); + } + } + + @Test + public void blockCaptureAbortAfterAttachReleasesViewAndPayload() { + BlockSnapshotMeta meta = meta(21); + HistoryCommitMarker marker = marker(meta, participants()); + byte[] accountKey = bytes(2, 1); + ArchiveBlockForwardMutationCapture capture = + new ArchiveBlockForwardMutationCapture(meta, limits()); + capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), + BlockChangeView.PostValue.absent()); + + try (Fixture fixture = new Fixture(participants())) { + fixture.rootPut("account", accountKey, bytes(1, 9)); + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").delete(accountKey)); + capture.attach(view); + assertTrue(capture.hasAttachedView()); + assertFalse(capture.isPayloadReleased()); + + capture.abort(); + + assertFalse(capture.hasAttachedView()); + assertTrue(capture.isPayloadReleased()); + assertThrows(ArchivePersistenceException.class, capture::abort); + assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); + assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); + } + } + + @Test + public void captureLimitsAcceptExactBoundaryWithDeleteAndPresentEmpty() { + BlockSnapshotMeta meta = meta(16); + HistoryCommitMarker marker = marker(meta, participants()); + byte[] accountKey = bytes(2, 1); + byte[] rawAccount = bytes(1, 2); + byte[] canonicalAccount = bytes(1, 3); + byte[] emptyAsset = assetKey(accountKey, 1); + byte[] deletedAsset = assetKey(accountKey, 2); + ArchiveBlockForwardMutationLimits exact = + new ArchiveBlockForwardMutationLimits(1, 2, 3, 1, 13); + ArchiveBlockForwardMutationCapture capture = + new ArchiveBlockForwardMutationCapture(meta, exact); + capture.recordAssetPut(meta, accountKey, emptyAsset, new byte[0]); + capture.recordAssetDelete(meta, accountKey, deletedAsset); + capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(canonicalAccount)); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").put(accountKey, rawAccount)); + capture.attach(view); + ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, + capture.seal(marker)); + assertEquals(0, plan.getMutations("account-asset").get(0).getValue().length); + assertNull(plan.getMutations("account-asset").get(1).getValue()); + } + } + + @Test + public void captureLimitsRejectEveryDimensionAndNegativeConfiguration() { + BlockSnapshotMeta meta = meta(17); + byte[] accountKey = bytes(2, 1); + byte[] assetKey = assetKey(accountKey, 1); + assertThrows(IllegalArgumentException.class, + () -> new ArchiveBlockForwardMutationLimits(-1, 1, 1, 1, 1)); + + AccountAssetForwardMutationRecorder accounts = new AccountAssetForwardMutationRecorder(meta, + new ArchiveBlockForwardMutationLimits(0, 1, 3, 1, 10)); + assertThrows(ArchivePersistenceException.class, + () -> accounts.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), + BlockChangeView.PostValue.absent())); + + AccountAssetForwardMutationRecorder assets = new AccountAssetForwardMutationRecorder(meta, + new ArchiveBlockForwardMutationLimits(1, 0, 3, 1, 10)); + assets.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), + BlockChangeView.PostValue.absent()); + assertThrows(ArchivePersistenceException.class, + () -> assets.recordAssetDelete(meta, accountKey, assetKey)); + + AccountAssetForwardMutationRecorder keys = new AccountAssetForwardMutationRecorder(meta, + new ArchiveBlockForwardMutationLimits(1, 1, 1, 1, 10)); + assertThrows(ArchivePersistenceException.class, + () -> keys.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), + BlockChangeView.PostValue.absent())); + + AccountAssetForwardMutationRecorder values = new AccountAssetForwardMutationRecorder(meta, + new ArchiveBlockForwardMutationLimits(1, 1, 3, 0, 10)); + assertThrows(ArchivePersistenceException.class, + () -> values.recordAccount(meta, accountKey, + BlockChangeView.PostValue.present(bytes(1, 2)), + BlockChangeView.PostValue.present(bytes(1, 3)))); + + AccountAssetForwardMutationRecorder total = new AccountAssetForwardMutationRecorder(meta, + new ArchiveBlockForwardMutationLimits(1, 1, 3, 1, 3)); + assertThrows(ArchivePersistenceException.class, + () -> total.recordAccount(meta, accountKey, + BlockChangeView.PostValue.present(bytes(1, 2)), + BlockChangeView.PostValue.present(bytes(1, 3)))); + } + + @Test + public void captureLimitRejectionAndDuplicatesDoNotConsumeReservation() { + BlockSnapshotMeta meta = meta(18); + HistoryCommitMarker marker = marker(meta, participants()); + byte[] accountKey = bytes(2, 1); + byte[] firstAsset = assetKey(accountKey, 1); + byte[] secondAsset = assetKey(accountKey, 2); + ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture(meta, + new ArchiveBlockForwardMutationLimits(1, 2, 3, 1, 10)); + + assertThrows(ArchivePersistenceException.class, + () -> capture.recordAccount(meta, accountKey, + BlockChangeView.PostValue.present(bytes(2, 2)), + BlockChangeView.PostValue.present(bytes(2, 3)))); + capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), + BlockChangeView.PostValue.absent()); + assertThrows(ArchivePersistenceException.class, + () -> capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), + BlockChangeView.PostValue.absent())); + capture.recordAssetDelete(meta, accountKey, firstAsset); + assertThrows(ArchivePersistenceException.class, + () -> capture.recordAssetDelete(meta, accountKey, firstAsset)); + capture.recordAssetDelete(meta, accountKey, secondAsset); + + try (Fixture fixture = new Fixture(participants())) { + fixture.rootPut("account", accountKey, bytes(1, 9)); + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").delete(accountKey)); + capture.attach(view); + ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, + capture.seal(marker)); + assertNull(plan.getMutations("account").get(0).getValue()); + assertEquals(2, plan.getMutations("account-asset").size()); + } + } + + @Test + public void captureLimitsReserveAttachedViewAtomicallyAndAllowRetry() { + BlockSnapshotMeta meta = meta(19); + HistoryCommitMarker marker = marker(meta, participants()); + ArchiveBlockForwardMutationCapture total = new ArchiveBlockForwardMutationCapture(meta, + new ArchiveBlockForwardMutationLimits(0, 0, 3, 3, 3)); + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView tooLarge = fixture.capture(meta, + databases -> databases.get("abi").put(bytes(2, 1), bytes(2, 2))); + assertThrows(ArchivePersistenceException.class, () -> total.attach(tooLarge)); + BlockChangeView exact = fixture.capture(meta, + databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + total.attach(exact); + assertEquals(meta.getEpoch(), total.seal(marker).getTargetEpoch()); + } + + ArchiveBlockForwardMutationCapture key = new ArchiveBlockForwardMutationCapture(meta, + new ArchiveBlockForwardMutationLimits(0, 0, 1, 2, 10)); + ArchiveBlockForwardMutationCapture value = new ArchiveBlockForwardMutationCapture(meta, + new ArchiveBlockForwardMutationLimits(0, 0, 2, 1, 10)); + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView keyTooLarge = fixture.capture(meta, + databases -> databases.get("abi").put(bytes(2, 1), new byte[0])); + assertThrows(ArchivePersistenceException.class, () -> key.attach(keyTooLarge)); + BlockChangeView valueTooLarge = fixture.capture(meta, + databases -> databases.get("abi").put(bytes(1, 1), bytes(2, 2))); + assertThrows(ArchivePersistenceException.class, () -> value.attach(valueTooLarge)); + } + } + + private static Entry entry(byte[] accountKey, byte[] rawAccount, byte[] canonicalAccount, + AssetMutation... mutations) { + return new Entry(accountKey, BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(canonicalAccount), Arrays.asList(mutations)); + } + + private static byte[] assetKey(byte[] accountKey, int suffix) { + byte[] key = Arrays.copyOf(accountKey, accountKey.length + 1); + key[key.length - 1] = (byte) suffix; + return key; + } + + private static ArchiveBlockForwardMutationLimits limits() { + return new ArchiveBlockForwardMutationLimits(100, 1_000, 1_024, 1024 * 1024, + 10L * 1024 * 1024); + } + + private static List participants() { + List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(participants); + return participants; + } + + private static BlockSnapshotMeta meta(int epoch) { + return BlockSnapshotMeta.forBlock(epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L); + } + + private static HistoryCommitMarker marker(BlockSnapshotMeta meta, List participants) { + int epoch = (int) meta.getEpoch(); + return new HistoryCommitMarker(meta, epoch - 1, + new HistoryLocation(0, epoch * 100L, 100, epoch, bytes(32, epoch + 20)), + new HistoryIndexLocation(epoch * 50L, 50, bytes(32, epoch + 30)), + bytes(16, epoch + 40), participants); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } + + @FunctionalInterface + private interface Mutator { + void mutate(Map databases); + } + + private static final class Fixture implements AutoCloseable { + private final SnapshotManager manager = new SnapshotManager(""); + private final Map roots = new LinkedHashMap<>(); + private final Map databases = new LinkedHashMap<>(); + private final List ordered = new ArrayList<>(); + + private Fixture(List participants) { + for (String participant : participants) { + MemoryDb root = new MemoryDb(participant); + Chainbase database = new Chainbase(new SnapshotRoot(root)); + roots.put(participant, root); + databases.put(participant, database); + ordered.add(database); + manager.add(database); + } + manager.enable(); + } + + private void rootPut(String dbName, byte[] key, byte[] value) { + roots.get(dbName).put(key, value); + } + + private BlockChangeView capture(BlockSnapshotMeta meta, Mutator mutator) { + try (ISession session = manager.buildSession()) { + mutator.mutate(databases); + return BlockChangeView.capture(meta, ordered); + } + } + + @Override + public void close() { + manager.shutdown(); + } + } + + private static final class MemoryDb implements DB, Flusher { + private final String name; + private final Map values = new LinkedHashMap<>(); + + private MemoryDb(String name) { + this.name = name; + } + + @Override + public byte[] get(byte[] key) { + byte[] value = values.get(WrappedByteArray.of(key)); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + @Override + public void put(byte[] key, byte[] value) { + values.put(WrappedByteArray.copyOf(key), Arrays.copyOf(value, value.length)); + } + + @Override + public long size() { + return values.size(); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + @Override + public void remove(byte[] key) { + values.remove(WrappedByteArray.of(key)); + } + + @Override + public Iterator> iterator() { + List> entries = new ArrayList<>(); + values.forEach((key, value) -> entries.add(new AbstractMap.SimpleImmutableEntry<>( + key.getBytes(), Arrays.copyOf(value, value.length)))); + return entries.iterator(); + } + + @Override + public void close() { + values.clear(); + } + + @Override + public void flush(Map batch) { + batch.forEach((key, value) -> { + if (value == null || value.getBytes() == null) { + values.remove(key); + } else { + values.put(WrappedByteArray.copyOf(key.getBytes()), value.getBytes()); + } + }); + } + + @Override + public void reset() { + values.clear(); + } + + @Override + public String getDbName() { + return name; + } + + @Override + public void stat() { + } + + @Override + public DB newInstance() { + return new MemoryDb(name); + } + } +} From da75e6857804bc444d01a317e2ac292a78a6c514 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 12:11:25 +0800 Subject: [PATCH 015/161] test(chainbase): cover archive capture recovery Drive an exact-27 capture batch through mixed native participants, inject a partial apply failure, and verify durable-plan replay, digest propagation, ownership, and restart convergence. --- ...chiveBlockForwardMutationRecoveryTest.java | 404 ++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java new file mode 100644 index 00000000000..2afe892ce2c --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java @@ -0,0 +1,404 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.tron.common.BaseMethodTest; +import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveTargetApplyCoordinator.Stage; +import org.tron.core.db2.common.DB; +import org.tron.core.db2.common.Flusher; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.core.SnapshotRoot; + +/** End-to-end ownership and recovery test from block capture to durable mixed participants. */ +public class ArchiveBlockForwardMutationRecoveryTest extends BaseMethodTest { + + private static final List PARTICIPANTS = participants(); + + @Test + public void captureBatchRecoversOnlyRemainingParticipantsFromDurablePlan() throws Exception { + Path archive = temporaryFolder.newFolder("capture-recovery").toPath(); + List markers = initializeHistory(archive); + HistoryCommitMarker initial = markers.get(0); + HistoryCommitMarker target = markers.get(1); + Path checkpointPath = archive.resolve("progress/checkpoint.progress"); + Path readerPath = archive.resolve("progress/reader.progress"); + ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); + new ArchiveProgressFile(checkpointPath, progressCodec).store(global(Kind.APPLY_CHECKPOINT, + initial)); + new ArchiveProgressFile(readerPath, progressCodec).store(global(Kind.READER_VISIBLE, initial)); + + byte[] accountKey = bytes(2, 1); + byte[] rawAccount = bytes(3, 2); + byte[] canonicalAccount = bytes(3, 3); + byte[] assetKey = append(accountKey, 4); + byte[] assetValue = bytes(3, 5); + byte[] proposalKey = bytes(2, 6); + byte[] proposalValue = bytes(3, 7); + byte[] expectedAccountKey = copy(accountKey); + byte[] expectedCanonicalAccount = copy(canonicalAccount); + byte[] expectedAssetKey = copy(assetKey); + byte[] expectedAssetValue = copy(assetValue); + byte[] expectedProposalKey = copy(proposalKey); + byte[] expectedProposalValue = copy(proposalValue); + + LevelDbArchiveParticipant account = new LevelDbArchiveParticipant( + archive.resolve("participants/account"), "account", PARTICIPANTS); + RocksDbArchiveParticipant accountAsset = new RocksDbArchiveParticipant( + archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS); + Map counted = new LinkedHashMap<>(); + Map memory = new LinkedHashMap<>(); + Map engines = new LinkedHashMap<>(); + try { + for (String participant : PARTICIPANTS) { + ArchiveParticipant delegate; + if ("account".equals(participant)) { + delegate = account; + } else if ("account-asset".equals(participant)) { + delegate = accountAsset; + } else { + MemoryParticipant inMemory = new MemoryParticipant(); + memory.put(participant, inMemory); + delegate = inMemory; + } + CountingParticipant engine = new CountingParticipant(delegate); + engine.apply(Collections.emptyList(), participant(participant, initial)); + counted.put(participant, engine); + engines.put(participant, engine); + } + + ArchiveParticipantMutationBatch batch; + try (ViewFixture viewFixture = new ViewFixture()) { + ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture( + target.getMeta(), new ArchiveBlockForwardMutationLimits( + 10, 10, 1024, 1024, 1024 * 1024)); + capture.recordAccount(target.getMeta(), accountKey, + BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(canonicalAccount)); + capture.recordAssetPut(target.getMeta(), accountKey, assetKey, assetValue); + BlockChangeView view = viewFixture.capture(target.getMeta(), databases -> { + databases.get("account").put(accountKey, rawAccount); + databases.get("proposal").put(proposalKey, proposalValue); + }); + capture.attach(view); + batch = capture.seal(target); + } + + Arrays.fill(accountKey, (byte) 9); + Arrays.fill(rawAccount, (byte) 9); + Arrays.fill(canonicalAccount, (byte) 9); + Arrays.fill(assetKey, (byte) 9); + Arrays.fill(assetValue, (byte) 9); + Arrays.fill(proposalKey, (byte) 9); + Arrays.fill(proposalValue, (byte) 9); + + String firstParticipant = PARTICIPANTS.get(0); + String secondParticipant = PARTICIPANTS.get(1); + try (HistoryCommitStore history = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, + checkpointPath, engines, readerPath, PARTICIPANTS, action -> action.run(), + (stage, participant) -> { + if (stage == Stage.AFTER_PARTICIPANT + && firstParticipant.equals(participant)) { + throw new IOException("injected after first participant"); + } + }, temporary -> { }); + assertThrows(IOException.class, () -> coordinator.apply(batch, () -> { })); + } + + assertEquals(2, counted.get(firstParticipant).getApplyCount()); + assertEquals(1, counted.get(secondParticipant).getApplyCount()); + AtomicInteger refreshes = new AtomicInteger(); + try (ArchiveParticipantRecoveryStorage recovery = + new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, engines, + readerPath, PARTICIPANTS, action -> action.run(), refreshes::incrementAndGet)) { + new ArchiveRecoveryExecutor(recovery).recover(); + } + + assertEquals(2, counted.get(firstParticipant).getApplyCount()); + for (String participant : PARTICIPANTS) { + assertEquals(2, counted.get(participant).getApplyCount()); + } + assertEquals(1, refreshes.get()); + assertArrayEquals(expectedCanonicalAccount, account.get(expectedAccountKey)); + assertArrayEquals(expectedAssetValue, accountAsset.get(expectedAssetKey)); + assertArrayEquals(expectedProposalValue, + memory.get("proposal").get(expectedProposalKey)); + + ArchiveProgressEnvelope checkpoint = + new ArchiveProgressFile(checkpointPath, progressCodec).load(); + ArchiveProgressEnvelope reader = + new ArchiveProgressFile(readerPath, progressCodec).load(); + byte[] planDigest = checkpoint.getMutationPlanDigest(); + assertArrayEquals(planDigest, account.loadProgress().getMutationPlanDigest()); + assertArrayEquals(planDigest, accountAsset.loadProgress().getMutationPlanDigest()); + assertArrayEquals(planDigest, reader.getMutationPlanDigest()); + assertEquals(target.getMeta().getEpoch(), reader.getEpoch()); + assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); + + try (ArchiveParticipantRecoveryStorage fixed = + new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, engines, + readerPath, PARTICIPANTS)) { + assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); + } + } finally { + accountAsset.close(); + account.close(); + } + } + + private static List initializeHistory(Path archive) throws Exception { + List markers = new ArrayList<>(); + try (HistorySegmentStore bodies = new HistorySegmentStore( + archive, new BlockHistoryCodec(), 4096); + HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); + HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + for (int epoch = 0; epoch <= 1; epoch++) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, hash(epoch), + hash(epoch - 1), epoch * 1_000L); + BlockReverseDiff diff = new BlockReverseDiff(meta, + Collections.singletonList(new BlockReverseDiff.DbGroup("account", + Collections.singletonList(new BlockReverseDiff.Entry(bytes(2, epoch + 10), + OldValue.absent()))))); + HistoryLocation body = bodies.append(diff); + HistoryIndexLocation location = index.append(HistoryIndexRecord.from(diff, body)); + markers.add(new HistoryCommitMarker(meta, epoch - 1L, body, location, + bytes(16, epoch + 40), PARTICIPANTS)); + } + bodies.sync(); + index.sync(); + commits.commitAll(markers); + ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), + commits.getRecordLength(), commits.head(), new HistoryCommitMarkerCodec()); + } + return markers; + } + + private static ArchiveProgressEnvelope participant(String participant, + HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, participant, + marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private static List participants() { + List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(participants); + return Collections.unmodifiableList(participants); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } + + private static byte[] append(byte[] prefix, int suffix) { + byte[] result = Arrays.copyOf(prefix, prefix.length + 1); + result[result.length - 1] = (byte) suffix; + return result; + } + + private static byte[] copy(byte[] value) { + return Arrays.copyOf(value, value.length); + } + + @FunctionalInterface + private interface Mutator { + void mutate(Map databases); + } + + private static final class ViewFixture implements AutoCloseable { + private final SnapshotManager manager = new SnapshotManager(""); + private final Map databases = new LinkedHashMap<>(); + private final List ordered = new ArrayList<>(); + + private ViewFixture() { + for (String participant : PARTICIPANTS) { + Chainbase database = new Chainbase(new SnapshotRoot(new ViewMemoryDb(participant))); + databases.put(participant, database); + ordered.add(database); + manager.add(database); + } + manager.enable(); + } + + private BlockChangeView capture(BlockSnapshotMeta meta, Mutator mutator) { + try (ISession session = manager.buildSession()) { + mutator.mutate(databases); + return BlockChangeView.capture(meta, ordered); + } + } + + @Override + public void close() { + manager.shutdown(); + } + } + + private static final class CountingParticipant implements ArchiveParticipant { + private final ArchiveParticipant delegate; + private int applyCount; + + private CountingParticipant(ArchiveParticipant delegate) { + this.delegate = delegate; + } + + @Override + public void apply(List mutations, + ArchiveProgressEnvelope progress) throws IOException { + delegate.apply(mutations, progress); + applyCount++; + } + + @Override + public ArchiveProgressEnvelope loadProgress() throws IOException { + return delegate.loadProgress(); + } + + private int getApplyCount() { + return applyCount; + } + } + + private static final class MemoryParticipant implements ArchiveParticipant { + private final Map values = new LinkedHashMap<>(); + private ArchiveProgressEnvelope progress; + + @Override + public void apply(List mutations, + ArchiveProgressEnvelope progress) { + for (ArchiveParticipantMutation mutation : mutations) { + byte[] value = mutation.getValue(); + WrappedByteArray key = WrappedByteArray.copyOf(mutation.getKey()); + if (value == null) { + values.remove(key); + } else { + values.put(key, copy(value)); + } + } + this.progress = progress; + } + + @Override + public ArchiveProgressEnvelope loadProgress() { + return progress; + } + + private byte[] get(byte[] key) { + byte[] value = values.get(WrappedByteArray.of(key)); + return value == null ? null : copy(value); + } + } + + private static final class ViewMemoryDb implements DB, Flusher { + private final String name; + private final Map values = new LinkedHashMap<>(); + + private ViewMemoryDb(String name) { + this.name = name; + } + + @Override + public byte[] get(byte[] key) { + byte[] value = values.get(WrappedByteArray.of(key)); + return value == null ? null : copy(value); + } + + @Override + public void put(byte[] key, byte[] value) { + values.put(WrappedByteArray.copyOf(key), copy(value)); + } + + @Override + public long size() { + return values.size(); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + @Override + public void remove(byte[] key) { + values.remove(WrappedByteArray.of(key)); + } + + @Override + public Iterator> iterator() { + List> entries = new ArrayList<>(); + values.forEach((key, value) -> entries.add(new AbstractMap.SimpleImmutableEntry<>( + key.getBytes(), copy(value)))); + return entries.iterator(); + } + + @Override + public void close() { + values.clear(); + } + + @Override + public void flush(Map batch) { + batch.forEach((key, value) -> { + if (value == null || value.getBytes() == null) { + values.remove(key); + } else { + values.put(WrappedByteArray.copyOf(key.getBytes()), value.getBytes()); + } + }); + } + + @Override + public void reset() { + values.clear(); + } + + @Override + public String getDbName() { + return name; + } + + @Override + public void stat() { + } + + @Override + public DB newInstance() { + return new ViewMemoryDb(name); + } + } +} From 8cb92f135ad0b34a2312e5bb1533e9b15aaf6ac1 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 12:43:31 +0800 Subject: [PATCH 016/161] test(chainbase): cover consecutive archive targets Apply two capture-produced targets through exact-27 participants. Verify per-target plan digests, overwrite and delete semantics, partial replay boundaries, reader publication, and restart convergence. --- ...chiveBlockForwardMutationRecoveryTest.java | 180 +++++++++++++++++- 1 file changed, 177 insertions(+), 3 deletions(-) diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java index 2afe892ce2c..d646dbb12ec 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import java.io.IOException; @@ -37,7 +38,7 @@ public class ArchiveBlockForwardMutationRecoveryTest extends BaseMethodTest { @Test public void captureBatchRecoversOnlyRemainingParticipantsFromDurablePlan() throws Exception { Path archive = temporaryFolder.newFolder("capture-recovery").toPath(); - List markers = initializeHistory(archive); + List markers = initializeHistory(archive, 1); HistoryCommitMarker initial = markers.get(0); HistoryCommitMarker target = markers.get(1); Path checkpointPath = archive.resolve("progress/checkpoint.progress"); @@ -167,14 +168,150 @@ archive, new HistoryCommitMarkerCodec())) { } } - private static List initializeHistory(Path archive) throws Exception { + @Test + public void consecutiveCaptureTargetsReplaceDigestAndRecoverPutDelete() throws Exception { + Path archive = temporaryFolder.newFolder("consecutive-capture-recovery").toPath(); + List markers = initializeHistory(archive, 2); + HistoryCommitMarker initial = markers.get(0); + HistoryCommitMarker firstTarget = markers.get(1); + HistoryCommitMarker secondTarget = markers.get(2); + Path checkpointPath = archive.resolve("progress/checkpoint.progress"); + Path readerPath = archive.resolve("progress/reader.progress"); + ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); + new ArchiveProgressFile(checkpointPath, progressCodec).store(global(Kind.APPLY_CHECKPOINT, + initial)); + new ArchiveProgressFile(readerPath, progressCodec).store(global(Kind.READER_VISIBLE, initial)); + + byte[] accountKey = bytes(2, 1); + byte[] assetKey = append(accountKey, 4); + byte[] proposalKey = bytes(2, 6); + byte[] firstCanonical = bytes(3, 11); + byte[] firstAsset = bytes(3, 12); + byte[] firstProposal = bytes(3, 13); + byte[] secondCanonical = bytes(3, 21); + byte[] secondProposal = bytes(3, 23); + + try (ParticipantFixture participants = new ParticipantFixture(archive, initial)) { + ArchiveParticipantMutationBatch firstBatch = capture(firstTarget, accountKey, + bytes(3, 10), firstCanonical, assetKey, firstAsset, false, + proposalKey, firstProposal); + try (HistoryCommitStore history = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + new ArchiveTargetApplyCoordinator(history, checkpointPath, participants.engines, + readerPath, PARTICIPANTS, action -> action.run()).apply(firstBatch, () -> { }); + } + + ArchiveProgressEnvelope firstCheckpoint = + new ArchiveProgressFile(checkpointPath, progressCodec).load(); + byte[] firstDigest = firstCheckpoint.getMutationPlanDigest(); + assertArrayEquals(firstCanonical, participants.account.get(accountKey)); + assertArrayEquals(firstAsset, participants.accountAsset.get(assetKey)); + assertArrayEquals(firstProposal, + participants.memory.get("proposal").get(proposalKey)); + assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); + for (String participant : PARTICIPANTS) { + assertEquals(2, participants.counted.get(participant).getApplyCount()); + } + + ArchiveParticipantMutationBatch secondBatch = capture(secondTarget, accountKey, + bytes(3, 20), secondCanonical, assetKey, null, true, + proposalKey, secondProposal); + String failureParticipant = "account-asset"; + int failureIndex = PARTICIPANTS.indexOf(failureParticipant); + try (HistoryCommitStore history = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, + checkpointPath, participants.engines, readerPath, PARTICIPANTS, + action -> action.run(), + (stage, participant) -> failAfter(stage, participant, failureParticipant), + temporary -> { }); + assertThrows(IOException.class, () -> coordinator.apply(secondBatch, () -> { })); + } + + for (int index = 0; index < PARTICIPANTS.size(); index++) { + int expected = index <= failureIndex ? 3 : 2; + assertEquals(expected, + participants.counted.get(PARTICIPANTS.get(index)).getApplyCount()); + } + AtomicInteger refreshes = new AtomicInteger(); + try (ArchiveParticipantRecoveryStorage recovery = + new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, + participants.engines, readerPath, PARTICIPANTS, action -> action.run(), + refreshes::incrementAndGet)) { + new ArchiveRecoveryExecutor(recovery).recover(); + } + + for (String participant : PARTICIPANTS) { + assertEquals(3, participants.counted.get(participant).getApplyCount()); + } + assertEquals(1, refreshes.get()); + assertArrayEquals(secondCanonical, participants.account.get(accountKey)); + assertNull(participants.accountAsset.get(assetKey)); + assertArrayEquals(secondProposal, + participants.memory.get("proposal").get(proposalKey)); + + ArchiveProgressEnvelope secondCheckpoint = + new ArchiveProgressFile(checkpointPath, progressCodec).load(); + ArchiveProgressEnvelope reader = + new ArchiveProgressFile(readerPath, progressCodec).load(); + byte[] secondDigest = secondCheckpoint.getMutationPlanDigest(); + assertFalse(Arrays.equals(firstDigest, secondDigest)); + assertArrayEquals(secondDigest, + participants.account.loadProgress().getMutationPlanDigest()); + assertArrayEquals(secondDigest, + participants.accountAsset.loadProgress().getMutationPlanDigest()); + assertArrayEquals(secondDigest, reader.getMutationPlanDigest()); + assertEquals(secondTarget.getMeta().getEpoch(), reader.getEpoch()); + assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); + + try (ArchiveParticipantRecoveryStorage fixed = + new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, + participants.engines, readerPath, PARTICIPANTS)) { + assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); + } + } + } + + private static ArchiveParticipantMutationBatch capture(HistoryCommitMarker target, + byte[] accountKey, byte[] rawAccount, byte[] canonicalAccount, byte[] assetKey, + byte[] assetValue, boolean deleteAsset, byte[] proposalKey, byte[] proposalValue) { + try (ViewFixture viewFixture = new ViewFixture()) { + ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture( + target.getMeta(), new ArchiveBlockForwardMutationLimits( + 10, 10, 1024, 1024, 1024 * 1024)); + capture.recordAccount(target.getMeta(), accountKey, + BlockChangeView.PostValue.present(rawAccount), + BlockChangeView.PostValue.present(canonicalAccount)); + if (deleteAsset) { + capture.recordAssetDelete(target.getMeta(), accountKey, assetKey); + } else { + capture.recordAssetPut(target.getMeta(), accountKey, assetKey, assetValue); + } + BlockChangeView view = viewFixture.capture(target.getMeta(), databases -> { + databases.get("account").put(accountKey, rawAccount); + databases.get("proposal").put(proposalKey, proposalValue); + }); + capture.attach(view); + return capture.seal(target); + } + } + + private static void failAfter(Stage stage, String participant, String failureParticipant) + throws IOException { + if (stage == Stage.AFTER_PARTICIPANT && failureParticipant.equals(participant)) { + throw new IOException("injected during second target"); + } + } + + private static List initializeHistory(Path archive, int lastEpoch) + throws Exception { List markers = new ArrayList<>(); try (HistorySegmentStore bodies = new HistorySegmentStore( archive, new BlockHistoryCodec(), 4096); HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); HistoryCommitStore commits = new HistoryCommitStore( archive, new HistoryCommitMarkerCodec())) { - for (int epoch = 0; epoch <= 1; epoch++) { + for (int epoch = 0; epoch <= lastEpoch; epoch++) { BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L); BlockReverseDiff diff = new BlockReverseDiff(meta, @@ -269,6 +406,43 @@ public void close() { } } + private static final class ParticipantFixture implements AutoCloseable { + private final LevelDbArchiveParticipant account; + private final RocksDbArchiveParticipant accountAsset; + private final Map counted = new LinkedHashMap<>(); + private final Map memory = new LinkedHashMap<>(); + private final Map engines = new LinkedHashMap<>(); + + private ParticipantFixture(Path archive, HistoryCommitMarker initial) throws IOException { + account = new LevelDbArchiveParticipant( + archive.resolve("participants/account"), "account", PARTICIPANTS); + accountAsset = new RocksDbArchiveParticipant( + archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS); + for (String participant : PARTICIPANTS) { + ArchiveParticipant delegate; + if ("account".equals(participant)) { + delegate = account; + } else if ("account-asset".equals(participant)) { + delegate = accountAsset; + } else { + MemoryParticipant inMemory = new MemoryParticipant(); + memory.put(participant, inMemory); + delegate = inMemory; + } + CountingParticipant engine = new CountingParticipant(delegate); + engine.apply(Collections.emptyList(), participant(participant, initial)); + counted.put(participant, engine); + engines.put(participant, engine); + } + } + + @Override + public void close() throws IOException { + accountAsset.close(); + account.close(); + } + } + private static final class CountingParticipant implements ArchiveParticipant { private final ArchiveParticipant delegate; private int applyCount; From cbff71ae6176a60222cded4cee72998ba6e44293 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 18:58:56 +0800 Subject: [PATCH 017/161] feat(chainbase): own archive block projections Prepare one target-bound Account and AccountAsset projection before snapshot merge, retain its forward payload through the revoking lifecycle, and seal it only against durable history markers. Keep the integration default-off and preserve the existing proposal 66 physical layout. --- .../archive/AccountAssetArchiveProjector.java | 92 ++++-- .../AccountAssetBlockProjectionBridge.java | 232 +++++++++++++++ .../AccountAssetOldPhysicalAssetsSource.java | 28 ++ ...AccountAssetPreparedBlockPayloadOwner.java | 191 ++++++++++++ .../AccountAssetTargetActivationResolver.java | 80 +++++ .../archive/ArchiveBlockForwardPayload.java | 38 +++ .../ArchiveBlockProjectionPreparer.java | 10 + .../db2/archive/ArchiveHistoryWriter.java | 8 + .../DurableHistoryMarkerRangeReceipt.java | 130 +++++++++ .../archive/SnapshotOldValueCollector.java | 30 +- .../tron/core/db2/core/SnapshotManager.java | 275 +++++++++++++++++- .../main/java/org/tron/core/db/Manager.java | 8 +- 12 files changed, 1082 insertions(+), 40 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetOldPhysicalAssetsSource.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetPreparedBlockPayloadOwner.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardPayload.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockProjectionPreparer.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceipt.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java index 70a3b7fb9c0..943f9a6d4a2 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java @@ -6,14 +6,14 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.BooleanSupplier; +import java.util.TreeSet; +import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; import org.tron.core.db2.common.WrappedByteArray; -import org.tron.core.store.AccountAssetStore; import org.tron.protos.Protocol.Account; /** @@ -25,34 +25,43 @@ public final class AccountAssetArchiveProjector { public static final String ACCOUNT_DB = "account"; public static final String ACCOUNT_ASSET_DB = "account-asset"; - private final AccountAssetStore assetStore; - private final BooleanSupplier optimizationEnabled; - - public AccountAssetArchiveProjector(AccountAssetStore assetStore, - BooleanSupplier optimizationEnabled) { - this.assetStore = assetStore; - this.optimizationEnabled = optimizationEnabled; - } - - Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue rawPost) { + Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue rawPost, + boolean targetAssetOptimizationEnabled, + Map oldPhysicalAssetsForAddress) { Account oldAccount = parse(rawOld); Account postAccount = rawPost.isPresent() ? parse(rawPost.getValue()) : null; boolean projectPost = postAccount != null - && (postAccount.getAssetOptimized() || optimizationEnabled.getAsBoolean()); + && (postAccount.getAssetOptimized() || targetAssetOptimizationEnabled); + + boolean requiresOldPhysicalAssets = requiresOldPhysicalAssets(oldAccount, postAccount); + if (requiresOldPhysicalAssets && oldPhysicalAssetsForAddress == null) { + throw new ArchivePersistenceException( + "Optimized Account projection requires explicit old physical assets"); + } + Map physicalSnapshot = copyPhysicalAssets(accountKey, + oldPhysicalAssetsForAddress == null ? Collections.emptyMap() + : oldPhysicalAssetsForAddress); Map oldAssets = physicalAssets(accountKey, oldAccount, - oldAccount != null && oldAccount.getAssetOptimized()); - Map postAssets = physicalAssets(accountKey, postAccount, projectPost); + oldAccount != null && oldAccount.getAssetOptimized(), physicalSnapshot); + Map postAssets = physicalAssets(accountKey, postAccount, projectPost, + physicalSnapshot); - Set assetKeys = new HashSet<>(oldAssets.keySet()); + Set assetKeys = new TreeSet<>((left, right) -> + BlockReverseDiff.compareUnsigned(left.getBytes(), right.getBytes())); + assetKeys.addAll(oldAssets.keySet()); assetKeys.addAll(postAssets.keySet()); List reverseAssets = new ArrayList<>(); + List forwardAssets = new ArrayList<>(); for (WrappedByteArray assetKey : assetKeys) { byte[] oldValue = oldAssets.get(assetKey); byte[] postValue = postAssets.get(assetKey); if (!Arrays.equals(oldValue, postValue)) { reverseAssets.add(new BlockReverseDiff.Entry(assetKey.getBytes(), OldValue.fromNullable(oldValue))); + forwardAssets.add(new AssetMutation(assetKey.getBytes(), postValue == null + ? BlockChangeView.PostValue.absent() + : BlockChangeView.PostValue.present(postValue))); } } @@ -61,17 +70,54 @@ Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue r BlockChangeView.PostValue canonicalPost = postAccount == null ? BlockChangeView.PostValue.absent() : BlockChangeView.PostValue.present(canonicalAccount(postAccount, projectPost)); - return new Projection(canonicalOld, canonicalPost, reverseAssets); + return new Projection(canonicalOld, canonicalPost, reverseAssets, forwardAssets); + } + + boolean requiresOldPhysicalAssets(byte[] rawOld, BlockChangeView.PostValue rawPost) { + return requiresOldPhysicalAssets(parse(rawOld), + rawPost.isPresent() ? parse(rawPost.getValue()) : null); + } + + private boolean requiresOldPhysicalAssets(Account oldAccount, Account postAccount) { + return oldAccount != null && oldAccount.getAssetOptimized() + || postAccount != null && postAccount.getAssetOptimized(); + } + + private Map copyPhysicalAssets(byte[] accountKey, + Map oldPhysicalAssetsForAddress) { + Map copy = new HashMap<>(); + oldPhysicalAssetsForAddress.forEach((key, value) -> { + if (key == null || value == null) { + throw new ArchivePersistenceException("Old physical AccountAsset input contains null"); + } + byte[] physicalKey = key.getBytes(); + if (physicalKey.length <= accountKey.length + || !startsWith(physicalKey, accountKey)) { + throw new ArchivePersistenceException( + "Old physical AccountAsset input does not belong to changed Account"); + } + copy.put(WrappedByteArray.copyOf(physicalKey), Arrays.copyOf(value, value.length)); + }); + return copy; + } + + private boolean startsWith(byte[] value, byte[] prefix) { + for (int i = 0; i < prefix.length; i++) { + if (value[i] != prefix[i]) { + return false; + } + } + return true; } private Map physicalAssets(byte[] accountKey, Account account, - boolean projected) { + boolean projected, Map physicalSnapshot) { Map result = new HashMap<>(); if (account == null || !projected) { return result; } if (account.getAssetOptimized()) { - assetStore.prefixQuery(accountKey).forEach((key, value) -> result.put( + physicalSnapshot.forEach((key, value) -> result.put( WrappedByteArray.copyOf(key.getBytes()), Arrays.copyOf(value, value.length))); } account.getAssetV2Map().forEach((token, balance) -> { @@ -113,12 +159,14 @@ static final class Projection { final OldValue oldAccount; final BlockChangeView.PostValue postAccount; final List reverseAssets; + final List forwardAssets; private Projection(OldValue oldAccount, BlockChangeView.PostValue postAccount, - List reverseAssets) { + List reverseAssets, List forwardAssets) { this.oldAccount = oldAccount; this.postAccount = postAccount; - this.reverseAssets = reverseAssets; + this.reverseAssets = Collections.unmodifiableList(new ArrayList<>(reverseAssets)); + this.forwardAssets = Collections.unmodifiableList(new ArrayList<>(forwardAssets)); } } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java new file mode 100644 index 00000000000..2a054b23e34 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java @@ -0,0 +1,232 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.tron.core.db2.archive.AccountAssetForwardMutationManifest.Entry; +import org.tron.core.db2.archive.BlockChangeView.Change; +import org.tron.core.db2.archive.BlockChangeView.DatabaseChanges; +import org.tron.core.db2.archive.BlockChangeView.PostValue; +import org.tron.core.db2.common.WrappedByteArray; + +/** + * Standalone bridge which prepares reverse and forward projections before a durable history marker + * exists, then seals the forward projection against that marker exactly once. + */ +public final class AccountAssetBlockProjectionBridge { + + private final AccountAssetArchiveProjector projector; + private final AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource; + private final List participants; + + public AccountAssetBlockProjectionBridge(AccountAssetArchiveProjector projector, + AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource) { + this.projector = Objects.requireNonNull(projector, "projector"); + this.oldPhysicalAssetsSource = Objects.requireNonNull(oldPhysicalAssetsSource, + "oldPhysicalAssetsSource"); + List expected = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(expected); + participants = Collections.unmodifiableList(expected); + } + + public PreparedBlockProjection prepare(BlockChangeView view, + TargetAssetOptimization activation) { + BlockChangeView input = Objects.requireNonNull(view, "view"); + TargetAssetOptimization targetActivation = Objects.requireNonNull(activation, "activation"); + validateBeforeProjection(input, targetActivation); + + List groups = new ArrayList<>(); + List accountAssetEntries = new ArrayList<>(); + List forwardEntries = new ArrayList<>(); + for (DatabaseChanges database : input.getDatabases()) { + List entries = new ArrayList<>(); + for (Change change : database.getChanges()) { + byte[] key = change.getKey(); + OldValue oldValue = OldValue.fromNullable(database.getPrevious(key)); + PostValue postValue = change.getPostValue(); + if (AccountAssetArchiveProjector.ACCOUNT_DB.equals(database.getDbName())) { + Map oldPhysicalAssets = Collections.emptyMap(); + if (projector.requiresOldPhysicalAssets( + oldValue.isPresent() ? oldValue.getValue() : null, postValue)) { + oldPhysicalAssets = AccountAssetOldPhysicalAssetsSource.captureRequired( + oldPhysicalAssetsSource, key); + } + AccountAssetArchiveProjector.Projection projection = projector.project(key, + oldValue.isPresent() ? oldValue.getValue() : null, postValue, + targetActivation.isEnabled(), oldPhysicalAssets); + oldValue = projection.oldAccount; + postValue = projection.postAccount; + accountAssetEntries.addAll(projection.reverseAssets); + forwardEntries.add(new Entry(key, change.getPostValue(), projection.postAccount, + projection.forwardAssets)); + } + if (!sameLogicalValue(oldValue, postValue)) { + entries.add(new BlockReverseDiff.Entry(key, oldValue)); + } + } + if (!entries.isEmpty()) { + groups.add(new BlockReverseDiff.DbGroup(database.getDbName(), entries)); + } + } + if (!accountAssetEntries.isEmpty()) { + groups.add(new BlockReverseDiff.DbGroup(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + accountAssetEntries)); + } + + BlockReverseDiff reverse = new BlockReverseDiff(input.getMeta(), groups); + return new PreparedBlockProjection(input, participants, reverse, forwardEntries); + } + + private void validateBeforeProjection(BlockChangeView view, + TargetAssetOptimization activation) { + if (!view.getMeta().equals(activation.getMeta())) { + throw new ArchivePersistenceException("Block projection activation identity mismatch"); + } + List actual = new ArrayList<>(); + for (DatabaseChanges database : view.getDatabases()) { + actual.add(database.getDbName()); + if (AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(database.getDbName()) + && !database.getChanges().isEmpty()) { + throw new ArchivePersistenceException( + "AccountAsset block projection requires one derived physical mutation source"); + } + } + Collections.sort(actual); + if (!actual.equals(participants)) { + throw new ArchivePersistenceException( + "Block projection does not cover the exact VERSIONED_STATE set"); + } + } + + private static boolean sameLogicalValue(OldValue oldValue, PostValue postValue) { + return oldValue.isPresent() == postValue.isPresent() + && (!oldValue.isPresent() || Arrays.equals(oldValue.getValue(), postValue.getValue())); + } + + /** Target-bound proposal-66 state; mismatched identity is rejected before any Store read. */ + public static final class TargetAssetOptimization { + private final BlockSnapshotMeta meta; + private final boolean enabled; + + private TargetAssetOptimization(BlockSnapshotMeta meta, boolean enabled) { + this.meta = Objects.requireNonNull(meta, "meta"); + this.enabled = enabled; + } + + public static TargetAssetOptimization forTarget(BlockSnapshotMeta meta, boolean enabled) { + return new TargetAssetOptimization(meta, enabled); + } + + BlockSnapshotMeta getMeta() { + return meta; + } + + boolean isEnabled() { + return enabled; + } + } + + /** Meta-bound projection owner which can be sealed or aborted exactly once. */ + public static final class PreparedBlockProjection { + private final BlockSnapshotMeta meta; + private final List participants; + private BlockChangeView view; + private BlockReverseDiff reverseDiff; + private List forwardEntries; + private State state = State.PREPARED; + + private PreparedBlockProjection(BlockChangeView view, List participants, + BlockReverseDiff reverseDiff, List forwardEntries) { + this.view = Objects.requireNonNull(view, "view"); + this.meta = view.getMeta(); + this.participants = Collections.unmodifiableList(new ArrayList<>(participants)); + this.reverseDiff = Objects.requireNonNull(reverseDiff, "reverseDiff"); + this.forwardEntries = Collections.unmodifiableList(new ArrayList<>(forwardEntries)); + } + + public synchronized BlockReverseDiff getReverseDiff() { + if (state == State.ABORTED) { + throw new ArchivePersistenceException("Prepared block projection was aborted"); + } + return reverseDiff; + } + + synchronized BlockSnapshotMeta getMeta() { + return meta; + } + + synchronized void requirePreparedOwnership() { + requirePrepared(); + } + + public synchronized AccountAssetForwardMutationManifest seal(HistoryCommitMarker marker) { + AccountAssetForwardMutationManifest manifest = previewSeal(marker); + completeSeal(); + return manifest; + } + + public synchronized ArchiveBlockForwardPayload sealPayload(HistoryCommitMarker marker) { + ArchiveBlockForwardPayload payload = previewSealPayload(marker); + completeSeal(); + return payload; + } + + synchronized AccountAssetForwardMutationManifest previewSeal(HistoryCommitMarker marker) { + HistoryCommitMarker target = validateMarker(marker); + return new AccountAssetForwardMutationManifest(target, forwardEntries); + } + + synchronized ArchiveBlockForwardPayload previewSealPayload(HistoryCommitMarker marker) { + HistoryCommitMarker target = validateMarker(marker); + return new ArchiveBlockForwardPayload(target, view, + new AccountAssetForwardMutationManifest(target, forwardEntries)); + } + + synchronized HistoryCommitMarker validateMarker(HistoryCommitMarker marker) { + requirePrepared(); + HistoryCommitMarker target = Objects.requireNonNull(marker, "marker"); + if (!meta.equals(target.getMeta())) { + throw new ArchivePersistenceException("Prepared block projection target mismatch"); + } + if (!participants.equals(target.getDatabases())) { + throw new ArchivePersistenceException( + "Prepared block projection participant set mismatch"); + } + return target; + } + + synchronized void completeSeal() { + requirePrepared(); + view = null; + forwardEntries = Collections.emptyList(); + state = State.SEALED; + } + + synchronized boolean retainsCapturedView() { + return view != null; + } + + public synchronized void abort() { + requirePrepared(); + view = null; + reverseDiff = null; + forwardEntries = Collections.emptyList(); + state = State.ABORTED; + } + + private void requirePrepared() { + if (state != State.PREPARED) { + throw new ArchivePersistenceException("Prepared block projection is terminal"); + } + } + + private enum State { + PREPARED, + SEALED, + ABORTED + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetOldPhysicalAssetsSource.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetOldPhysicalAssetsSource.java new file mode 100644 index 00000000000..3598b640838 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetOldPhysicalAssetsSource.java @@ -0,0 +1,28 @@ +package org.tron.core.db2.archive; + +import java.util.Map; +import java.util.Objects; +import org.tron.core.db2.common.WrappedByteArray; + +/** Commit-time source for one changed account's old physical account-asset rows. */ +@FunctionalInterface +public interface AccountAssetOldPhysicalAssetsSource { + + /** Returns the complete old physical rows for {@code accountKey}. */ + Map capture(byte[] accountKey); + + static Map captureRequired( + AccountAssetOldPhysicalAssetsSource source, byte[] accountKey) { + try { + Map captured = Objects.requireNonNull( + Objects.requireNonNull(source, "source").capture(accountKey), + "old physical AccountAsset input"); + return captured; + } catch (ArchivePersistenceException failure) { + throw failure; + } catch (RuntimeException failure) { + throw new ArchivePersistenceException( + "Failed to capture old physical AccountAsset input", failure); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetPreparedBlockPayloadOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetPreparedBlockPayloadOwner.java new file mode 100644 index 00000000000..ad165b28948 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetPreparedBlockPayloadOwner.java @@ -0,0 +1,191 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.PreparedBlockProjection; + +/** Standalone ownership seam from one block layer to one immutable contiguous flush batch. */ +public final class AccountAssetPreparedBlockPayloadOwner { + + private static final Object OWNERSHIP_LOCK = new Object(); + + private final BlockSnapshotMeta meta; + private PreparedBlockProjection projection; + private State state = State.EMPTY; + + public AccountAssetPreparedBlockPayloadOwner(BlockSnapshotMeta meta) { + this.meta = Objects.requireNonNull(meta, "meta"); + } + + public void attach(PreparedBlockProjection prepared) { + synchronized (OWNERSHIP_LOCK) { + if (state != State.EMPTY) { + throw new ArchivePersistenceException("Block payload owner already left empty state"); + } + PreparedBlockProjection candidate = Objects.requireNonNull(prepared, "prepared"); + if (!meta.equals(candidate.getMeta())) { + throw new ArchivePersistenceException("Block payload owner target mismatch"); + } + candidate.requirePreparedOwnership(); + projection = candidate; + state = State.ATTACHED; + } + } + + public BlockReverseDiff getReverseDiff() { + synchronized (OWNERSHIP_LOCK) { + requireAttached(); + return projection.getReverseDiff(); + } + } + + public void discard() { + synchronized (OWNERSHIP_LOCK) { + requireAttached(); + projection.abort(); + projection = null; + state = State.DISCARDED; + } + } + + public boolean isAttachedTo(BlockSnapshotMeta expectedMeta) { + synchronized (OWNERSHIP_LOCK) { + return state == State.ATTACHED && meta.equals(expectedMeta); + } + } + + public static FrozenBatch freezeContiguous( + List owners) { + synchronized (OWNERSHIP_LOCK) { + List candidates = new ArrayList<>( + Objects.requireNonNull(owners, "owners")); + if (candidates.isEmpty()) { + throw new ArchivePersistenceException("Flush batch must contain at least one payload"); + } + Set unique = new HashSet<>(); + BlockSnapshotMeta previous = null; + for (AccountAssetPreparedBlockPayloadOwner owner : candidates) { + AccountAssetPreparedBlockPayloadOwner candidate = Objects.requireNonNull(owner, "owner"); + if (!unique.add(candidate)) { + throw new ArchivePersistenceException("Flush batch contains duplicate payload owner"); + } + candidate.requireAttached(); + if (previous != null && !isNext(previous, candidate.meta)) { + throw new ArchivePersistenceException("Flush batch payloads are not contiguous"); + } + previous = candidate.meta; + } + + List payloads = new ArrayList<>(); + for (AccountAssetPreparedBlockPayloadOwner owner : candidates) { + payloads.add(owner.transfer()); + } + return new FrozenBatch(payloads); + } + } + + private static boolean isNext(BlockSnapshotMeta previous, BlockSnapshotMeta current) { + return current.getEpoch() == previous.getEpoch() + 1 + && current.getBlockNumber() == previous.getBlockNumber() + 1 + && Arrays.equals(current.getParentHash(), previous.getBlockHash()); + } + + private PreparedBlockProjection transfer() { + requireAttached(); + PreparedBlockProjection transferred = projection; + projection = null; + state = State.TRANSFERRED; + return transferred; + } + + private void requireAttached() { + if (state != State.ATTACHED) { + throw new ArchivePersistenceException("Block payload owner is not attached"); + } + } + + private enum State { + EMPTY, + ATTACHED, + TRANSFERRED, + DISCARDED + } + + /** Immutable flush-range owner; a marker mismatch does not consume any block payload. */ + public static final class FrozenBatch { + private final List expectedMetas; + private List payloads; + private BatchState state = BatchState.FROZEN; + + private FrozenBatch(List payloads) { + this.payloads = Collections.unmodifiableList(new ArrayList<>(payloads)); + List metas = new ArrayList<>(payloads.size()); + for (PreparedBlockProjection payload : payloads) { + metas.add(payload.getMeta()); + } + expectedMetas = Collections.unmodifiableList(metas); + } + + public synchronized List getExpectedMetas() { + requireFrozen(); + return expectedMetas; + } + + public synchronized boolean contains(BlockSnapshotMeta meta) { + return expectedMetas.contains(Objects.requireNonNull(meta, "meta")); + } + + public synchronized List seal( + List markers) { + requireFrozen(); + List targets = new ArrayList<>( + Objects.requireNonNull(markers, "markers")); + if (targets.size() != payloads.size()) { + throw new ArchivePersistenceException("Flush batch marker count mismatch"); + } + + List sealed = new ArrayList<>(); + for (int i = 0; i < payloads.size(); i++) { + sealed.add(payloads.get(i).previewSealPayload(targets.get(i))); + } + for (PreparedBlockProjection payload : payloads) { + payload.completeSeal(); + } + payloads = Collections.emptyList(); + state = BatchState.SEALED; + return Collections.unmodifiableList(sealed); + } + + public synchronized void abort() { + requireFrozen(); + for (PreparedBlockProjection payload : payloads) { + payload.abort(); + } + payloads = Collections.emptyList(); + state = BatchState.ABORTED; + } + + public synchronized void abortIfFrozen() { + if (state == BatchState.FROZEN) { + abort(); + } + } + + private void requireFrozen() { + if (state != BatchState.FROZEN) { + throw new ArchivePersistenceException("Flush batch payload owner is terminal"); + } + } + + private enum BatchState { + FROZEN, + SEALED, + ABORTED + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java new file mode 100644 index 00000000000..a06263284cf --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java @@ -0,0 +1,80 @@ +package org.tron.core.db2.archive; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; +import org.tron.common.utils.ByteArray; +import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.TargetAssetOptimization; +import org.tron.core.db2.archive.BlockChangeView.Change; +import org.tron.core.db2.archive.BlockChangeView.DatabaseChanges; +import org.tron.core.db2.archive.BlockChangeView.PostValue; + +/** Resolves proposal-66 activation from the target block's exact properties post view. */ +public final class AccountAssetTargetActivationResolver { + + static final String PROPERTIES_DB = "properties"; + static final String PROPOSAL_66_KEY = "ALLOW_ASSET_OPTIMIZATION"; + static final String PROPOSAL_53_KEY = "ALLOW_ACCOUNT_ASSET_OPTIMIZATION"; + + private static final byte[] PROPOSAL_66_PHYSICAL_KEY = + PROPOSAL_66_KEY.getBytes(StandardCharsets.UTF_8); + + public TargetAssetOptimization resolve(BlockSnapshotMeta target, BlockChangeView view) { + BlockSnapshotMeta expectedTarget = Objects.requireNonNull(target, "target"); + BlockChangeView input = Objects.requireNonNull(view, "view"); + if (!expectedTarget.equals(input.getMeta())) { + throw new ArchivePersistenceException("Asset optimization target identity mismatch"); + } + + DatabaseChanges properties = null; + for (DatabaseChanges database : input.getDatabases()) { + if (PROPERTIES_DB.equals(database.getDbName())) { + if (properties != null) { + throw new ArchivePersistenceException("Duplicate properties block view"); + } + properties = database; + } + } + if (properties == null) { + throw new ArchivePersistenceException("Missing properties block view"); + } + + byte[] value = null; + boolean changed = false; + for (Change change : properties.getChanges()) { + if (Arrays.equals(PROPOSAL_66_PHYSICAL_KEY, change.getKey())) { + if (changed) { + throw new ArchivePersistenceException("Duplicate proposal-66 property mutation"); + } + changed = true; + PostValue postValue = change.getPostValue(); + if (!postValue.isPresent()) { + throw new ArchivePersistenceException("Proposal-66 property must not be deleted"); + } + value = postValue.getValue(); + } + } + if (!changed) { + value = properties.getPrevious(PROPOSAL_66_PHYSICAL_KEY); + } + return TargetAssetOptimization.forTarget(expectedTarget, decode(value)); + } + + static byte[] proposal66PhysicalKey() { + return Arrays.copyOf(PROPOSAL_66_PHYSICAL_KEY, PROPOSAL_66_PHYSICAL_KEY.length); + } + + private static boolean decode(byte[] value) { + if (value == null) { + throw new ArchivePersistenceException("Missing proposal-66 property value"); + } + if (value.length != Long.BYTES) { + throw new ArchivePersistenceException("Proposal-66 property value must be exactly 8 bytes"); + } + long decoded = ByteArray.toLong(value); + if (decoded != 0L && decoded != 1L) { + throw new ArchivePersistenceException("Proposal-66 property value must be 0 or 1"); + } + return decoded == 1L; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardPayload.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardPayload.java new file mode 100644 index 00000000000..0879e76846a --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardPayload.java @@ -0,0 +1,38 @@ +package org.tron.core.db2.archive; + +import java.util.Objects; + +/** Immutable handoff of one committed target's exact view and AccountAsset projection. */ +public final class ArchiveBlockForwardPayload { + + private final HistoryCommitMarker marker; + private final BlockChangeView view; + private final AccountAssetForwardMutationManifest accountAssetManifest; + + ArchiveBlockForwardPayload(HistoryCommitMarker marker, BlockChangeView view, + AccountAssetForwardMutationManifest accountAssetManifest) { + this.marker = Objects.requireNonNull(marker, "marker"); + this.view = Objects.requireNonNull(view, "view"); + this.accountAssetManifest = Objects.requireNonNull(accountAssetManifest, + "accountAssetManifest"); + if (!marker.getMeta().equals(view.getMeta())) { + throw new ArchivePersistenceException("Forward payload view target mismatch"); + } + } + + public BlockSnapshotMeta getMeta() { + return marker.getMeta(); + } + + public HistoryCommitMarker getMarker() { + return marker; + } + + public BlockChangeView getView() { + return view; + } + + public AccountAssetForwardMutationManifest getAccountAssetManifest() { + return accountAssetManifest; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockProjectionPreparer.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockProjectionPreparer.java new file mode 100644 index 00000000000..1e0633c5518 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockProjectionPreparer.java @@ -0,0 +1,10 @@ +package org.tron.core.db2.archive; + +import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.PreparedBlockProjection; + +/** Prepares one identity-bound reverse and forward projection from one immutable block view. */ +@FunctionalInterface +public interface ArchiveBlockProjectionPreparer { + + PreparedBlockProjection prepare(BlockChangeView view); +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java index 41a9804aadf..79505a6c12b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java @@ -136,6 +136,14 @@ public synchronized HistoryCommitMarker committedHead() { return commits.head(); } + synchronized HistoryCommitMarker committedMarker(long epoch) { + HistoryCommitMarker marker = commits.get(epoch); + if (marker == null) { + return null; + } + return commitCodec.decode(commitCodec.encode(marker)); + } + @Override public synchronized void awaitCommitted(long epoch) { HistoryCommitMarker head = commits.head(); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceipt.java b/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceipt.java new file mode 100644 index 00000000000..84221639d9f --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceipt.java @@ -0,0 +1,130 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; + +/** Bounded authoritative marker receipt for one exact frozen flush range. */ +public final class DurableHistoryMarkerRangeReceipt { + + private final Source source; + private final int maxMarkers; + private final List participants; + private final HistoryCommitMarkerCodec codec = new HistoryCommitMarkerCodec(); + + public DurableHistoryMarkerRangeReceipt(ArchiveHistoryWriter writer, int maxMarkers) { + this(new WriterSource(writer), maxMarkers); + } + + DurableHistoryMarkerRangeReceipt(Source source, int maxMarkers) { + this.source = Objects.requireNonNull(source, "source"); + if (maxMarkers <= 0) { + throw new IllegalArgumentException("maxMarkers must be positive"); + } + this.maxMarkers = maxMarkers; + List expected = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(expected); + participants = Collections.unmodifiableList(expected); + } + + public List seal(FrozenBatch batch) { + FrozenBatch target = Objects.requireNonNull(batch, "batch"); + return target.seal(read(target.getExpectedMetas())); + } + + public List read(List expectedMetas) { + List expected = new ArrayList<>( + Objects.requireNonNull(expectedMetas, "expectedMetas")); + validateExpectedRange(expected); + + List markers = new ArrayList<>(expected.size()); + for (BlockSnapshotMeta meta : expected) { + HistoryCommitMarker marker = source.marker(meta.getEpoch()); + validateMarker(meta, marker); + markers.add(marker); + } + + List receipt = new ArrayList<>(markers.size()); + for (int i = 0; i < markers.size(); i++) { + HistoryCommitMarker marker = markers.get(i); + BlockReverseDiff body = source.readCommitted(marker.getMeta().getEpoch()); + if (body == null || !expected.get(i).equals(body.getMeta())) { + throw new ArchivePersistenceException( + "Committed history body does not match marker range"); + } + HistoryCommitMarker reloaded = source.marker(marker.getMeta().getEpoch()); + validateMarker(expected.get(i), reloaded); + if (!Arrays.equals(codec.encode(marker), codec.encode(reloaded))) { + throw new ArchivePersistenceException("History marker changed while building receipt"); + } + receipt.add(codec.decode(codec.encode(reloaded))); + } + return Collections.unmodifiableList(receipt); + } + + private void validateExpectedRange(List expected) { + if (expected.isEmpty()) { + throw new ArchivePersistenceException("Marker receipt range must not be empty"); + } + if (expected.size() > maxMarkers) { + throw new ArchivePersistenceException("Marker receipt range exceeds configured bound"); + } + BlockSnapshotMeta previous = null; + for (BlockSnapshotMeta meta : expected) { + BlockSnapshotMeta current = Objects.requireNonNull(meta, "expectedMeta"); + if (previous != null && !isNext(previous, current)) { + throw new ArchivePersistenceException("Expected marker receipt range is not contiguous"); + } + previous = current; + } + } + + private void validateMarker(BlockSnapshotMeta expected, HistoryCommitMarker marker) { + if (marker == null) { + throw new ArchivePersistenceException( + "Committed history marker is missing for epoch " + expected.getEpoch()); + } + if (!expected.equals(marker.getMeta())) { + throw new ArchivePersistenceException("Committed history marker target mismatch"); + } + if (marker.getPreviousEpoch() != expected.getEpoch() - 1) { + throw new ArchivePersistenceException("Committed history marker predecessor mismatch"); + } + if (!participants.equals(marker.getDatabases())) { + throw new ArchivePersistenceException("Committed history marker participant set mismatch"); + } + } + + private static boolean isNext(BlockSnapshotMeta previous, BlockSnapshotMeta current) { + return current.getEpoch() == previous.getEpoch() + 1 + && current.getBlockNumber() == previous.getBlockNumber() + 1 + && Arrays.equals(current.getParentHash(), previous.getBlockHash()); + } + + interface Source { + HistoryCommitMarker marker(long epoch); + + BlockReverseDiff readCommitted(long epoch); + } + + private static final class WriterSource implements Source { + private final ArchiveHistoryWriter writer; + + private WriterSource(ArchiveHistoryWriter writer) { + this.writer = Objects.requireNonNull(writer, "writer"); + } + + @Override + public HistoryCommitMarker marker(long epoch) { + return writer.committedMarker(epoch); + } + + @Override + public BlockReverseDiff readCommitted(long epoch) { + return writer.readCommitted(epoch); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java index 826b046178b..dcd149b732d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java @@ -2,25 +2,42 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BooleanSupplier; +import org.tron.core.db2.common.WrappedByteArray; /** Scheme 2 collector: read old values from the completed block layer's previous view. */ public final class SnapshotOldValueCollector implements OldValueCollector { private final AccountAssetArchiveProjector accountAssetProjector; + private final AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource; + private final BooleanSupplier optimizationEnabled; public SnapshotOldValueCollector() { - this(null); + this(null, null, null); } - public SnapshotOldValueCollector(AccountAssetArchiveProjector accountAssetProjector) { + public SnapshotOldValueCollector(AccountAssetArchiveProjector accountAssetProjector, + AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource, + BooleanSupplier optimizationEnabled) { this.accountAssetProjector = accountAssetProjector; + this.oldPhysicalAssetsSource = oldPhysicalAssetsSource; + this.optimizationEnabled = optimizationEnabled; + if (accountAssetProjector != null) { + Objects.requireNonNull(oldPhysicalAssetsSource, "oldPhysicalAssetsSource"); + Objects.requireNonNull(optimizationEnabled, "optimizationEnabled"); + } } @Override public BlockReverseDiff collect(BlockChangeView view) { List groups = new ArrayList<>(); List accountAssetEntries = new ArrayList<>(); + boolean targetAssetOptimizationEnabled = accountAssetProjector != null + && optimizationEnabled.getAsBoolean(); for (BlockChangeView.DatabaseChanges database : view.getDatabases()) { List entries = new ArrayList<>(); for (BlockChangeView.Change change : database.getChanges()) { @@ -29,8 +46,15 @@ public BlockReverseDiff collect(BlockChangeView view) { BlockChangeView.PostValue postValue = change.getPostValue(); if (accountAssetProjector != null && AccountAssetArchiveProjector.ACCOUNT_DB.equals(database.getDbName())) { + Map oldPhysicalAssets = Collections.emptyMap(); + if (accountAssetProjector.requiresOldPhysicalAssets( + oldValue.isPresent() ? oldValue.getValue() : null, postValue)) { + oldPhysicalAssets = AccountAssetOldPhysicalAssetsSource.captureRequired( + oldPhysicalAssetsSource, key); + } AccountAssetArchiveProjector.Projection projection = accountAssetProjector.project( - key, oldValue.isPresent() ? oldValue.getValue() : null, postValue); + key, oldValue.isPresent() ? oldValue.getValue() : null, postValue, + targetAssetOptimizationEnabled, oldPhysicalAssets); oldValue = projection.oldAccount; postValue = projection.postAccount; accountAssetEntries.addAll(projection.reverseAssets); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index d90917a1c84..8d9d1da638d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -36,13 +36,20 @@ import org.tron.core.db.RevokingDatabase; import org.tron.core.db.TronDatabase; import org.tron.core.db2.ISession; -import org.tron.core.db2.archive.ArchiveStoreScope; +import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.PreparedBlockProjection; +import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner; +import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; +import org.tron.core.db2.archive.ArchiveBlockForwardPayload; +import org.tron.core.db2.archive.ArchiveBlockProjectionPreparer; import org.tron.core.db2.archive.ArchiveStateBarrier.ArchiveStateAction; +import org.tron.core.db2.archive.ArchiveStoreScope; import org.tron.core.db2.archive.BlockChangeView; import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockReverseDiffSink; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.archive.DurableBlockReverseDiffSink; +import org.tron.core.db2.archive.DurableHistoryMarkerRangeReceipt; +import org.tron.core.db2.archive.HistoryCommitMarker; import org.tron.core.db2.archive.OldValueCollector; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.IRevokingDB; @@ -95,6 +102,11 @@ public class SnapshotManager implements RevokingDatabase { private int checkpointVersion = 1; // default v1 private OldValueCollector oldValueCollector; + private ArchiveBlockProjectionPreparer archiveBlockProjectionPreparer; + private final Map + archiveForwardPayloadOwners = new HashMap<>(); + private FrozenBatch pendingArchiveForwardFlush; + private List sealedArchiveForwardFlush; private BlockReverseDiffSink blockReverseDiffSink; @Getter private volatile long archiveReadableEpoch = -1; @@ -253,17 +265,41 @@ public synchronized void commit(BlockSnapshotMeta meta) { } BlockReverseDiff reverseDiff = null; - if (oldValueCollector != null) { - reverseDiff = Objects.requireNonNull( - oldValueCollector.collect(BlockChangeView.capture(meta, dbs)), - "archive collector returned null"); - } + AccountAssetPreparedBlockPayloadOwner forwardOwner = null; + PreparedBlockProjection projection = null; + boolean forwardOwnerAttached = false; + try { + if (archiveBlockProjectionPreparer != null) { + if (archiveForwardPayloadOwners.containsKey(meta)) { + throw new IllegalStateException("Archive forward payload owner already exists: " + meta); + } + BlockChangeView view = BlockChangeView.capture(meta, dbs); + projection = Objects.requireNonNull( + archiveBlockProjectionPreparer.prepare(view), + "archive projection preparer returned null"); + forwardOwner = new AccountAssetPreparedBlockPayloadOwner(meta); + forwardOwner.attach(projection); + forwardOwnerAttached = true; + reverseDiff = forwardOwner.getReverseDiff(); + } else if (oldValueCollector != null) { + reverseDiff = Objects.requireNonNull( + oldValueCollector.collect(BlockChangeView.capture(meta, dbs)), + "archive collector returned null"); + } - dbs.forEach(db -> { - if (db.getHead().isOptimized()) { - db.getHead().reloadToMem(); + dbs.forEach(db -> { + if (db.getHead().isOptimized()) { + db.getHead().reloadToMem(); + } + }); + } catch (RuntimeException | Error failure) { + if (forwardOwnerAttached) { + forwardOwner.discard(); + } else if (projection != null) { + projection.abort(); } - }); + throw failure; + } // All fallible work is complete. From here the prepared payload is owned by the block layer; // fastPop/reorg can discard it without touching durable archive state. @@ -271,6 +307,9 @@ public synchronized void commit(BlockSnapshotMeta meta) { ((SnapshotImpl) db.getHead()).attachArchiveBlock(meta, ArchiveStoreScope.isStateDatabase(db.getDbName()) ? reverseDiff : null); } + if (forwardOwner != null) { + archiveForwardPayloadOwners.put(meta, forwardOwner); + } --activeSession; } @@ -313,6 +352,154 @@ public synchronized void installArchiveCollector(OldValueCollector collector, blockReverseDiffSink = Objects.requireNonNull(sink, "sink"); } + /** Enables shared reverse/forward preparation; absent by default and installed independently. */ + public synchronized void installArchiveProjectionPreparer( + ArchiveBlockProjectionPreparer preparer) { + ArchiveStoreScope.validate(dbs); + if (oldValueCollector == null || blockReverseDiffSink == null) { + throw new IllegalStateException("Archive collector must be installed before its preparer"); + } + archiveBlockProjectionPreparer = Objects.requireNonNull(preparer, "preparer"); + } + + /** Visible for lifecycle verification until the flush freeze coordinator consumes this registry. */ + public synchronized int getArchiveForwardPayloadOwnerCount() { + return archiveForwardPayloadOwners.size(); + } + + /** Visible for lifecycle verification until the flush freeze coordinator consumes this registry. */ + public synchronized boolean hasArchiveForwardPayloadOwner(BlockSnapshotMeta meta) { + return archiveForwardPayloadOwners.containsKey(Objects.requireNonNull(meta, "meta")); + } + + /** Atomically transfers the exact oldest flush range from the registry to one pending owner. */ + public synchronized FrozenBatch freezeArchiveForwardFlushRange() { + if (pendingArchiveForwardFlush != null) { + return pendingArchiveForwardFlush; + } + if (sealedArchiveForwardFlush != null) { + throw new IllegalStateException("Archive forward flush is sealed and awaiting claim"); + } + if (archiveBlockProjectionPreparer == null) { + throw new IllegalStateException("Archive projection preparer is not installed"); + } + if (flushCount <= 0 || flushCount > size) { + throw new IllegalStateException("Archive forward flush range is empty or exceeds topology"); + } + + List topology = stateLayerMetas(); + if (topology.size() != size) { + throw new IllegalStateException("Archive state topology size mismatch"); + } + java.util.Set unique = new java.util.LinkedHashSet<>(topology); + if (unique.size() != topology.size()) { + throw new IllegalStateException("Archive state topology contains duplicate block metadata"); + } + BlockSnapshotMeta previous = null; + for (BlockSnapshotMeta current : topology) { + if (previous != null && (current.getEpoch() != previous.getEpoch() + 1 + || current.getBlockNumber() != previous.getBlockNumber() + 1 + || !Arrays.equals(current.getParentHash(), previous.getBlockHash()))) { + throw new IllegalStateException("Archive state topology is not contiguous"); + } + previous = current; + } + if (!archiveForwardPayloadOwners.keySet().equals(unique)) { + throw new IllegalStateException("Archive forward owner registry does not match topology"); + } + for (BlockSnapshotMeta meta : topology) { + AccountAssetPreparedBlockPayloadOwner owner = archiveForwardPayloadOwners.get(meta); + if (owner == null || !owner.isAttachedTo(meta)) { + throw new IllegalStateException("Archive forward owner is missing or not attached"); + } + } + + List range = new ArrayList<>(topology.subList(0, flushCount)); + List owners = new ArrayList<>(range.size()); + for (BlockSnapshotMeta meta : range) { + owners.add(archiveForwardPayloadOwners.get(meta)); + } + + FrozenBatch frozen = AccountAssetPreparedBlockPayloadOwner.freezeContiguous(owners); + for (BlockSnapshotMeta meta : range) { + archiveForwardPayloadOwners.remove(meta); + } + pendingArchiveForwardFlush = frozen; + return frozen; + } + + public synchronized boolean hasPendingArchiveForwardFlush() { + return pendingArchiveForwardFlush != null || sealedArchiveForwardFlush != null; + } + + /** Seals the pending range with an externally validated exact marker list. */ + public synchronized void sealPendingArchiveForwardFlush(List markers) { + FrozenBatch pending = requirePendingArchiveForwardFlush(); + List sealed = pending.seal(markers); + sealedArchiveForwardFlush = sealed; + pendingArchiveForwardFlush = null; + } + + /** Reads an exact durable marker receipt and seals the same pending range. */ + public synchronized void sealPendingArchiveForwardFlush( + DurableHistoryMarkerRangeReceipt receipt) { + FrozenBatch pending = requirePendingArchiveForwardFlush(); + List sealed = Objects.requireNonNull(receipt, "receipt") + .seal(pending); + sealedArchiveForwardFlush = sealed; + pendingArchiveForwardFlush = null; + } + + /** Transfers the sealed ordered payloads exactly once and clears manager ownership. */ + public synchronized List claimArchiveForwardFlushPayloads() { + if (sealedArchiveForwardFlush == null) { + throw new IllegalStateException("Archive forward flush is not sealed"); + } + List claimed = sealedArchiveForwardFlush; + sealedArchiveForwardFlush = null; + return claimed; + } + + private FrozenBatch requirePendingArchiveForwardFlush() { + if (sealedArchiveForwardFlush != null) { + throw new IllegalStateException("Archive forward flush is already sealed"); + } + if (pendingArchiveForwardFlush == null) { + throw new IllegalStateException("Archive forward flush range is not frozen"); + } + return pendingArchiveForwardFlush; + } + + private List stateLayerMetas() { + List reference = null; + for (Chainbase db : dbs) { + if (!ArchiveStoreScope.isStateDatabase(db.getDbName())) { + continue; + } + List candidate = new ArrayList<>(size); + Snapshot next = db.getHead().getRoot(); + for (int i = 0; i < size; i++) { + next = next.getNext(); + if (!Snapshot.isImpl(next)) { + throw new IllegalStateException("Archive state topology is missing a snapshot layer"); + } + BlockSnapshotMeta meta = ((SnapshotImpl) next).getBlockSnapshotMeta(); + if (meta == null) { + throw new IllegalStateException("Archive state topology contains an unbound layer"); + } + candidate.add(meta); + } + if (reference != null && !reference.equals(candidate)) { + throw new IllegalStateException("Block metadata differs across state database topology"); + } + reference = candidate; + } + if (reference == null) { + throw new IllegalStateException("Archive mode has no state database topology"); + } + return reference; + } + /** Runs latest-state snapshot acquisition inside the canonical apply/flush monitor. */ public synchronized void withArchiveStateBarrier(ArchiveStateAction action) throws IOException { Objects.requireNonNull(action, "action").run(); @@ -343,10 +530,59 @@ public synchronized void pop() { } @Override - public void fastPop() { + public synchronized void fastPop() { + if (activeSession != 0) { + throw new RevokingStoreIllegalStateException( + String.format("activeSession has to be equal 0, current %d", activeSession)); + } + if (size <= 0) { + throw new RevokingStoreIllegalStateException( + String.format("there is not snapshot to be popped, current: %d", size)); + } + BlockSnapshotMeta poppedMeta = currentStateHeadBlockMeta(); + if ((pendingArchiveForwardFlush != null && pendingArchiveForwardFlush.contains(poppedMeta)) + || sealedArchiveForwardFlushContains(poppedMeta)) { + throw new IllegalStateException("Cannot pop a block owned by pending archive flush"); + } + if (poppedMeta != null) { + AccountAssetPreparedBlockPayloadOwner owner = archiveForwardPayloadOwners.get(poppedMeta); + if (owner != null) { + owner.discard(); + archiveForwardPayloadOwners.remove(poppedMeta); + } + } pop(); } + private BlockSnapshotMeta currentStateHeadBlockMeta() { + BlockSnapshotMeta current = null; + for (Chainbase db : dbs) { + if (!ArchiveStoreScope.isStateDatabase(db.getDbName()) || !Snapshot.isImpl(db.getHead())) { + continue; + } + BlockSnapshotMeta candidate = ((SnapshotImpl) db.getHead()).getBlockSnapshotMeta(); + if (candidate != null && current != null && !current.equals(candidate)) { + throw new IllegalStateException("Current block metadata differs across state databases"); + } + if (candidate != null) { + current = candidate; + } + } + return current; + } + + private boolean sealedArchiveForwardFlushContains(BlockSnapshotMeta meta) { + if (sealedArchiveForwardFlush == null || meta == null) { + return false; + } + for (ArchiveBlockForwardPayload payload : sealedArchiveForwardFlush) { + if (meta.equals(payload.getMeta())) { + return true; + } + } + return false; + } + public synchronized void enable() { disabled = false; } @@ -371,6 +607,7 @@ public synchronized void disable() { @Override public void shutdown() { + abortArchiveForwardPayloads(); ExecutorServiceManager.shutdownAndAwaitTermination(pruneCheckpointThread, pruneName); flushServices.forEach((key, value) -> ExecutorServiceManager.shutdownAndAwaitTermination(value, "flush-service-" + key)); @@ -383,6 +620,22 @@ public void shutdown() { } } + private synchronized void abortArchiveForwardPayloads() { + if (pendingArchiveForwardFlush != null) { + pendingArchiveForwardFlush.abortIfFrozen(); + pendingArchiveForwardFlush = null; + } + sealedArchiveForwardFlush = null; + for (Map.Entry entry + : archiveForwardPayloadOwners.entrySet()) { + AccountAssetPreparedBlockPayloadOwner owner = entry.getValue(); + if (owner != null && owner.isAttachedTo(entry.getKey())) { + owner.discard(); + } + } + archiveForwardPayloadOwners.clear(); + } + public void updateSolidity(int hops) { for (int i = 0; i < hops; i++) { for (Chainbase db : dbs) { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index f4dd479c159..916ecfffa36 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -644,11 +644,11 @@ private void initStateArchive() { } AsyncArchiveHistorySink sink = new AsyncArchiveHistorySink(writer, storage.getStateArchiveQueueCapacity()); - AccountAssetArchiveProjector projector = new AccountAssetArchiveProjector( - chainBaseManager.getAccountAssetStore(), - () -> getDynamicPropertiesStore().supportAllowAccountAssetOptimization()); + AccountAssetArchiveProjector projector = new AccountAssetArchiveProjector(); ((SnapshotManager) revokingStore).installArchiveCollector( - new SnapshotOldValueCollector(projector), sink); + new SnapshotOldValueCollector(projector, + accountKey -> chainBaseManager.getAccountAssetStore().prefixQuery(accountKey), + () -> getDynamicPropertiesStore().supportAllowAccountAssetOptimization()), sink); archiveHistoryWriter = writer; if (archiveHead != null) { ((SnapshotManager) revokingStore).markArchiveReadableThrough(archiveHead.getEpoch()); From cece7542b45bc2e16ea8761ed93344b4198a9a5d Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 19:02:46 +0800 Subject: [PATCH 018/161] refactor(chainbase): exclude abi from archive state Classify ABI as mutable auxiliary metadata outside the versioned state participant set while retaining an explicit tombstone classification. --- .../tron/core/db2/archive/ArchiveStoreScope.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java index 76d6ad8e4f9..f5ebe0f18a8 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java @@ -12,7 +12,6 @@ public final class ArchiveStoreScope { private static final Set STATE_DATABASES = immutableSet( - "abi", "accountid-index", "account-index", "account", @@ -40,6 +39,9 @@ public final class ArchiveStoreScope { "nullifier", "IncrementalMerkleTree"); + // Candidate store ID 1 is reserved for abi and must never be reused by state history. + private static final Set EXCLUDED_DATABASES = immutableSet("abi"); + private static final Set NON_STATE_DATABASES = immutableSet( "account-trace", "accountTrie", @@ -63,7 +65,8 @@ public static boolean isStateDatabase(String dbName) { } public static boolean isClassified(String dbName) { - return STATE_DATABASES.contains(dbName) || NON_STATE_DATABASES.contains(dbName); + return STATE_DATABASES.contains(dbName) || NON_STATE_DATABASES.contains(dbName) + || EXCLUDED_DATABASES.contains(dbName); } public static Set getStateDatabases() { @@ -74,6 +77,14 @@ public static Set getNonStateDatabases() { return NON_STATE_DATABASES; } + public static boolean isExcludedDatabase(String dbName) { + return EXCLUDED_DATABASES.contains(dbName); + } + + public static Set getExcludedDatabases() { + return EXCLUDED_DATABASES; + } + public static void validate(Collection databases) { Set duplicates = databases.stream() .collect(Collectors.groupingBy(Chainbase::getDbName, Collectors.counting())) From a6cbf9b744a92a6c81068f49a92bcc1dbaa03810 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 19:03:04 +0800 Subject: [PATCH 019/161] test(chainbase): cover archive projection ownership Exercise proposal-bound projection, layer-to-flush ownership, durable marker sealing, retry and reorg boundaries, exact participant coverage, and ABI exclusion. --- ...AccountAssetBlockProjectionBridgeTest.java | 808 ++++++++++++++++++ ...chiveBlockForwardMutationRecoveryTest.java | 109 +++ ...ParticipantMutationBatchCollectorTest.java | 26 +- .../DurableHistoryMarkerRangeReceiptTest.java | 196 +++++ .../SnapshotOldValueCollectorTest.java | 688 ++++++++++++++- 5 files changed, 1800 insertions(+), 27 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceiptTest.java diff --git a/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java b/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java new file mode 100644 index 00000000000..708ebc6077e --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java @@ -0,0 +1,808 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.primitives.Longs; +import com.google.protobuf.ByteString; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.tron.common.BaseMethodTest; +import org.tron.common.utils.ByteArray; +import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.PreparedBlockProjection; +import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.TargetAssetOptimization; +import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; +import org.tron.core.db2.common.DB; +import org.tron.core.db2.common.Flusher; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.core.SnapshotRoot; +import org.tron.core.store.AccountAssetStore; +import org.tron.protos.Protocol.Account; + +public class AccountAssetBlockProjectionBridgeTest extends BaseMethodTest { + + @Test + public void sharesExactAccountProjectionAcrossDeterministicReverseAndForwardBuilders() { + BlockSnapshotMeta meta = meta(1); + HistoryCommitMarker marker = marker(meta); + byte[] updateKey = bytes(2, 1); + byte[] deleteKey = bytes(2, 2); + byte[] updateAsset = assetKey(updateKey, "1000001"); + byte[] deleteAsset = assetKey(deleteKey, "1000002"); + Account oldUpdate = optimizedAccount(updateKey); + Account oldDelete = optimizedAccount(deleteKey); + Account postUpdate = oldUpdate.toBuilder().putAssetV2("1000001", 80L).build(); + AccountAssetStore assetStore = mock(AccountAssetStore.class); + when(assetStore.prefixQuery(any(byte[].class))).thenAnswer(invocation -> { + byte[] accountKey = invocation.getArgument(0); + Map assets = new LinkedHashMap<>(); + if (Arrays.equals(accountKey, updateKey)) { + assets.put(WrappedByteArray.copyOf(updateAsset), Longs.toByteArray(100L)); + } else if (Arrays.equals(accountKey, deleteKey)) { + assets.put(WrappedByteArray.copyOf(deleteAsset), Longs.toByteArray(200L)); + } + return assets; + }); + AccountAssetBlockProjectionBridge bridge = bridge(assetStore); + + try (Fixture fixture = new Fixture(participants())) { + fixture.rootPut("account", updateKey, oldUpdate.toByteArray()); + fixture.rootPut("account", deleteKey, oldDelete.toByteArray()); + BlockChangeView view = fixture.capture(meta, databases -> { + databases.get("account").put(updateKey, postUpdate.toByteArray()); + databases.get("account").delete(deleteKey); + }); + + PreparedBlockProjection first = bridge.prepare(view, + TargetAssetOptimization.forTarget(meta, true)); + verify(assetStore, times(2)).prefixQuery(any(byte[].class)); + BlockReverseDiff.DbGroup reverseAssets = group(first.getReverseDiff(), "account-asset"); + assertEquals(2, reverseAssets.getEntries().size()); + assertArrayEquals(Longs.toByteArray(100L), + reverseAssets.getEntries().get(0).getOldValue().getValue()); + assertArrayEquals(Longs.toByteArray(200L), + reverseAssets.getEntries().get(1).getOldValue().getValue()); + + ArchiveTargetMutationPlan firstPlan = plan(marker, view, first.seal(marker)); + assertArrayEquals(Longs.toByteArray(80L), + firstPlan.getMutations("account-asset").get(0).getValue()); + assertNull(firstPlan.getMutations("account-asset").get(1).getValue()); + + PreparedBlockProjection retry = bridge.prepare(view, + TargetAssetOptimization.forTarget(meta, true)); + verify(assetStore, times(4)).prefixQuery(any(byte[].class)); + ArchiveTargetMutationPlan retryPlan = plan(marker, view, retry.seal(marker)); + assertArrayEquals(new BlockHistoryCodec().encode(first.getReverseDiff()), + new BlockHistoryCodec().encode(retry.getReverseDiff())); + assertArrayEquals(firstPlan.digest(), retryPlan.digest()); + } + } + + @Test + public void rejectsActivationIdentityAndCoverageBeforeAnyPhysicalRead() { + BlockSnapshotMeta meta = meta(2); + HistoryCommitMarker marker = marker(meta); + byte[] accountKey = bytes(2, 3); + Account old = optimizedAccount(accountKey); + AccountAssetStore assetStore = mock(AccountAssetStore.class); + AccountAssetBlockProjectionBridge bridge = bridge(assetStore); + + try (Fixture exact = new Fixture(participants())) { + exact.rootPut("account", accountKey, old.toByteArray()); + BlockChangeView view = exact.capture(meta, + databases -> databases.get("account").delete(accountKey)); + assertThrows(ArchivePersistenceException.class, + () -> bridge.prepare(view, + TargetAssetOptimization.forTarget(meta(3), true))); + } + try (Fixture incomplete = new Fixture(Collections.singletonList("account"))) { + incomplete.rootPut("account", accountKey, old.toByteArray()); + BlockChangeView view = incomplete.capture(meta, + databases -> databases.get("account").delete(accountKey)); + assertThrows(ArchivePersistenceException.class, + () -> bridge.prepare(view, + TargetAssetOptimization.forTarget(meta, true))); + } + try (Fixture duplicateSource = new Fixture(participants())) { + duplicateSource.rootPut("account", accountKey, old.toByteArray()); + BlockChangeView view = duplicateSource.capture(meta, databases -> { + databases.get("account").delete(accountKey); + databases.get("account-asset").put(assetKey(accountKey, "1000004"), + Longs.toByteArray(40L)); + }); + assertThrows(ArchivePersistenceException.class, + () -> bridge.prepare(view, + TargetAssetOptimization.forTarget(meta, true))); + } + verify(assetStore, never()).prefixQuery(any(byte[].class)); + } + + @Test + public void projectionFailurePublishesNoPartialResultAndAllowsFreshRetry() { + BlockSnapshotMeta meta = meta(4); + HistoryCommitMarker marker = marker(meta); + byte[] validKey = bytes(2, 4); + byte[] invalidKey = bytes(2, 5); + Account validOld = optimizedAccount(validKey); + Account validPost = validOld.toBuilder().putAssetV2("1000003", 30L).build(); + AccountAssetStore assetStore = mock(AccountAssetStore.class); + when(assetStore.prefixQuery(any(byte[].class))).thenReturn(Collections.emptyMap()); + AccountAssetBlockProjectionBridge bridge = bridge(assetStore); + + try (Fixture fixture = new Fixture(participants())) { + fixture.rootPut("account", validKey, validOld.toByteArray()); + BlockChangeView failing = fixture.capture(meta, databases -> { + databases.get("account").put(validKey, validPost.toByteArray()); + databases.get("account").put(invalidKey, bytes(3, 99)); + }); + assertThrows(IllegalStateException.class, + () -> bridge.prepare(failing, + TargetAssetOptimization.forTarget(meta, true))); + + BlockChangeView retry = fixture.capture(meta, + databases -> databases.get("account").put(validKey, validPost.toByteArray())); + PreparedBlockProjection result = bridge.prepare(retry, + TargetAssetOptimization.forTarget(meta, true)); + assertEquals(meta, result.getReverseDiff().getMeta()); + assertEquals(1, plan(marker, retry, result.seal(marker)) + .getMutations("account").size()); + } + verify(assetStore, times(2)).prefixQuery(any(byte[].class)); + } + + @Test + public void physicalInputFailurePublishesNothingAndFreshPrepareCanRetry() { + BlockSnapshotMeta meta = meta(24); + byte[] accountKey = bytes(2, 24); + Account old = optimizedAccount(accountKey); + boolean[] fail = {true}; + AccountAssetOldPhysicalAssetsSource source = key -> { + if (fail[0]) { + throw new IllegalStateException("injected physical input failure"); + } + return Collections.emptyMap(); + }; + AccountAssetBlockProjectionBridge bridge = new AccountAssetBlockProjectionBridge( + new AccountAssetArchiveProjector(), source); + + try (Fixture fixture = new Fixture(participants())) { + fixture.rootPut("account", accountKey, old.toByteArray()); + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").delete(accountKey)); + TargetAssetOptimization activation = TargetAssetOptimization.forTarget(meta, true); + + ArchivePersistenceException failure = assertThrows(ArchivePersistenceException.class, + () -> bridge.prepare(view, activation)); + assertTrue(failure.getMessage().contains("old physical AccountAsset input")); + + fail[0] = false; + PreparedBlockProjection prepared = bridge.prepare(view, activation); + assertEquals(meta, prepared.getReverseDiff().getMeta()); + prepared.abort(); + } + } + + @Test + public void resolvesActivationBlockFromProposalSixtySixAndFeedsSharedBridge() { + BlockSnapshotMeta meta = meta(5); + HistoryCommitMarker marker = marker(meta); + byte[] accountKey = bytes(2, 6); + byte[] physicalAsset = assetKey(accountKey, "1000005"); + Account old = optimizedAccount(accountKey); + Account post = old.toBuilder().putAssetV2("1000005", 50L).build(); + AccountAssetStore assetStore = mock(AccountAssetStore.class); + Map assets = new LinkedHashMap<>(); + assets.put(WrappedByteArray.copyOf(physicalAsset), Longs.toByteArray(40L)); + when(assetStore.prefixQuery(any(byte[].class))).thenReturn(assets); + AccountAssetBlockProjectionBridge bridge = bridge(assetStore); + AccountAssetTargetActivationResolver resolver = + new AccountAssetTargetActivationResolver(); + + try (Fixture fixture = new Fixture(participants())) { + fixture.rootPut("account", accountKey, old.toByteArray()); + fixture.rootPut("properties", proposal66Key(), ByteArray.fromLong(0L)); + BlockChangeView view = fixture.capture(meta, databases -> { + databases.get("properties").put(proposal66Key(), ByteArray.fromLong(1L)); + databases.get("properties").put(proposal53Key(), ByteArray.fromLong(0L)); + databases.get("account").put(accountKey, post.toByteArray()); + }); + + TargetAssetOptimization activation = resolver.resolve(meta, view); + PreparedBlockProjection result = bridge.prepare(view, activation); + assertTrue(activation.isEnabled()); + assertArrayEquals(Longs.toByteArray(50L), + plan(marker, view, result.seal(marker)) + .getMutations("account-asset").get(0).getValue()); + } + verify(assetStore, times(1)).prefixQuery(any(byte[].class)); + } + + @Test + public void inheritsUnchangedProposalSixtySixWithoutUsingProposalFiftyThree() { + BlockSnapshotMeta meta = meta(6); + HistoryCommitMarker marker = marker(meta); + byte[] accountKey = bytes(2, 7); + Account raw = Account.newBuilder() + .setAddress(ByteString.copyFrom(accountKey)) + .putAssetV2("1000006", 60L) + .build(); + AccountAssetStore assetStore = mock(AccountAssetStore.class); + AccountAssetBlockProjectionBridge bridge = bridge(assetStore); + AccountAssetTargetActivationResolver resolver = + new AccountAssetTargetActivationResolver(); + + try (Fixture fixture = new Fixture(participants())) { + fixture.rootPut("properties", proposal66Key(), ByteArray.fromLong(0L)); + fixture.rootPut("properties", proposal53Key(), ByteArray.fromLong(1L)); + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").put(accountKey, raw.toByteArray())); + + TargetAssetOptimization activation = resolver.resolve(meta, view); + PreparedBlockProjection result = bridge.prepare(view, activation); + assertFalse(activation.isEnabled()); + ArchiveTargetMutationPlan plan = plan(marker, view, result.seal(marker)); + assertArrayEquals(raw.toByteArray(), plan.getMutations("account").get(0).getValue()); + assertEquals(0, plan.getMutations("account-asset").size()); + } + verify(assetStore, never()).prefixQuery(any(byte[].class)); + } + + @Test + public void rejectsMissingCorruptSubstitutedAndReorgActivationBeforePrefix() { + BlockSnapshotMeta meta = meta(7); + HistoryCommitMarker marker = marker(meta); + byte[] accountKey = bytes(2, 8); + Account old = optimizedAccount(accountKey); + AccountAssetStore assetStore = mock(AccountAssetStore.class); + AccountAssetBlockProjectionBridge bridge = bridge(assetStore); + AccountAssetTargetActivationResolver resolver = + new AccountAssetTargetActivationResolver(); + + try (Fixture missing = new Fixture(participants())) { + missing.rootPut("account", accountKey, old.toByteArray()); + missing.rootPut("properties", proposal53Key(), ByteArray.fromLong(1L)); + BlockChangeView view = missing.capture(meta, + databases -> databases.get("account").delete(accountKey)); + assertThrows(ArchivePersistenceException.class, + () -> bridge.prepare(view, resolver.resolve(meta, view))); + } + try (Fixture corrupt = new Fixture(participants())) { + corrupt.rootPut("account", accountKey, old.toByteArray()); + corrupt.rootPut("properties", proposal66Key(), new byte[] {1}); + BlockChangeView view = corrupt.capture(meta, + databases -> databases.get("account").delete(accountKey)); + assertThrows(ArchivePersistenceException.class, + () -> bridge.prepare(view, resolver.resolve(meta, view))); + } + try (Fixture noncanonical = new Fixture(participants())) { + noncanonical.rootPut("account", accountKey, old.toByteArray()); + noncanonical.rootPut("properties", proposal66Key(), ByteArray.fromLong(2L)); + BlockChangeView view = noncanonical.capture(meta, + databases -> databases.get("account").delete(accountKey)); + assertThrows(ArchivePersistenceException.class, + () -> bridge.prepare(view, resolver.resolve(meta, view))); + } + try (Fixture deleted = new Fixture(participants())) { + deleted.rootPut("account", accountKey, old.toByteArray()); + deleted.rootPut("properties", proposal66Key(), ByteArray.fromLong(1L)); + BlockChangeView view = deleted.capture(meta, databases -> { + databases.get("properties").delete(proposal66Key()); + databases.get("account").delete(accountKey); + }); + assertThrows(ArchivePersistenceException.class, + () -> bridge.prepare(view, resolver.resolve(meta, view))); + } + try (Fixture reorg = new Fixture(participants())) { + reorg.rootPut("account", accountKey, old.toByteArray()); + reorg.rootPut("properties", proposal66Key(), ByteArray.fromLong(1L)); + BlockChangeView view = reorg.capture(meta, + databases -> databases.get("account").delete(accountKey)); + assertThrows(ArchivePersistenceException.class, + () -> bridge.prepare(view, resolver.resolve(meta(8), view))); + } + verify(assetStore, never()).prefixQuery(any(byte[].class)); + } + + @Test + public void rejectsWrongMarkerWithoutConsumingPreparedProjectionAndSealsExactlyOnce() { + BlockSnapshotMeta meta = meta(9); + HistoryCommitMarker marker = marker(meta); + byte[] accountKey = bytes(2, 9); + Account account = optimizedAccount(accountKey); + AccountAssetStore assetStore = mock(AccountAssetStore.class); + when(assetStore.prefixQuery(any(byte[].class))).thenReturn(Collections.emptyMap()); + AccountAssetBlockProjectionBridge bridge = bridge(assetStore); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView view = fixture.capture(meta, + databases -> databases.get("account").put(accountKey, account.toByteArray())); + PreparedBlockProjection prepared = bridge.prepare(view, + TargetAssetOptimization.forTarget(meta, true)); + + assertThrows(ArchivePersistenceException.class, + () -> prepared.seal(marker(meta(10)))); + assertThrows(ArchivePersistenceException.class, + () -> prepared.seal(marker(meta, Collections.singletonList("account")))); + assertEquals(meta, prepared.getReverseDiff().getMeta()); + assertTrue(prepared.retainsCapturedView()); + + AccountAssetForwardMutationManifest manifest = prepared.seal(marker); + assertEquals(1, plan(marker, view, manifest).getMutations("account").size()); + assertFalse(prepared.retainsCapturedView()); + assertThrows(ArchivePersistenceException.class, () -> prepared.seal(marker)); + assertThrows(ArchivePersistenceException.class, prepared::abort); + } + } + + @Test + public void sealsEmptyBlockAndAbortReleasesPreparedPayload() { + BlockSnapshotMeta meta = meta(11); + HistoryCommitMarker marker = marker(meta); + AccountAssetStore assetStore = mock(AccountAssetStore.class); + AccountAssetBlockProjectionBridge bridge = bridge(assetStore); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView view = fixture.capture(meta, databases -> { }); + TargetAssetOptimization activation = TargetAssetOptimization.forTarget(meta, false); + PreparedBlockProjection sealed = bridge.prepare(view, activation); + assertTrue(sealed.getReverseDiff().getGroups().isEmpty()); + assertTrue(plan(marker, view, sealed.seal(marker)).getMutations().values().stream() + .allMatch(List::isEmpty)); + + PreparedBlockProjection aborted = bridge.prepare(view, activation); + assertTrue(aborted.retainsCapturedView()); + aborted.abort(); + assertFalse(aborted.retainsCapturedView()); + assertThrows(ArchivePersistenceException.class, aborted::getReverseDiff); + assertThrows(ArchivePersistenceException.class, () -> aborted.seal(marker)); + assertThrows(ArchivePersistenceException.class, aborted::abort); + } + verify(assetStore, never()).prefixQuery(any(byte[].class)); + } + + @Test + public void layerOwnersTransferContiguousPayloadsAndBatchSealExactlyOnce() { + BlockSnapshotMeta firstMeta = meta(12); + BlockSnapshotMeta secondMeta = meta(13); + HistoryCommitMarker firstMarker = marker(firstMeta); + HistoryCommitMarker secondMarker = marker(secondMeta); + AccountAssetBlockProjectionBridge bridge = emptyBridge(); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView firstView = fixture.capture(firstMeta, databases -> { }); + BlockChangeView secondView = fixture.capture(secondMeta, databases -> { }); + PreparedBlockProjection first = bridge.prepare(firstView, + TargetAssetOptimization.forTarget(firstMeta, false)); + PreparedBlockProjection second = bridge.prepare(secondView, + TargetAssetOptimization.forTarget(secondMeta, false)); + AccountAssetPreparedBlockPayloadOwner firstOwner = + new AccountAssetPreparedBlockPayloadOwner(firstMeta); + AccountAssetPreparedBlockPayloadOwner secondOwner = + new AccountAssetPreparedBlockPayloadOwner(secondMeta); + firstOwner.attach(first); + secondOwner.attach(second); + assertEquals(firstMeta, firstOwner.getReverseDiff().getMeta()); + + FrozenBatch batch = AccountAssetPreparedBlockPayloadOwner.freezeContiguous( + Arrays.asList(firstOwner, secondOwner)); + assertThrows(ArchivePersistenceException.class, firstOwner::getReverseDiff); + assertThrows(ArchivePersistenceException.class, firstOwner::discard); + assertThrows(ArchivePersistenceException.class, + () -> AccountAssetPreparedBlockPayloadOwner.freezeContiguous( + Collections.singletonList(secondOwner))); + + assertThrows(ArchivePersistenceException.class, + () -> batch.seal(Arrays.asList(firstMarker, marker(meta(14))))); + assertTrue(first.retainsCapturedView()); + assertTrue(second.retainsCapturedView()); + List payloads = batch.seal( + Arrays.asList(firstMarker, secondMarker)); + assertEquals(2, payloads.size()); + assertEquals(firstMeta, payloads.get(0).getMeta()); + assertSame(firstView, payloads.get(0).getView()); + assertEquals(secondMeta, payloads.get(1).getMeta()); + assertSame(secondView, payloads.get(1).getView()); + assertTrue(plan(firstMarker, payloads.get(0).getView(), + payloads.get(0).getAccountAssetManifest()).getMutations().values().stream() + .allMatch(List::isEmpty)); + assertFalse(first.retainsCapturedView()); + assertFalse(second.retainsCapturedView()); + assertThrows(ArchivePersistenceException.class, + () -> batch.seal(Arrays.asList(firstMarker, secondMarker))); + assertThrows(ArchivePersistenceException.class, batch::abort); + } + } + + @Test + public void sealedForwardPayloadCarriesExactViewAndRejectsMixedIdentity() { + BlockSnapshotMeta firstMeta = meta(14); + BlockSnapshotMeta secondMeta = meta(15); + AccountAssetBlockProjectionBridge bridge = emptyBridge(); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView firstView = fixture.capture(firstMeta, databases -> { }); + BlockChangeView secondView = fixture.capture(secondMeta, databases -> { }); + PreparedBlockProjection prepared = bridge.prepare(firstView, + TargetAssetOptimization.forTarget(firstMeta, false)); + + ArchiveBlockForwardPayload payload = prepared.sealPayload(marker(firstMeta)); + assertEquals(firstMeta, payload.getMeta()); + assertSame(firstView, payload.getView()); + assertFalse(prepared.retainsCapturedView()); + assertThrows(ArchivePersistenceException.class, + () -> prepared.sealPayload(marker(firstMeta))); + + AccountAssetForwardMutationManifest manifest = + new AccountAssetForwardMutationManifest(marker(firstMeta), Collections.emptyList()); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveBlockForwardPayload(marker(firstMeta), secondView, manifest)); + } + } + + @Test + public void layerOwnerRejectsWrongTargetAndDoubleAttachWithoutConsumingCallerPayload() { + BlockSnapshotMeta firstMeta = meta(15); + BlockSnapshotMeta secondMeta = meta(16); + AccountAssetBlockProjectionBridge bridge = emptyBridge(); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView firstView = fixture.capture(firstMeta, databases -> { }); + BlockChangeView secondView = fixture.capture(secondMeta, databases -> { }); + PreparedBlockProjection first = bridge.prepare(firstView, + TargetAssetOptimization.forTarget(firstMeta, false)); + PreparedBlockProjection second = bridge.prepare(secondView, + TargetAssetOptimization.forTarget(secondMeta, false)); + PreparedBlockProjection sealed = bridge.prepare(firstView, + TargetAssetOptimization.forTarget(firstMeta, false)); + sealed.seal(marker(firstMeta)); + PreparedBlockProjection aborted = bridge.prepare(firstView, + TargetAssetOptimization.forTarget(firstMeta, false)); + aborted.abort(); + AccountAssetPreparedBlockPayloadOwner owner = + new AccountAssetPreparedBlockPayloadOwner(firstMeta); + + assertThrows(ArchivePersistenceException.class, () -> owner.attach(second)); + assertEquals(secondMeta, second.getReverseDiff().getMeta()); + assertThrows(ArchivePersistenceException.class, () -> owner.attach(sealed)); + assertThrows(ArchivePersistenceException.class, () -> owner.attach(aborted)); + owner.attach(first); + assertThrows(ArchivePersistenceException.class, () -> owner.attach(second)); + assertEquals(secondMeta, second.getReverseDiff().getMeta()); + owner.discard(); + second.abort(); + } + } + + @Test + public void fastPopDiscardAndFrozenShutdownAbortReleaseEveryPayload() { + BlockSnapshotMeta firstMeta = meta(17); + BlockSnapshotMeta secondMeta = meta(18); + AccountAssetBlockProjectionBridge bridge = emptyBridge(); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView firstView = fixture.capture(firstMeta, databases -> { }); + BlockChangeView secondView = fixture.capture(secondMeta, databases -> { }); + PreparedBlockProjection discarded = bridge.prepare(firstView, + TargetAssetOptimization.forTarget(firstMeta, false)); + AccountAssetPreparedBlockPayloadOwner discardedOwner = + new AccountAssetPreparedBlockPayloadOwner(firstMeta); + discardedOwner.attach(discarded); + discardedOwner.discard(); + assertFalse(discarded.retainsCapturedView()); + assertThrows(ArchivePersistenceException.class, discarded::getReverseDiff); + assertThrows(ArchivePersistenceException.class, discardedOwner::discard); + + PreparedBlockProjection frozen = bridge.prepare(secondView, + TargetAssetOptimization.forTarget(secondMeta, false)); + AccountAssetPreparedBlockPayloadOwner frozenOwner = + new AccountAssetPreparedBlockPayloadOwner(secondMeta); + frozenOwner.attach(frozen); + FrozenBatch batch = AccountAssetPreparedBlockPayloadOwner.freezeContiguous( + Collections.singletonList(frozenOwner)); + batch.abort(); + assertFalse(frozen.retainsCapturedView()); + assertThrows(ArchivePersistenceException.class, frozen::getReverseDiff); + assertThrows(ArchivePersistenceException.class, batch::abort); + assertThrows(ArchivePersistenceException.class, + () -> batch.seal(Collections.singletonList(marker(secondMeta)))); + } + } + + @Test + public void nonContiguousAndDuplicateFreezeFailureLeavesLayerOwnersAttached() { + BlockSnapshotMeta firstMeta = meta(19); + BlockSnapshotMeta thirdMeta = meta(21); + AccountAssetBlockProjectionBridge bridge = emptyBridge(); + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView firstView = fixture.capture(firstMeta, databases -> { }); + BlockChangeView thirdView = fixture.capture(thirdMeta, databases -> { }); + AccountAssetPreparedBlockPayloadOwner firstOwner = + new AccountAssetPreparedBlockPayloadOwner(firstMeta); + AccountAssetPreparedBlockPayloadOwner thirdOwner = + new AccountAssetPreparedBlockPayloadOwner(thirdMeta); + firstOwner.attach(bridge.prepare(firstView, + TargetAssetOptimization.forTarget(firstMeta, false))); + thirdOwner.attach(bridge.prepare(thirdView, + TargetAssetOptimization.forTarget(thirdMeta, false))); + + assertThrows(ArchivePersistenceException.class, + () -> AccountAssetPreparedBlockPayloadOwner.freezeContiguous( + Arrays.asList(firstOwner, thirdOwner))); + assertThrows(ArchivePersistenceException.class, + () -> AccountAssetPreparedBlockPayloadOwner.freezeContiguous( + Arrays.asList(firstOwner, firstOwner))); + assertEquals(firstMeta, firstOwner.getReverseDiff().getMeta()); + assertEquals(thirdMeta, thirdOwner.getReverseDiff().getMeta()); + firstOwner.discard(); + thirdOwner.discard(); + } + } + + @Test + public void durableMarkerReceiptFailureLeavesFrozenBatchRetryable() { + BlockSnapshotMeta meta = meta(22); + HistoryCommitMarker marker = marker(meta); + AccountAssetBlockProjectionBridge bridge = emptyBridge(); + boolean[] substitute = {true}; + DurableHistoryMarkerRangeReceipt.Source source = + new DurableHistoryMarkerRangeReceipt.Source() { + @Override + public HistoryCommitMarker marker(long epoch) { + return substitute[0] ? AccountAssetBlockProjectionBridgeTest.marker(meta(23)) : marker; + } + + @Override + public BlockReverseDiff readCommitted(long epoch) { + return new BlockReverseDiff(meta, Collections.emptyList()); + } + }; + + try (Fixture fixture = new Fixture(participants())) { + BlockChangeView view = fixture.capture(meta, databases -> { }); + AccountAssetPreparedBlockPayloadOwner owner = + new AccountAssetPreparedBlockPayloadOwner(meta); + PreparedBlockProjection prepared = bridge.prepare(view, + TargetAssetOptimization.forTarget(meta, false)); + owner.attach(prepared); + FrozenBatch batch = AccountAssetPreparedBlockPayloadOwner.freezeContiguous( + Collections.singletonList(owner)); + DurableHistoryMarkerRangeReceipt receipt = + new DurableHistoryMarkerRangeReceipt(source, 1); + + assertThrows(ArchivePersistenceException.class, () -> receipt.seal(batch)); + assertEquals(meta, batch.getExpectedMetas().get(0)); + assertTrue(prepared.retainsCapturedView()); + substitute[0] = false; + List payloads = receipt.seal(batch); + assertEquals(1, payloads.size()); + assertSame(view, payloads.get(0).getView()); + assertFalse(prepared.retainsCapturedView()); + assertTrue(plan(payloads.get(0).getMarker(), payloads.get(0).getView(), + payloads.get(0).getAccountAssetManifest()).getMutations().values().stream() + .allMatch(List::isEmpty)); + assertThrows(ArchivePersistenceException.class, () -> receipt.seal(batch)); + } + } + + private static AccountAssetBlockProjectionBridge emptyBridge() { + return new AccountAssetBlockProjectionBridge(new AccountAssetArchiveProjector(), + accountKey -> Collections.emptyMap()); + } + + private static AccountAssetBlockProjectionBridge bridge(AccountAssetStore assetStore) { + return new AccountAssetBlockProjectionBridge(new AccountAssetArchiveProjector(), + accountKey -> assetStore.prefixQuery(accountKey)); + } + + private static ArchiveTargetMutationPlan plan(HistoryCommitMarker marker, BlockChangeView view, + AccountAssetForwardMutationManifest manifest) { + ArchiveParticipantMutationBatch batch = new ArchiveParticipantMutationBatchCollector( + manifest).collect(marker, view); + return new ArchiveTargetMutationPlanBuilder().build(marker, batch); + } + + private static BlockReverseDiff.DbGroup group(BlockReverseDiff diff, String dbName) { + return diff.getGroups().stream() + .filter(group -> dbName.equals(group.getDbName())) + .findFirst() + .orElseThrow(AssertionError::new); + } + + private static Account optimizedAccount(byte[] accountKey) { + return Account.newBuilder() + .setAddress(ByteString.copyFrom(accountKey)) + .setAssetOptimized(true) + .build(); + } + + private static byte[] assetKey(byte[] accountKey, String token) { + byte[] tokenBytes = token.getBytes(java.nio.charset.StandardCharsets.UTF_8); + byte[] key = Arrays.copyOf(accountKey, accountKey.length + tokenBytes.length); + System.arraycopy(tokenBytes, 0, key, accountKey.length, tokenBytes.length); + return key; + } + + private static byte[] proposal66Key() { + return AccountAssetTargetActivationResolver.proposal66PhysicalKey(); + } + + private static byte[] proposal53Key() { + return AccountAssetTargetActivationResolver.PROPOSAL_53_KEY.getBytes( + java.nio.charset.StandardCharsets.UTF_8); + } + + private static List participants() { + List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(participants); + return participants; + } + + private static BlockSnapshotMeta meta(int epoch) { + return BlockSnapshotMeta.forBlock(epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L); + } + + private static HistoryCommitMarker marker(BlockSnapshotMeta meta) { + return marker(meta, participants()); + } + + private static HistoryCommitMarker marker(BlockSnapshotMeta meta, + List markerParticipants) { + int epoch = (int) meta.getEpoch(); + return new HistoryCommitMarker(meta, epoch - 1, + new HistoryLocation(0, epoch * 100L, 100, epoch, bytes(32, epoch + 20)), + new HistoryIndexLocation(epoch * 50L, 50, bytes(32, epoch + 30)), + bytes(16, epoch + 40), markerParticipants); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(int length, int value) { + byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) value); + return bytes; + } + + @FunctionalInterface + private interface Mutator { + void mutate(Map databases); + } + + private static final class Fixture implements AutoCloseable { + private final SnapshotManager manager = new SnapshotManager(""); + private final Map roots = new LinkedHashMap<>(); + private final Map databases = new LinkedHashMap<>(); + private final List ordered = new ArrayList<>(); + + private Fixture(List participants) { + for (String participant : participants) { + MemoryDb root = new MemoryDb(participant); + Chainbase database = new Chainbase(new SnapshotRoot(root)); + roots.put(participant, root); + databases.put(participant, database); + ordered.add(database); + manager.add(database); + } + manager.enable(); + } + + private void rootPut(String dbName, byte[] key, byte[] value) { + roots.get(dbName).put(key, value); + } + + private BlockChangeView capture(BlockSnapshotMeta meta, Mutator mutator) { + try (ISession session = manager.buildSession()) { + mutator.mutate(databases); + return BlockChangeView.capture(meta, ordered); + } + } + + @Override + public void close() { + manager.shutdown(); + } + } + + private static final class MemoryDb implements DB, Flusher { + private final String name; + private final Map values = new LinkedHashMap<>(); + + private MemoryDb(String name) { + this.name = name; + } + + @Override + public byte[] get(byte[] key) { + byte[] value = values.get(WrappedByteArray.copyOf(key)); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + @Override + public void put(byte[] key, byte[] value) { + values.put(WrappedByteArray.copyOf(key), Arrays.copyOf(value, value.length)); + } + + @Override + public long size() { + return values.size(); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + @Override + public void remove(byte[] key) { + values.remove(WrappedByteArray.copyOf(key)); + } + + @Override + public Iterator> iterator() { + List> entries = new ArrayList<>(); + values.forEach((key, value) -> entries.add(new AbstractMap.SimpleImmutableEntry<>( + key.getBytes(), Arrays.copyOf(value, value.length)))); + return entries.iterator(); + } + + @Override + public void flush(Map rows) { + rows.forEach((key, value) -> { + if (value == null || value.getBytes() == null) { + remove(key.getBytes()); + } else { + put(key.getBytes(), value.getBytes()); + } + }); + } + + @Override + public void close() { + values.clear(); + } + + @Override + public void reset() { + values.clear(); + } + + @Override + public String getDbName() { + return name; + } + + @Override + public void stat() { + } + + @Override + public DB newInstance() { + return new MemoryDb(name); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java index d646dbb12ec..a9a80b7e9a0 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import java.io.IOException; import java.nio.file.Files; @@ -272,6 +273,98 @@ archive, new HistoryCommitMarkerCodec())) { } } + @Test + public void emptyCaptureTargetAdvancesProgressWithoutChangingBusinessData() throws Exception { + Path archive = temporaryFolder.newFolder("empty-capture-recovery").toPath(); + List markers = initializeHistory(archive, 2); + HistoryCommitMarker initial = markers.get(0); + HistoryCommitMarker firstTarget = markers.get(1); + HistoryCommitMarker emptyTarget = markers.get(2); + Path checkpointPath = archive.resolve("progress/checkpoint.progress"); + Path readerPath = archive.resolve("progress/reader.progress"); + ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); + new ArchiveProgressFile(checkpointPath, progressCodec).store(global(Kind.APPLY_CHECKPOINT, + initial)); + new ArchiveProgressFile(readerPath, progressCodec).store(global(Kind.READER_VISIBLE, initial)); + + byte[] accountKey = bytes(2, 1); + byte[] assetKey = append(accountKey, 4); + byte[] proposalKey = bytes(2, 6); + byte[] accountValue = bytes(3, 11); + byte[] assetValue = bytes(3, 12); + byte[] proposalValue = bytes(3, 13); + + try (ParticipantFixture participants = new ParticipantFixture(archive, initial)) { + ArchiveParticipantMutationBatch firstBatch = capture(firstTarget, accountKey, + bytes(3, 10), accountValue, assetKey, assetValue, false, + proposalKey, proposalValue); + try (HistoryCommitStore history = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + new ArchiveTargetApplyCoordinator(history, checkpointPath, participants.engines, + readerPath, PARTICIPANTS, action -> action.run()).apply(firstBatch, () -> { }); + } + byte[] firstDigest = new ArchiveProgressFile(checkpointPath, progressCodec) + .load().getMutationPlanDigest(); + + ArchiveParticipantMutationBatch emptyBatch = captureEmpty(emptyTarget); + assertTrue(emptyBatch.getMutations().isEmpty()); + assertEquals(PARTICIPANTS, emptyBatch.getParticipants()); + try (HistoryCommitStore history = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, + checkpointPath, participants.engines, readerPath, PARTICIPANTS, + action -> action.run(), + (stage, participant) -> failAfterEmptyCheckpoint(stage), temporary -> { }); + assertThrows(IOException.class, () -> coordinator.apply(emptyBatch, () -> { })); + } + + for (String participant : PARTICIPANTS) { + assertEquals(2, participants.counted.get(participant).getApplyCount()); + } + assertArrayEquals(accountValue, participants.account.get(accountKey)); + assertArrayEquals(assetValue, participants.accountAsset.get(assetKey)); + assertArrayEquals(proposalValue, + participants.memory.get("proposal").get(proposalKey)); + + AtomicInteger refreshes = new AtomicInteger(); + try (ArchiveParticipantRecoveryStorage recovery = + new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, + participants.engines, readerPath, PARTICIPANTS, action -> action.run(), + refreshes::incrementAndGet)) { + new ArchiveRecoveryExecutor(recovery).recover(); + } + + for (String participant : PARTICIPANTS) { + assertEquals(3, participants.counted.get(participant).getApplyCount()); + } + assertEquals(1, refreshes.get()); + assertArrayEquals(accountValue, participants.account.get(accountKey)); + assertArrayEquals(assetValue, participants.accountAsset.get(assetKey)); + assertArrayEquals(proposalValue, + participants.memory.get("proposal").get(proposalKey)); + + ArchiveProgressEnvelope checkpoint = + new ArchiveProgressFile(checkpointPath, progressCodec).load(); + ArchiveProgressEnvelope reader = + new ArchiveProgressFile(readerPath, progressCodec).load(); + byte[] emptyDigest = checkpoint.getMutationPlanDigest(); + assertFalse(Arrays.equals(firstDigest, emptyDigest)); + for (String participant : PARTICIPANTS) { + assertArrayEquals(emptyDigest, + participants.engines.get(participant).loadProgress().getMutationPlanDigest()); + } + assertArrayEquals(emptyDigest, reader.getMutationPlanDigest()); + assertEquals(emptyTarget.getMeta().getEpoch(), reader.getEpoch()); + assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); + + try (ArchiveParticipantRecoveryStorage fixed = + new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, + participants.engines, readerPath, PARTICIPANTS)) { + assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); + } + } + } + private static ArchiveParticipantMutationBatch capture(HistoryCommitMarker target, byte[] accountKey, byte[] rawAccount, byte[] canonicalAccount, byte[] assetKey, byte[] assetValue, boolean deleteAsset, byte[] proposalKey, byte[] proposalValue) { @@ -296,6 +389,16 @@ private static ArchiveParticipantMutationBatch capture(HistoryCommitMarker targe } } + private static ArchiveParticipantMutationBatch captureEmpty(HistoryCommitMarker target) { + try (ViewFixture viewFixture = new ViewFixture()) { + ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture( + target.getMeta(), new ArchiveBlockForwardMutationLimits(0, 0, 0, 0, 0)); + BlockChangeView view = viewFixture.capture(target.getMeta(), databases -> { }); + capture.attach(view); + return capture.seal(target); + } + } + private static void failAfter(Stage stage, String participant, String failureParticipant) throws IOException { if (stage == Stage.AFTER_PARTICIPANT && failureParticipant.equals(participant)) { @@ -303,6 +406,12 @@ private static void failAfter(Stage stage, String participant, String failurePar } } + private static void failAfterEmptyCheckpoint(Stage stage) throws IOException { + if (stage == Stage.AFTER_CHECKPOINT) { + throw new IOException("injected after empty checkpoint"); + } + } + private static List initializeHistory(Path archive, int lastEpoch) throws Exception { List markers = new ArrayList<>(); diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java index c9cb85ef17f..32b9d99de39 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java @@ -41,12 +41,12 @@ public void collectsExactPostPutDeleteAndEmptyDeterministically() { second.rootPut("storage-row", deleted, bytes(1, 8)); BlockChangeView firstView = first.capture(meta, databases -> { databases.get("proposal").put(bytes(2, 3), bytes(1, 3)); - databases.get("abi").put(bytes(2, 1), new byte[0]); + databases.get("code").put(bytes(2, 1), new byte[0]); databases.get("storage-row").delete(deleted); }); BlockChangeView secondView = second.capture(meta, databases -> { databases.get("storage-row").delete(deleted); - databases.get("abi").put(bytes(2, 1), new byte[0]); + databases.get("code").put(bytes(2, 1), new byte[0]); databases.get("proposal").put(bytes(2, 3), bytes(1, 3)); }); ArchiveParticipantMutationBatchCollector collector = @@ -56,7 +56,7 @@ public void collectsExactPostPutDeleteAndEmptyDeterministically() { ArchiveTargetMutationPlan secondPlan = new ArchiveTargetMutationPlanBuilder().build(marker, collector.collect(marker, secondView)); - assertArrayEquals(new byte[0], firstPlan.getMutations("abi").get(0).getValue()); + assertArrayEquals(new byte[0], firstPlan.getMutations("code").get(0).getValue()); assertNull(firstPlan.getMutations("storage-row").get(0).getValue()); assertArrayEquals(bytes(1, 3), firstPlan.getMutations("proposal").get(0).getValue()); @@ -106,15 +106,15 @@ public void rejectsViewIdentityCoverageAndMissingProjectionResult() { HistoryCommitMarker marker = marker(meta, participants()); try (Fixture exact = new Fixture(participants())) { BlockChangeView view = exact.capture(meta, - databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); assertThrows(ArchivePersistenceException.class, () -> new ArchiveParticipantMutationBatchCollector().collect( marker(meta(2), participants()), view)); } - try (Fixture incomplete = new Fixture(Collections.singletonList("abi"))) { + try (Fixture incomplete = new Fixture(Collections.singletonList("code"))) { BlockChangeView view = incomplete.capture(meta, - databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); assertThrows(ArchivePersistenceException.class, () -> new ArchiveParticipantMutationBatchCollector().collect(marker, view)); } @@ -466,10 +466,10 @@ public void blockCapturePreconditionFailuresRemainRetryableBeforeManifestConsump try (Fixture exact = new Fixture(participants()); Fixture other = new Fixture(participants())) { BlockChangeView wrongView = other.capture(otherMeta, - databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); assertThrows(ArchivePersistenceException.class, () -> capture.attach(wrongView)); BlockChangeView view = exact.capture(meta, - databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); capture.attach(view); assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); assertThrows(ArchivePersistenceException.class, () -> capture.seal(otherMarker)); @@ -532,7 +532,7 @@ public void blockCaptureAbortBeforeAttachReleasesPayloadAndRejectsEveryTerminalA try (Fixture fixture = new Fixture(participants())) { BlockChangeView view = fixture.capture(meta, - databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); } } @@ -680,10 +680,10 @@ public void captureLimitsReserveAttachedViewAtomicallyAndAllowRetry() { new ArchiveBlockForwardMutationLimits(0, 0, 3, 3, 3)); try (Fixture fixture = new Fixture(participants())) { BlockChangeView tooLarge = fixture.capture(meta, - databases -> databases.get("abi").put(bytes(2, 1), bytes(2, 2))); + databases -> databases.get("code").put(bytes(2, 1), bytes(2, 2))); assertThrows(ArchivePersistenceException.class, () -> total.attach(tooLarge)); BlockChangeView exact = fixture.capture(meta, - databases -> databases.get("abi").put(bytes(1, 1), bytes(1, 2))); + databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); total.attach(exact); assertEquals(meta.getEpoch(), total.seal(marker).getTargetEpoch()); } @@ -694,10 +694,10 @@ public void captureLimitsReserveAttachedViewAtomicallyAndAllowRetry() { new ArchiveBlockForwardMutationLimits(0, 0, 2, 1, 10)); try (Fixture fixture = new Fixture(participants())) { BlockChangeView keyTooLarge = fixture.capture(meta, - databases -> databases.get("abi").put(bytes(2, 1), new byte[0])); + databases -> databases.get("code").put(bytes(2, 1), new byte[0])); assertThrows(ArchivePersistenceException.class, () -> key.attach(keyTooLarge)); BlockChangeView valueTooLarge = fixture.capture(meta, - databases -> databases.get("abi").put(bytes(1, 1), bytes(2, 2))); + databases -> databases.get("code").put(bytes(1, 1), bytes(2, 2))); assertThrows(ArchivePersistenceException.class, () -> value.attach(valueTooLarge)); } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceiptTest.java b/framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceiptTest.java new file mode 100644 index 00000000000..72b770d30a1 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceiptTest.java @@ -0,0 +1,196 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class DurableHistoryMarkerRangeReceiptTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void readsOnlyExactDurableRangeAndReopensWithIdenticalReceipt() throws Exception { + Path archive = temporaryFolder.newFolder("marker-receipt").toPath(); + List expected = Arrays.asList(meta(2), meta(3)); + List encoded = new ArrayList<>(); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( + archive, 4096, new java.util.LinkedHashSet<>(participants()))) { + writer.acceptAll(Arrays.asList(diff(1), diff(2), diff(3), diff(4))); + List receipt = + new DurableHistoryMarkerRangeReceipt(writer, 2).read(expected); + assertEquals(Arrays.asList(2L, 3L), epochs(receipt)); + HistoryCommitMarkerCodec codec = new HistoryCommitMarkerCodec(); + receipt.forEach(marker -> encoded.add(codec.encode(marker))); + } + + try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter( + archive, 4096, new java.util.LinkedHashSet<>(participants()))) { + List receipt = + new DurableHistoryMarkerRangeReceipt(reopened, 2).read(expected); + HistoryCommitMarkerCodec codec = new HistoryCommitMarkerCodec(); + assertArrayEquals(encoded.get(0), codec.encode(receipt.get(0))); + assertArrayEquals(encoded.get(1), codec.encode(receipt.get(1))); + assertEquals(4L, reopened.committedHead().getMeta().getEpoch()); + } + } + + @Test + public void markerPreflightRejectsMissingSubstitutedAndReorderedBeforeBodyRead() { + FakeSource source = new FakeSource(); + source.put(marker(meta(1)), diff(1)); + DurableHistoryMarkerRangeReceipt receipt = + new DurableHistoryMarkerRangeReceipt(source, 2); + List expected = Arrays.asList(meta(1), meta(2)); + + assertThrows(ArchivePersistenceException.class, () -> receipt.read(expected)); + assertEquals(0, source.bodyReads); + + source.putAt(2, marker(meta(3)), diff(2)); + assertThrows(ArchivePersistenceException.class, () -> receipt.read(expected)); + assertEquals(0, source.bodyReads); + + source.putAt(1, marker(meta(2)), diff(1)); + source.putAt(2, marker(meta(1)), diff(2)); + assertThrows(ArchivePersistenceException.class, () -> receipt.read(expected)); + assertEquals(0, source.bodyReads); + + source.clear(); + source.put(marker(meta(1)), diff(1)); + source.put(marker(meta(2)), diff(2)); + assertEquals(Arrays.asList(1L, 2L), epochs(receipt.read(expected))); + assertEquals(2, source.bodyReads); + } + + @Test + public void referenceFailureAndMarkerDriftLeaveReceiptRetryable() { + FakeSource source = new FakeSource(); + source.put(marker(meta(1)), diff(1)); + source.failBody = true; + DurableHistoryMarkerRangeReceipt receipt = + new DurableHistoryMarkerRangeReceipt(source, 1); + + assertThrows(ArchivePersistenceException.class, + () -> receipt.read(Collections.singletonList(meta(1)))); + source.failBody = false; + assertEquals(1, receipt.read(Collections.singletonList(meta(1))).size()); + + source.bodyReads = 0; + source.driftAfterBody = true; + assertThrows(ArchivePersistenceException.class, + () -> receipt.read(Collections.singletonList(meta(1)))); + source.driftAfterBody = false; + assertEquals(1, receipt.read(Collections.singletonList(meta(1))).size()); + } + + @Test + public void invalidOrOversizedExpectedRangeFailsBeforeSourceAction() { + FakeSource source = new FakeSource(); + DurableHistoryMarkerRangeReceipt receipt = + new DurableHistoryMarkerRangeReceipt(source, 1); + + assertThrows(ArchivePersistenceException.class, + () -> receipt.read(Collections.emptyList())); + assertThrows(ArchivePersistenceException.class, + () -> receipt.read(Arrays.asList(meta(1), meta(2)))); + assertThrows(ArchivePersistenceException.class, + () -> new DurableHistoryMarkerRangeReceipt(source, 2) + .read(Arrays.asList(meta(1), meta(3)))); + assertEquals(0, source.markerReads); + assertEquals(0, source.bodyReads); + } + + private static List epochs(List markers) { + List epochs = new ArrayList<>(); + markers.forEach(marker -> epochs.add(marker.getMeta().getEpoch())); + return epochs; + } + + private static List participants() { + List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(participants); + return participants; + } + + private static BlockReverseDiff diff(int epoch) { + return new BlockReverseDiff(meta(epoch), Collections.emptyList()); + } + + private static BlockSnapshotMeta meta(int epoch) { + return BlockSnapshotMeta.forBlock(epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L); + } + + private static HistoryCommitMarker marker(BlockSnapshotMeta meta) { + int epoch = (int) meta.getEpoch(); + return new HistoryCommitMarker(meta, epoch - 1, + new HistoryLocation(0, epoch * 100L, 100, epoch, bytes(32, epoch + 20)), + new HistoryIndexLocation(epoch * 50L, 50, bytes(32, epoch + 30)), + bytes(16, epoch + 40), participants()); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(int length, int value) { + byte[] result = new byte[length]; + Arrays.fill(result, (byte) value); + return result; + } + + private static final class FakeSource implements DurableHistoryMarkerRangeReceipt.Source { + private final Map markers = new LinkedHashMap<>(); + private final Map bodies = new LinkedHashMap<>(); + private int markerReads; + private int bodyReads; + private boolean failBody; + private boolean driftAfterBody; + + private void put(HistoryCommitMarker marker, BlockReverseDiff body) { + putAt(marker.getMeta().getEpoch(), marker, body); + } + + private void putAt(long epoch, HistoryCommitMarker marker, BlockReverseDiff body) { + markers.put(epoch, marker); + bodies.put(epoch, body); + } + + private void clear() { + markers.clear(); + bodies.clear(); + markerReads = 0; + bodyReads = 0; + } + + @Override + public HistoryCommitMarker marker(long epoch) { + markerReads++; + if (driftAfterBody && bodyReads > 0) { + return DurableHistoryMarkerRangeReceiptTest.marker(meta((int) epoch + 1)); + } + return markers.get(epoch); + } + + @Override + public BlockReverseDiff readCommitted(long epoch) { + bodyReads++; + if (failBody) { + throw new ArchivePersistenceException("injected body/index reference failure"); + } + return bodies.get(epoch); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index 7dfb7b09db1..0cf47c8c563 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -3,12 +3,14 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -27,10 +29,14 @@ import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.tron.common.BaseMethodTest; import org.tron.core.db.common.DbSourceInter; import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; +import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; import org.tron.core.db2.archive.BlockReverseDiff.Entry; import org.tron.core.db2.common.DB; @@ -49,7 +55,7 @@ public class SnapshotOldValueCollectorTest extends BaseMethodTest { @Test public void collectsBlockPreStateAfterNestedSessionsFinish() { - MemoryDb memoryDb = new MemoryDb("abi"); + MemoryDb memoryDb = new MemoryDb("code"); byte[] changed = bytes("changed"); byte[] deleted = bytes("deleted"); byte[] created = bytes("created"); @@ -92,7 +98,7 @@ public void collectsBlockPreStateAfterNestedSessionsFinish() { assertEquals(meta, ((SnapshotImpl) database.getHead()).getBlockSnapshotMeta()); assertEquals(1, diff.getGroups().size()); DbGroup group = diff.getGroups().get(0); - assertEquals("abi", group.getDbName()); + assertEquals("code", group.getDbName()); assertEquals(3, group.getEntries().size()); assertArrayEquals(bytes("old"), find(group, changed).getOldValue().getValue()); @@ -139,9 +145,33 @@ public void preservesStorageRowPhysicalKeyWithoutLogicalProjection() { manager.shutdown(); } + @Test + public void excludesAbiChangesFromCanonicalStateHistory() { + SnapshotManager manager = new SnapshotManager(""); + Chainbase abi = new Chainbase(new SnapshotRoot(new MemoryDb("abi"))); + Chainbase code = new Chainbase(new SnapshotRoot(new MemoryDb("code"))); + manager.add(abi); + manager.add(code); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + + try (ISession block = manager.buildSession()) { + abi.put(bytes("contract"), bytes("abi-metadata")); + code.put(bytes("contract"), bytes("runtime-code")); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + + BlockReverseDiff diff = prepared(code); + assertEquals(1, diff.getGroups().size()); + assertEquals("code", diff.getGroups().get(0).getDbName()); + assertFalse(diff.getGroups().stream().anyMatch(group -> "abi".equals(group.getDbName()))); + assertTrue(((SnapshotImpl) abi.getHead()).getPreparedArchiveBlock() == null); + manager.shutdown(); + } + @Test public void preservesPresentEmptyAndEmitsNoopBlockMetadata() { - MemoryDb memoryDb = new MemoryDb("abi"); + MemoryDb memoryDb = new MemoryDb("code"); byte[] key = bytes("key"); memoryDb.put(key, new byte[0]); SnapshotManager manager = new SnapshotManager(""); @@ -176,8 +206,8 @@ public void rejectsUnknownOrDuplicateRegisteredDatabaseNames() { unknown.shutdown(); SnapshotManager duplicate = new SnapshotManager(""); - duplicate.add(new Chainbase(new SnapshotRoot(new MemoryDb("abi")))); - duplicate.add(new Chainbase(new SnapshotRoot(new MemoryDb("abi")))); + duplicate.add(new Chainbase(new SnapshotRoot(new MemoryDb("code")))); + duplicate.add(new Chainbase(new SnapshotRoot(new MemoryDb("code")))); IllegalStateException duplicateError = assertThrows(IllegalStateException.class, () -> duplicate.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { })); assertTrue(duplicateError.getMessage().contains("Duplicate")); @@ -188,11 +218,15 @@ public void rejectsUnknownOrDuplicateRegisteredDatabaseNames() { public void classifiesEveryChainbaseRegisteredByTheApplication() { SnapshotManager applicationManager = context.getBean(SnapshotManager.class); ArchiveStoreScope.validate(applicationManager.getDbs()); + assertEquals(26, ArchiveStoreScope.getStateDatabases().size()); + assertFalse(ArchiveStoreScope.isStateDatabase("abi")); + assertTrue(ArchiveStoreScope.isExcludedDatabase("abi")); + assertTrue(ArchiveStoreScope.isClassified("abi")); } @Test public void matchesReferenceStateForRandomBlockOperations() { - MemoryDb memoryDb = new MemoryDb("abi"); + MemoryDb memoryDb = new MemoryDb("code"); SnapshotManager manager = new SnapshotManager(""); Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); manager.add(database); @@ -274,9 +308,9 @@ public void projectsAccountAssetTransitionBeforeRootMerge() { Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); manager.add(database); manager.enable(); - AccountAssetArchiveProjector projector = new AccountAssetArchiveProjector(assetStore, - () -> true); - manager.installArchiveCollector(new SnapshotOldValueCollector(projector), diff -> { }); + AccountAssetArchiveProjector projector = new AccountAssetArchiveProjector(); + manager.installArchiveCollector(new SnapshotOldValueCollector(projector, + accountKey -> assetStore.prefixQuery(accountKey), () -> true), diff -> { }); try (ISession block = manager.buildSession()) { database.put(address, postAccount.toByteArray()); @@ -327,7 +361,8 @@ public void projectsOldPhysicalAssetValueForOptimizedAccount() { manager.add(database); manager.enable(); manager.installArchiveCollector(new SnapshotOldValueCollector( - new AccountAssetArchiveProjector(assetStore, () -> true)), diff -> { }); + new AccountAssetArchiveProjector(), + accountKey -> assetStore.prefixQuery(accountKey), () -> true), diff -> { }); try (ISession block = manager.buildSession()) { database.put(address, postAccount.toByteArray()); @@ -345,9 +380,197 @@ public void projectsOldPhysicalAssetValueForOptimizedAccount() { manager.shutdown(); } + @Test + public void sharedAccountAssetProjectionUsesOneSnapshotAndStableForwardOrder() { + byte[] address = bytes("shared-projection-address"); + byte[] firstKey = Bytes.concat(address, bytes("1000001")); + byte[] secondKey = Bytes.concat(address, bytes("1000002")); + Account oldAccount = Account.newBuilder() + .setAddress(ByteString.copyFrom(address)) + .setAssetOptimized(true) + .build(); + Account postAccount = oldAccount.toBuilder() + .putAssetV2("1000001", 80L) + .putAssetV2("1000002", 0L) + .build(); + AccountAssetStore assetStore = mock(AccountAssetStore.class); + Map persisted = new LinkedHashMap<>(); + persisted.put(WrappedByteArray.copyOf(secondKey), Longs.toByteArray(200L)); + persisted.put(WrappedByteArray.copyOf(firstKey), Longs.toByteArray(100L)); + when(assetStore.prefixQuery(any(byte[].class))).thenReturn(persisted); + + AccountAssetArchiveProjector.Projection projection = + new AccountAssetArchiveProjector().project(address, oldAccount.toByteArray(), + BlockChangeView.PostValue.present(postAccount.toByteArray()), false, persisted); + + verify(assetStore, never()).prefixQuery(any(byte[].class)); + assertEquals(2, projection.reverseAssets.size()); + assertEquals(2, projection.forwardAssets.size()); + assertArrayEquals(firstKey, projection.reverseAssets.get(0).getKey()); + assertArrayEquals(secondKey, projection.reverseAssets.get(1).getKey()); + assertArrayEquals(firstKey, projection.forwardAssets.get(0).getPhysicalRawKey()); + assertArrayEquals(secondKey, projection.forwardAssets.get(1).getPhysicalRawKey()); + assertArrayEquals(Longs.toByteArray(100L), + projection.reverseAssets.get(0).getOldValue().getValue()); + assertArrayEquals(Longs.toByteArray(80L), + projection.forwardAssets.get(0).getPostValue().getValue()); + assertFalse(projection.forwardAssets.get(1).getPostValue().isPresent()); + assertThrows(UnsupportedOperationException.class, projection.reverseAssets::clear); + assertThrows(UnsupportedOperationException.class, projection.forwardAssets::clear); + Account canonicalPost = parseAccount(projection.postAccount.getValue()); + assertTrue(canonicalPost.getAssetOptimized()); + assertTrue(canonicalPost.getAssetV2Map().isEmpty()); + } + + @Test + public void pureProjectionRequiresAndCopiesExplicitOldPhysicalAssets() { + byte[] address = bytes("pure-input-address"); + byte[] assetKey = Bytes.concat(address, bytes("1000009")); + Account optimized = Account.newBuilder() + .setAddress(ByteString.copyFrom(address)) + .setAssetOptimized(true) + .build(); + Map oldPhysicalAssets = new HashMap<>(); + oldPhysicalAssets.put(WrappedByteArray.copyOf(assetKey), Longs.toByteArray(900L)); + AccountAssetArchiveProjector projector = new AccountAssetArchiveProjector(); + + assertThrows(ArchivePersistenceException.class, + () -> projector.project(address, optimized.toByteArray(), + BlockChangeView.PostValue.absent(), true, null)); + Map wrongAccountAssets = new HashMap<>(); + wrongAccountAssets.put(WrappedByteArray.copyOf(bytes("another-account-token")), + Longs.toByteArray(1L)); + assertThrows(ArchivePersistenceException.class, + () -> projector.project(address, optimized.toByteArray(), + BlockChangeView.PostValue.absent(), true, wrongAccountAssets)); + + AccountAssetArchiveProjector.Projection projection = projector.project(address, + optimized.toByteArray(), BlockChangeView.PostValue.absent(), true, + oldPhysicalAssets); + oldPhysicalAssets.clear(); + + assertEquals(1, projection.reverseAssets.size()); + assertArrayEquals(assetKey, projection.reverseAssets.get(0).getKey()); + assertArrayEquals(Longs.toByteArray(900L), + projection.reverseAssets.get(0).getOldValue().getValue()); + assertFalse(projection.forwardAssets.get(0).getPostValue().isPresent()); + } + + @Test + public void targetAssetOptimizationOverridesLegacySupplierAndCoversDelete() { + byte[] address = bytes("target-activation-address"); + byte[] assetKey = Bytes.concat(address, bytes("1000003")); + Account rawPost = Account.newBuilder() + .setAddress(ByteString.copyFrom(address)) + .putAssetV2("1000003", 300L) + .build(); + AccountAssetArchiveProjector projector = new AccountAssetArchiveProjector(); + + AccountAssetArchiveProjector.Projection enabled = projector.project(address, null, + BlockChangeView.PostValue.present(rawPost.toByteArray()), true, Collections.emptyMap()); + assertTrue(parseAccount(enabled.postAccount.getValue()).getAssetOptimized()); + assertEquals(1, enabled.forwardAssets.size()); + assertArrayEquals(assetKey, enabled.forwardAssets.get(0).getPhysicalRawKey()); + assertArrayEquals(Longs.toByteArray(300L), + enabled.forwardAssets.get(0).getPostValue().getValue()); + + AccountAssetArchiveProjector.Projection disabled = + new AccountAssetArchiveProjector().project(address, null, + BlockChangeView.PostValue.present(rawPost.toByteArray()), false, + Collections.emptyMap()); + assertArrayEquals(rawPost.toByteArray(), disabled.postAccount.getValue()); + assertTrue(disabled.forwardAssets.isEmpty()); + + Account optimizedOld = rawPost.toBuilder() + .setAssetOptimized(true) + .clearAssetV2() + .build(); + Map persisted = new HashMap<>(); + persisted.put(WrappedByteArray.copyOf(assetKey), Longs.toByteArray(300L)); + AccountAssetArchiveProjector.Projection deleted = projector.project(address, + optimizedOld.toByteArray(), BlockChangeView.PostValue.absent(), true, persisted); + assertFalse(deleted.postAccount.isPresent()); + assertEquals(1, deleted.reverseAssets.size()); + assertEquals(1, deleted.forwardAssets.size()); + assertFalse(deleted.forwardAssets.get(0).getPostValue().isPresent()); + } + + @Test + public void sharedProjectionUsesOuterFinalViewAfterNestedMergeAndRevoke() { + byte[] address = bytes("nested-account-address"); + byte[] assetKey = Bytes.concat(address, bytes("1000004")); + Account oldAccount = Account.newBuilder() + .setAddress(ByteString.copyFrom(address)) + .putAssetV2("1000004", 100L) + .build(); + MemoryDb memoryDb = new MemoryDb("account"); + memoryDb.put(address, oldAccount.toByteArray()); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + + try (ISession block = manager.buildSession()) { + database.put(address, oldAccount.toBuilder().putAssetV2("1000004", 90L) + .build().toByteArray()); + try (ISession merged = manager.buildSession()) { + database.put(address, oldAccount.toBuilder().putAssetV2("1000004", 80L) + .build().toByteArray()); + merged.merge(); + } + try (ISession revoked = manager.buildSession()) { + database.put(address, oldAccount.toBuilder().putAssetV2("1000004", 70L) + .build().toByteArray()); + } + BlockChangeView view = BlockChangeView.capture(meta, + Collections.singletonList(database)); + BlockChangeView.Change finalChange = view.getDatabases().get(0).getChanges().get(0); + AccountAssetArchiveProjector.Projection projection = + new AccountAssetArchiveProjector().project(address, oldAccount.toByteArray(), + finalChange.getPostValue(), true, Collections.emptyMap()); + assertEquals(1, projection.forwardAssets.size()); + AssetMutation mutation = projection.forwardAssets.get(0); + assertArrayEquals(assetKey, mutation.getPhysicalRawKey()); + assertArrayEquals(Longs.toByteArray(80L), mutation.getPostValue().getValue()); + } + manager.shutdown(); + } + + @Test + public void sharedProjectionMatchesSnapshotRootBytesWithProposalSixtySix() { + byte[] address = new byte[21]; + address[0] = 65; + address[20] = 79; + String token = "1000005"; + byte[] assetKey = Bytes.concat(address, bytes(token)); + Account rawPost = Account.newBuilder() + .setAddress(ByteString.copyFrom(address)) + .putAssetV2(token, 500L) + .build(); + AccountAssetArchiveProjector.Projection projection = + new AccountAssetArchiveProjector().project(address, null, + BlockChangeView.PostValue.present(rawPost.toByteArray()), true, + Collections.emptyMap()); + + chainBaseManager.getDynamicPropertiesStore().setAllowAccountAssetOptimization(0); + chainBaseManager.getDynamicPropertiesStore().setAllowAssetOptimization(1); + MemoryDb accountRootDb = new MemoryDb("account"); + SnapshotRoot accountRoot = new SnapshotRoot(accountRootDb); + accountRoot.put(address, rawPost.toByteArray()); + + assertArrayEquals(projection.postAccount.getValue(), accountRootDb.get(address)); + assertArrayEquals(Longs.toByteArray(500L), + chainBaseManager.getAccountAssetStore().get(assetKey)); + assertEquals(1, projection.forwardAssets.size()); + assertArrayEquals(assetKey, projection.forwardAssets.get(0).getPhysicalRawKey()); + assertArrayEquals(Longs.toByteArray(500L), + projection.forwardAssets.get(0).getPostValue().getValue()); + } + @Test public void archiveDurabilityFailurePreventsCheckpointAndRefresh() throws Exception { - MemoryDb memoryDb = new MemoryDb("abi"); + MemoryDb memoryDb = new MemoryDb("code"); SnapshotManager manager = new SnapshotManager(""); Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); manager.add(database); @@ -374,7 +597,7 @@ public void archiveDurabilityFailurePreventsCheckpointAndRefresh() throws Except @Test public void fastPopDiscardsPreparedPayloadWithoutRevertingDurableHistory() { - MemoryDb memoryDb = new MemoryDb("abi"); + MemoryDb memoryDb = new MemoryDb("code"); SnapshotManager manager = new SnapshotManager(""); Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); manager.add(database); @@ -400,7 +623,7 @@ public void fastPopDiscardsPreparedPayloadWithoutRevertingDurableHistory() { @Test public void collectorFailureLeavesSessionOwnedSoCloseRevokesLayer() { - MemoryDb memoryDb = new MemoryDb("abi"); + MemoryDb memoryDb = new MemoryDb("code"); SnapshotManager manager = new SnapshotManager(""); Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); manager.add(database); @@ -425,9 +648,380 @@ public void collectorFailureLeavesSessionOwnedSoCloseRevokesLayer() { manager.shutdown(); } + @Test + public void sharedProjectionPreparerIsDisabledUntilExplicitlyInstalled() { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + OldValueCollector collector = mock(OldValueCollector.class); + BlockReverseDiff reverse = mock(BlockReverseDiff.class); + when(collector.collect(any(BlockChangeView.class))).thenReturn(reverse); + manager.installArchiveCollector(collector, diff -> { }); + + try (ISession block = manager.buildSession()) { + database.put(bytes("key"), bytes("value")); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + + verify(collector).collect(any(BlockChangeView.class)); + assertEquals(0, manager.getArchiveForwardPayloadOwnerCount()); + manager.shutdown(); + } + + @Test + public void sharedProjectionPreparerOwnsOneCapturedViewAndReversePayload() { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + OldValueCollector legacy = mock(OldValueCollector.class); + manager.installArchiveCollector(legacy, diff -> { }); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + BlockReverseDiff reverse = mock(BlockReverseDiff.class); + AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = + preparedProjection(meta, reverse); + AtomicInteger calls = new AtomicInteger(); + AtomicReference captured = new AtomicReference<>(); + manager.installArchiveProjectionPreparer(view -> { + calls.incrementAndGet(); + captured.set(view); + return projection; + }); + + try (ISession block = manager.buildSession()) { + database.put(bytes("key"), bytes("value")); + block.commit(meta); + } + + assertEquals(1, calls.get()); + assertEquals(meta, captured.get().getMeta()); + assertEquals(reverse, prepared(database)); + assertTrue(manager.hasArchiveForwardPayloadOwner(meta)); + verify(legacy, never()).collect(any(BlockChangeView.class)); + manager.fastPop(); + verify(projection).abort(); + manager.shutdown(); + } + + @Test + public void projectionPrepareFailureLeavesSessionOwnedAndRegistryEmpty() { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + manager.installArchiveProjectionPreparer(view -> { + throw new ArchivePersistenceException("injected prepare failure"); + }); + + assertThrows(ArchivePersistenceException.class, () -> { + try (ISession block = manager.buildSession()) { + database.put(bytes("key"), bytes("value")); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + }); + + assertEquals(0, manager.getActiveSession()); + assertEquals(0, manager.size()); + assertEquals(0, manager.getArchiveForwardPayloadOwnerCount()); + assertTrue(database.getHead() instanceof SnapshotRoot); + manager.shutdown(); + } + + @Test + public void projectionAttachFailureAbortsUnownedPayloadAndRevokesLayer() { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + BlockSnapshotMeta target = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + AccountAssetBlockProjectionBridge.PreparedBlockProjection mismatched = + preparedProjection(BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L), + mock(BlockReverseDiff.class)); + manager.installArchiveProjectionPreparer(view -> mismatched); + + assertThrows(ArchivePersistenceException.class, () -> { + try (ISession block = manager.buildSession()) { + database.put(bytes("key"), bytes("value")); + block.commit(target); + } + }); + + verify(mismatched).abort(); + assertEquals(0, manager.getActiveSession()); + assertEquals(0, manager.size()); + assertEquals(0, manager.getArchiveForwardPayloadOwnerCount()); + assertTrue(database.getHead() instanceof SnapshotRoot); + manager.shutdown(); + } + + @Test + public void shortReorgDiscardsOnlySameMetaUnfrozenOwners() { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + BlockSnapshotMeta firstMeta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + BlockSnapshotMeta secondMeta = BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L); + AccountAssetBlockProjectionBridge.PreparedBlockProjection first = + preparedProjection(firstMeta, mock(BlockReverseDiff.class)); + AccountAssetBlockProjectionBridge.PreparedBlockProjection second = + preparedProjection(secondMeta, mock(BlockReverseDiff.class)); + manager.installArchiveProjectionPreparer( + view -> firstMeta.equals(view.getMeta()) ? first : second); + + try (ISession block = manager.buildSession()) { + database.put(bytes("key-1"), bytes("value-1")); + block.commit(firstMeta); + } + try (ISession block = manager.buildSession()) { + database.put(bytes("key-2"), bytes("value-2")); + block.commit(secondMeta); + } + + assertEquals(2, manager.getArchiveForwardPayloadOwnerCount()); + manager.fastPop(); + verify(second).abort(); + verify(first, never()).abort(); + assertTrue(manager.hasArchiveForwardPayloadOwner(firstMeta)); + assertFalse(manager.hasArchiveForwardPayloadOwner(secondMeta)); + manager.fastPop(); + verify(first).abort(); + assertEquals(0, manager.getArchiveForwardPayloadOwnerCount()); + manager.shutdown(); + } + + @Test + public void oldestForwardFlushRangeFreezesOnceAndExcludesFastPop() throws Exception { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + BlockSnapshotMeta firstMeta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + BlockSnapshotMeta secondMeta = BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L); + AccountAssetBlockProjectionBridge.PreparedBlockProjection first = + preparedProjection(firstMeta, mock(BlockReverseDiff.class)); + AccountAssetBlockProjectionBridge.PreparedBlockProjection second = + preparedProjection(secondMeta, mock(BlockReverseDiff.class)); + manager.installArchiveProjectionPreparer( + view -> firstMeta.equals(view.getMeta()) ? first : second); + commitBlock(manager, database, firstMeta, "key-1"); + commitBlock(manager, database, secondMeta, "key-2"); + setFlushCount(manager, 1); + + FrozenBatch pending = manager.freezeArchiveForwardFlushRange(); + + assertEquals(Collections.singletonList(firstMeta), pending.getExpectedMetas()); + assertSame(pending, manager.freezeArchiveForwardFlushRange()); + assertTrue(manager.hasPendingArchiveForwardFlush()); + assertEquals(1, manager.getArchiveForwardPayloadOwnerCount()); + manager.fastPop(); + verify(second).abort(); + verify(first, never()).abort(); + assertThrows(IllegalStateException.class, manager::fastPop); + assertEquals(1, manager.size()); + + manager.shutdown(); + verify(first).abort(); + } + + @Test + public void forwardFlushRegistryMismatchFailsBeforeOwnershipTransfer() throws Exception { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + BlockSnapshotMeta firstMeta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + BlockSnapshotMeta secondMeta = BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L); + AccountAssetBlockProjectionBridge.PreparedBlockProjection first = + preparedProjection(firstMeta, mock(BlockReverseDiff.class)); + AccountAssetBlockProjectionBridge.PreparedBlockProjection second = + preparedProjection(secondMeta, mock(BlockReverseDiff.class)); + manager.installArchiveProjectionPreparer( + view -> firstMeta.equals(view.getMeta()) ? first : second); + commitBlock(manager, database, firstMeta, "key-1"); + commitBlock(manager, database, secondMeta, "key-2"); + setFlushCount(manager, 1); + Map owners = forwardOwners(manager); + + AccountAssetPreparedBlockPayloadOwner removed = owners.remove(firstMeta); + assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); + assertEquals(1, owners.size()); + assertTrue(removed.isAttachedTo(firstMeta)); + owners.put(firstMeta, removed); + + BlockSnapshotMeta extraMeta = BlockSnapshotMeta.forBlock(3, hash(3), hash(2), 3L); + AccountAssetPreparedBlockPayloadOwner extra = + new AccountAssetPreparedBlockPayloadOwner(extraMeta); + extra.attach(preparedProjection(extraMeta, mock(BlockReverseDiff.class))); + owners.put(extraMeta, extra); + assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); + assertEquals(3, owners.size()); + assertTrue(removed.isAttachedTo(firstMeta)); + assertTrue(owners.get(secondMeta).isAttachedTo(secondMeta)); + + manager.shutdown(); + } + + @Test + public void forwardFlushRejectsTopologyGapAndUnattachedOwnerWithoutPartialTransfer() + throws Exception { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + BlockSnapshotMeta firstMeta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + BlockSnapshotMeta secondMeta = BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L); + AccountAssetBlockProjectionBridge.PreparedBlockProjection first = + preparedProjection(firstMeta, mock(BlockReverseDiff.class)); + AccountAssetBlockProjectionBridge.PreparedBlockProjection second = + preparedProjection(secondMeta, mock(BlockReverseDiff.class)); + manager.installArchiveProjectionPreparer( + view -> firstMeta.equals(view.getMeta()) ? first : second); + commitBlock(manager, database, firstMeta, "key-1"); + commitBlock(manager, database, secondMeta, "key-2"); + setFlushCount(manager, 1); + + SnapshotImpl newest = (SnapshotImpl) database.getHead(); + setBlockMeta(newest, firstMeta); + assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); + setBlockMeta(newest, BlockSnapshotMeta.forBlock(3, hash(3), hash(2), 3L)); + assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); + setBlockMeta(newest, secondMeta); + + Map owners = forwardOwners(manager); + owners.get(secondMeta).discard(); + assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); + assertEquals(2, owners.size()); + assertTrue(owners.get(firstMeta).isAttachedTo(firstMeta)); + verify(first, never()).abort(); + + manager.shutdown(); + verify(first).abort(); + verify(second).abort(); + } + + @Test + public void pendingForwardFlushSealRetriesAndClaimsOrderedPayloadsOnce() throws Exception { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + BlockSnapshotMeta firstMeta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + BlockSnapshotMeta secondMeta = BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L); + BlockChangeView firstView = mock(BlockChangeView.class); + BlockChangeView secondView = mock(BlockChangeView.class); + when(firstView.getMeta()).thenReturn(firstMeta); + when(secondView.getMeta()).thenReturn(secondMeta); + AccountAssetBlockProjectionBridge.PreparedBlockProjection first = + sealReadyProjection(firstMeta, firstView); + AccountAssetBlockProjectionBridge.PreparedBlockProjection second = + sealReadyProjection(secondMeta, secondView); + manager.installArchiveProjectionPreparer( + captured -> firstMeta.equals(captured.getMeta()) ? first : second); + commitBlock(manager, database, firstMeta, "key-1"); + commitBlock(manager, database, secondMeta, "key-2"); + setFlushCount(manager, 2); + FrozenBatch frozen = manager.freezeArchiveForwardFlushRange(); + + assertThrows(ArchivePersistenceException.class, + () -> manager.sealPendingArchiveForwardFlush( + Arrays.asList(marker(firstMeta), marker(BlockSnapshotMeta.forBlock( + 3, hash(3), hash(2), 3L))))); + assertSame(frozen, manager.freezeArchiveForwardFlushRange()); + verify(first, never()).completeSeal(); + verify(second, never()).completeSeal(); + + manager.sealPendingArchiveForwardFlush(Arrays.asList(marker(firstMeta), marker(secondMeta))); + + verify(first).completeSeal(); + verify(second).completeSeal(); + assertTrue(manager.hasPendingArchiveForwardFlush()); + assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); + assertThrows(IllegalStateException.class, manager::fastPop); + List claimed = + manager.claimArchiveForwardFlushPayloads(); + assertEquals(2, claimed.size()); + assertEquals(firstMeta, claimed.get(0).getMeta()); + assertSame(firstView, claimed.get(0).getView()); + assertEquals(secondMeta, claimed.get(1).getMeta()); + assertSame(secondView, claimed.get(1).getView()); + assertFalse(manager.hasPendingArchiveForwardFlush()); + assertThrows(IllegalStateException.class, manager::claimArchiveForwardFlushPayloads); + manager.shutdown(); + verify(first, never()).abort(); + verify(second, never()).abort(); + } + + @Test + public void durableReceiptFailureKeepsFrozenSlotAndShutdownReleasesSealedSlot() + throws Exception { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + HistoryCommitMarker committed = marker(meta); + BlockChangeView view = mock(BlockChangeView.class); + when(view.getMeta()).thenReturn(meta); + AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = + sealReadyProjection(meta, view); + manager.installArchiveProjectionPreparer(captured -> projection); + commitBlock(manager, database, meta, "key-1"); + setFlushCount(manager, 1); + FrozenBatch frozen = manager.freezeArchiveForwardFlushRange(); + boolean[] substitute = {true}; + DurableHistoryMarkerRangeReceipt receipt = new DurableHistoryMarkerRangeReceipt( + new DurableHistoryMarkerRangeReceipt.Source() { + @Override + public HistoryCommitMarker marker(long epoch) { + return substitute[0] + ? SnapshotOldValueCollectorTest.marker(BlockSnapshotMeta.forBlock( + 2, hash(2), hash(1), 2L)) : committed; + } + + @Override + public BlockReverseDiff readCommitted(long epoch) { + return new BlockReverseDiff(meta, Collections.emptyList()); + } + }, 1); + + assertThrows(ArchivePersistenceException.class, + () -> manager.sealPendingArchiveForwardFlush(receipt)); + assertSame(frozen, manager.freezeArchiveForwardFlushRange()); + substitute[0] = false; + manager.sealPendingArchiveForwardFlush(receipt); + assertTrue(manager.hasPendingArchiveForwardFlush()); + + manager.shutdown(); + + assertFalse(manager.hasPendingArchiveForwardFlush()); + assertThrows(IllegalStateException.class, manager::claimArchiveForwardFlushPayloads); + verify(projection).completeSeal(); + verify(projection, never()).abort(); + } + @Test public void flushPublishesOnlyTheNonRevertibleRange() throws Exception { - MemoryDb memoryDb = new MemoryDb("abi"); + MemoryDb memoryDb = new MemoryDb("code"); SnapshotManager manager = new SnapshotManager(""); Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); manager.add(database); @@ -469,10 +1063,76 @@ private static Entry find(DbGroup group, byte[] key) { .orElseThrow(AssertionError::new); } + private static Account parseAccount(byte[] value) { + try { + return Account.parseFrom(value); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw new AssertionError(e); + } + } + private static BlockReverseDiff prepared(Chainbase database) { return ((SnapshotImpl) database.getHead()).getPreparedArchiveBlock(); } + private static void commitBlock(SnapshotManager manager, Chainbase database, + BlockSnapshotMeta meta, String key) { + try (ISession block = manager.buildSession()) { + database.put(bytes(key), bytes("value-" + key)); + block.commit(meta); + } + } + + @SuppressWarnings("unchecked") + private static Map forwardOwners( + SnapshotManager manager) throws Exception { + java.lang.reflect.Field field = SnapshotManager.class.getDeclaredField( + "archiveForwardPayloadOwners"); + field.setAccessible(true); + return (Map) field.get(manager); + } + + private static void setBlockMeta(SnapshotImpl snapshot, BlockSnapshotMeta meta) + throws Exception { + java.lang.reflect.Field field = SnapshotImpl.class.getDeclaredField("blockSnapshotMeta"); + field.setAccessible(true); + field.set(snapshot, meta); + } + + private static AccountAssetBlockProjectionBridge.PreparedBlockProjection preparedProjection( + BlockSnapshotMeta meta, BlockReverseDiff reverse) { + AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = + mock(AccountAssetBlockProjectionBridge.PreparedBlockProjection.class); + when(projection.getMeta()).thenReturn(meta); + when(projection.getReverseDiff()).thenReturn(reverse); + return projection; + } + + private static AccountAssetBlockProjectionBridge.PreparedBlockProjection sealReadyProjection( + BlockSnapshotMeta meta, BlockChangeView view) { + AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = + preparedProjection(meta, mock(BlockReverseDiff.class)); + when(projection.previewSealPayload(any(HistoryCommitMarker.class))).thenAnswer(invocation -> { + HistoryCommitMarker target = invocation.getArgument(0); + if (!meta.equals(target.getMeta())) { + throw new ArchivePersistenceException("Prepared block projection target mismatch"); + } + return new ArchiveBlockForwardPayload(target, view, + new AccountAssetForwardMutationManifest(target, Collections.emptyList())); + }); + return projection; + } + + private static HistoryCommitMarker marker(BlockSnapshotMeta meta) { + int epoch = (int) meta.getEpoch(); + List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(participants); + return new HistoryCommitMarker(meta, epoch - 1, + new HistoryLocation(0, epoch * 100L, 100, epoch, hash(epoch + 20)), + new HistoryIndexLocation(epoch * 50L, 50, hash(epoch + 30)), + Arrays.copyOf(hash(epoch + 40), 16), participants); + } + private static void setFlushCount(SnapshotManager manager, int count) throws Exception { java.lang.reflect.Field flushCount = SnapshotManager.class.getDeclaredField("flushCount"); flushCount.setAccessible(true); From ca0e6a8084020e0c456c5ba3ffbe23021c956aba Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 20:31:40 +0800 Subject: [PATCH 020/161] feat(chainbase): seal archive flush receipts Expose durable marker receipt creation through direct and asynchronous history sinks, then gate forward payload sealing on the exact committed flush range. Retain submitted frozen ranges across durability or receipt failures so retries do not resubmit history. --- .../db2/archive/ArchiveHistoryWriter.java | 5 +++++ .../db2/archive/AsyncArchiveHistorySink.java | 6 ++++++ .../archive/DurableBlockReverseDiffSink.java | 2 ++ .../tron/core/db2/core/SnapshotManager.java | 21 ++++++++++++++++++- 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java index 79505a6c12b..d8a2d1ed9db 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java @@ -152,6 +152,11 @@ public synchronized void awaitCommitted(long epoch) { } } + @Override + public DurableHistoryMarkerRangeReceipt createMarkerRangeReceipt(int maxMarkers) { + return new DurableHistoryMarkerRangeReceipt(this, maxMarkers); + } + @Override public void releaseThrough(long epoch) { // The synchronous writer has no queue bookkeeping to release. diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java b/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java index 9e39ef72d64..d86a5d2e153 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java @@ -116,6 +116,12 @@ public void awaitCommitted(long epoch) { ensureOperational(); } + @Override + public DurableHistoryMarkerRangeReceipt createMarkerRangeReceipt(int maxMarkers) { + ensureOperational(); + return new DurableHistoryMarkerRangeReceipt(writer, maxMarkers); + } + /** Releases completed queue bookkeeping after the corresponding disk epoch is durable. */ @Override public void releaseThrough(long epoch) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java b/chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java index b6e2d4dc1b1..7b111eff1d1 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java @@ -5,5 +5,7 @@ public interface DurableBlockReverseDiffSink extends BlockReverseDiffSink { void awaitCommitted(long epoch); + DurableHistoryMarkerRangeReceipt createMarkerRangeReceipt(int maxMarkers); + void releaseThrough(long epoch); } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index 8d9d1da638d..98a56825f2c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -41,6 +41,7 @@ import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; import org.tron.core.db2.archive.ArchiveBlockForwardPayload; import org.tron.core.db2.archive.ArchiveBlockProjectionPreparer; +import org.tron.core.db2.archive.ArchivePersistenceException; import org.tron.core.db2.archive.ArchiveStateBarrier.ArchiveStateAction; import org.tron.core.db2.archive.ArchiveStoreScope; import org.tron.core.db2.archive.BlockChangeView; @@ -107,6 +108,7 @@ public class SnapshotManager implements RevokingDatabase { archiveForwardPayloadOwners = new HashMap<>(); private FrozenBatch pendingArchiveForwardFlush; private List sealedArchiveForwardFlush; + private Long submittedArchiveForwardHistoryEpoch; private BlockReverseDiffSink blockReverseDiffSink; @Getter private volatile long archiveReadableEpoch = -1; @@ -626,6 +628,7 @@ private synchronized void abortArchiveForwardPayloads() { pendingArchiveForwardFlush = null; } sealedArchiveForwardFlush = null; + submittedArchiveForwardHistoryEpoch = null; for (Map.Entry entry : archiveForwardPayloadOwners.entrySet()) { AccountAssetPreparedBlockPayloadOwner owner = entry.getValue(); @@ -754,6 +757,8 @@ private Long publishArchiveHistoryForFlush() { if (!(blockReverseDiffSink instanceof DurableBlockReverseDiffSink)) { throw new TronDBException("Archive sink cannot prove durable history before checkpoint"); } + FrozenBatch frozenForward = archiveBlockProjectionPreparer == null + ? null : freezeArchiveForwardFlushRange(); Chainbase stateDatabase = dbs.stream() .filter(db -> ArchiveStoreScope.isStateDatabase(db.getDbName())) .findFirst() @@ -787,8 +792,22 @@ private Long publishArchiveHistoryForFlush() { try { DurableBlockReverseDiffSink durableSink = (DurableBlockReverseDiffSink) blockReverseDiffSink; - durableSink.acceptAll(prepared); + if (frozenForward == null || submittedArchiveForwardHistoryEpoch == null) { + durableSink.acceptAll(prepared); + if (frozenForward != null) { + submittedArchiveForwardHistoryEpoch = last.getEpoch(); + } + } else if (submittedArchiveForwardHistoryEpoch.longValue() != last.getEpoch()) { + throw new ArchivePersistenceException( + "Submitted archive history target does not match frozen forward range"); + } durableSink.awaitCommitted(last.getEpoch()); + if (frozenForward != null) { + DurableHistoryMarkerRangeReceipt receipt = + durableSink.createMarkerRangeReceipt(prepared.size()); + sealPendingArchiveForwardFlush(receipt); + submittedArchiveForwardHistoryEpoch = null; + } } catch (RuntimeException e) { throw new TronDBException("Archive history durability gate failed", e); } From efb148f8a421bb9977a2599388277e14fa49a1c1 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 20:31:59 +0800 Subject: [PATCH 021/161] test(chainbase): cover durable flush sealing Verify direct and asynchronous receipt authority, disabled behavior, and retry ordering across durable wait and receipt failures without duplicate history submission. --- .../archive/AsyncArchiveHistorySinkTest.java | 19 +++ .../DurableHistoryMarkerRangeReceiptTest.java | 4 +- .../SnapshotOldValueCollectorTest.java | 114 +++++++++++++++++- 3 files changed, 134 insertions(+), 3 deletions(-) diff --git a/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java b/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java index cd5bc38d71a..807c77470a8 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -57,6 +58,24 @@ public void persistsOneFlushRangeAsOneDurabilityBatch() throws Exception { } } + @Test + public void createsReceiptFromTheSameDurableWriterAuthority() throws Exception { + Path archive = temporaryFolder.newFolder("async-receipt").toPath(); + ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, + ArchiveStoreScope.getStateDatabases()); + try (AsyncArchiveHistorySink sink = new AsyncArchiveHistorySink(writer, 1)) { + BlockReverseDiff committed = diff(1); + sink.accept(committed); + sink.awaitCommitted(1); + + List receipt = sink.createMarkerRangeReceipt(1) + .read(Collections.singletonList(committed.getMeta())); + + assertEquals(1, receipt.size()); + assertEquals(committed.getMeta(), receipt.get(0).getMeta()); + } + } + @Test public void removesQueuedForkHeadWithoutPublishingIt() throws Exception { Path archive = temporaryFolder.newFolder("queued-reorg").toPath(); diff --git a/framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceiptTest.java b/framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceiptTest.java index 72b770d30a1..ee376db75c5 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceiptTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceiptTest.java @@ -29,7 +29,7 @@ public void readsOnlyExactDurableRangeAndReopensWithIdenticalReceipt() throws Ex archive, 4096, new java.util.LinkedHashSet<>(participants()))) { writer.acceptAll(Arrays.asList(diff(1), diff(2), diff(3), diff(4))); List receipt = - new DurableHistoryMarkerRangeReceipt(writer, 2).read(expected); + writer.createMarkerRangeReceipt(2).read(expected); assertEquals(Arrays.asList(2L, 3L), epochs(receipt)); HistoryCommitMarkerCodec codec = new HistoryCommitMarkerCodec(); receipt.forEach(marker -> encoded.add(codec.encode(marker))); @@ -38,7 +38,7 @@ public void readsOnlyExactDurableRangeAndReopensWithIdenticalReceipt() throws Ex try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter( archive, 4096, new java.util.LinkedHashSet<>(participants()))) { List receipt = - new DurableHistoryMarkerRangeReceipt(reopened, 2).read(expected); + reopened.createMarkerRangeReceipt(2).read(expected); HistoryCommitMarkerCodec codec = new HistoryCommitMarkerCodec(); assertArrayEquals(encoded.get(0), codec.encode(receipt.get(0))); assertArrayEquals(encoded.get(1), codec.encode(receipt.get(1))); diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index 0cf47c8c563..d8eae24567c 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -595,6 +595,57 @@ public void archiveDurabilityFailurePreventsCheckpointAndRefresh() throws Except manager.shutdown(); } + @Test + public void flushRetriesDurabilityAndReceiptWithoutResubmittingHistory() throws Exception { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + manager.setUnChecked(false); + CheckTmpStore checkpoint = mock(CheckTmpStore.class); + DbSourceInter checkpointDb = mock(DbSourceInter.class); + when(checkpointDb.iterator()).thenReturn(Collections.emptyIterator()); + when(checkpoint.getDbSource()).thenReturn(checkpointDb); + manager.setCheckTmpStore(checkpoint); + ArchiveHistoryWriter writer = new ArchiveHistoryWriter( + temporaryFolder.newFolder("flush-receipt-retry").toPath(), 4096, + ArchiveStoreScope.getStateDatabases()); + FailOnceReceiptSink sink = new FailOnceReceiptSink(writer); + manager.installArchiveCollector(new SnapshotOldValueCollector(), sink); + AtomicReference prepared = + new AtomicReference<>(); + manager.installArchiveProjectionPreparer(view -> { + AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = + sealReadyProjection(view.getMeta(), view); + prepared.set(projection); + return projection; + }); + + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + commitBlock(manager, database, meta, "key-1"); + setFlushCount(manager, 1); + + assertThrows(TronError.class, manager::flush); + assertTrue(manager.hasPendingArchiveForwardFlush()); + verify(checkpoint, never()).updateByBatch(any(Map.class)); + + assertThrows(TronError.class, manager::flush); + assertTrue(manager.hasPendingArchiveForwardFlush()); + verify(checkpoint, never()).updateByBatch(any(Map.class)); + + manager.flush(); + + assertEquals(1, sink.acceptAllCalls); + assertEquals(3, sink.awaitCalls); + assertEquals(2, sink.receiptCalls); + verify(prepared.get()).completeSeal(); + assertEquals(1, manager.claimArchiveForwardFlushPayloads().size()); + assertFalse(manager.hasPendingArchiveForwardFlush()); + manager.shutdown(); + writer.close(); + } + @Test public void fastPopDiscardsPreparedPayloadWithoutRevertingDurableHistory() { MemoryDb memoryDb = new MemoryDb("code"); @@ -1111,7 +1162,7 @@ private static AccountAssetBlockProjectionBridge.PreparedBlockProjection prepare private static AccountAssetBlockProjectionBridge.PreparedBlockProjection sealReadyProjection( BlockSnapshotMeta meta, BlockChangeView view) { AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = - preparedProjection(meta, mock(BlockReverseDiff.class)); + preparedProjection(meta, new BlockReverseDiff(meta, Collections.emptyList())); when(projection.previewSealPayload(any(HistoryCommitMarker.class))).thenAnswer(invocation -> { HistoryCommitMarker target = invocation.getArgument(0); if (!meta.equals(target.getMeta())) { @@ -1180,6 +1231,67 @@ private static Map copy(Map source) { return copy; } + private static final class FailOnceReceiptSink implements DurableBlockReverseDiffSink { + private final ArchiveHistoryWriter writer; + private int acceptAllCalls; + private int awaitCalls; + private int receiptCalls; + + private FailOnceReceiptSink(ArchiveHistoryWriter writer) { + this.writer = writer; + } + + @Override + public void accept(BlockReverseDiff diff) { + writer.accept(diff); + } + + @Override + public void acceptAll(List diffs) { + acceptAllCalls++; + writer.acceptAll(diffs); + } + + @Override + public void revert(BlockSnapshotMeta meta) { + writer.revert(meta); + } + + @Override + public void awaitCommitted(long epoch) { + awaitCalls++; + if (awaitCalls == 1) { + throw new ArchivePersistenceException("injected durable wait failure"); + } + writer.awaitCommitted(epoch); + } + + @Override + public DurableHistoryMarkerRangeReceipt createMarkerRangeReceipt(int maxMarkers) { + receiptCalls++; + if (receiptCalls == 1) { + return new DurableHistoryMarkerRangeReceipt( + new DurableHistoryMarkerRangeReceipt.Source() { + @Override + public HistoryCommitMarker marker(long epoch) { + return null; + } + + @Override + public BlockReverseDiff readCommitted(long epoch) { + return writer.readCommitted(epoch); + } + }, maxMarkers); + } + return writer.createMarkerRangeReceipt(maxMarkers); + } + + @Override + public void releaseThrough(long epoch) { + writer.releaseThrough(epoch); + } + } + private static final class MemoryDb implements DB, Flusher { private final String name; private final Map values = new LinkedHashMap<>(); From 9d244b0c1f6551d5dacdcc7e02200a9b3b585f54 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 22:11:44 +0800 Subject: [PATCH 022/161] test(chainbase): assert legacy asset freeze --- .../java/org/tron/core/BandwidthProcessorTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java index cf652af3650..e4ec5071085 100755 --- a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java +++ b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java @@ -723,6 +723,10 @@ public void sameTokenNameOpenConsumeSuccess() { // V2 chainBaseManager.getAssetIssueV2Store().put(assetIssueCapsule.createDbV2Key(), assetIssueCapsule); + AssetIssueCapsule legacyBefore = + chainBaseManager.getAssetIssueStore().get(assetIssueCapsule.createDbKey()); + byte[] legacyBytesBefore = legacyBefore == null + ? null : legacyBefore.getInstance().toByteArray(); AccountCapsule ownerCapsule = new AccountCapsule( @@ -777,6 +781,13 @@ public void sameTokenNameOpenConsumeSuccess() { chainBaseManager.getAssetIssueV2Store().get(assetIssueCapsule.createDbV2Key()); Assert.assertNotNull(assetIssueCapsuleV2); Assert.assertEquals(assetIssueCapsuleV2.getPublicFreeAssetNetUsage(), byteSize); + AssetIssueCapsule legacyAfter = + chainBaseManager.getAssetIssueStore().get(assetIssueCapsule.createDbKey()); + if (legacyBytesBefore == null) { + Assert.assertNull(legacyAfter); + } else { + Assert.assertArrayEquals(legacyBytesBefore, legacyAfter.getInstance().toByteArray()); + } AccountCapsule fromAccount = chainBaseManager.getAccountStore().get(ByteArray.fromHexString(OWNER_ADDRESS)); From 238a59a8bc6f30b4cd02cdbb9fddf29676f63afd Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 22:11:52 +0800 Subject: [PATCH 023/161] feat(chainbase): bind archive exact-26 scope --- .../AccountAssetBlockProjectionBridge.java | 4 +- .../AccountAssetForwardMutationManifest.java | 4 +- .../core/db2/archive/ArchiveBaseManifest.java | 55 +++++++- .../archive/ArchiveParticipantDescriptor.java | 133 ++++++++++++++++++ ...hiveParticipantMutationBatchCollector.java | 4 +- .../core/db2/archive/ArchiveStoreScope.java | 43 +----- .../ArchiveTargetMutationPlanBuilder.java | 4 +- .../DurableHistoryMarkerRangeReceipt.java | 4 +- 8 files changed, 195 insertions(+), 56 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java index 2a054b23e34..6c1e8f0af67 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java @@ -27,9 +27,7 @@ public AccountAssetBlockProjectionBridge(AccountAssetArchiveProjector projector, this.projector = Objects.requireNonNull(projector, "projector"); this.oldPhysicalAssetsSource = Objects.requireNonNull(oldPhysicalAssetsSource, "oldPhysicalAssetsSource"); - List expected = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); - Collections.sort(expected); - participants = Collections.unmodifiableList(expected); + participants = ArchiveParticipantDescriptor.current().getParticipants(); } public PreparedBlockProjection prepare(BlockChangeView view, diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java index 332126cdb9e..a08944a3205 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java @@ -100,9 +100,7 @@ private static boolean samePostValue(PostValue left, PostValue right) { } private static List sortedParticipants() { - List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); - Collections.sort(participants); - return participants; + return ArchiveParticipantDescriptor.current().getParticipants(); } /** One changed account's exact raw input and canonical physical outputs. */ diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java index 44b11564de4..4f7d8e66c50 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java @@ -21,22 +21,25 @@ final class ArchiveBaseManifest { private static final int MAGIC = 0x54414d46; // TAMF - private static final short VERSION = 1; + private static final short VERSION = 2; private static final int MAX_LENGTH = 1024 * 1024; private final Path directory; private final Path path; private final List participants; + private final String scopeIdentity; private BaseIdentity base; ArchiveBaseManifest(Path directory, List participants) throws IOException { this.directory = directory; this.path = directory.resolve("MANIFEST"); this.participants = new ArrayList<>(participants); + this.scopeIdentity = scopeIdentity(this.participants); Files.createDirectories(directory); if (Files.exists(path)) { base = decode(Files.readAllBytes(path)); - if (!this.participants.equals(base.participants)) { + if (!scopeIdentity.equals(base.scopeIdentity) + || !this.participants.equals(base.participants)) { throw new ArchivePersistenceException("Archive manifest participant set mismatch"); } } @@ -51,7 +54,7 @@ synchronized void ensureBase(BlockSnapshotMeta firstArchivedBlock) throws IOExce } return; } - BaseIdentity identity = new BaseIdentity(epoch, hash, participants); + BaseIdentity identity = new BaseIdentity(scopeIdentity, epoch, hash, participants); byte[] encoded = encode(identity); Path temporary = directory.resolve(".MANIFEST-" + UUID.randomUUID()); Files.write(temporary, encoded); @@ -76,6 +79,7 @@ private static byte[] encode(BaseIdentity identity) throws IOException { output.writeShort(VERSION); output.writeShort(0); output.writeInt(0); + writeString(output, identity.scopeIdentity); output.writeLong(identity.epoch); output.write(identity.hash); output.writeInt(identity.participants.size()); @@ -115,6 +119,7 @@ private static BaseIdentity decode(byte[] encoded) throws IOException { || input.readInt() != encoded.length) { throw new ArchivePersistenceException("Unsupported archive manifest header"); } + String scopeIdentity = readString(input, "Archive manifest scope identity is invalid"); long epoch = input.readLong(); byte[] hash = new byte[32]; input.readFully(hash); @@ -141,16 +146,56 @@ private static BaseIdentity decode(byte[] encoded) throws IOException { if (input.available() != Integer.BYTES) { throw new ArchivePersistenceException("Archive manifest payload mismatch"); } - return new BaseIdentity(epoch, hash, participants); + return new BaseIdentity(scopeIdentity, epoch, hash, participants); } } + private static String scopeIdentity(List participants) { + if (ArchiveParticipantDescriptor.current().getParticipants().equals(participants)) { + return ArchiveParticipantDescriptor.FORMAT_ID; + } + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + for (String participant : participants) { + writeString(output, participant); + } + output.flush(); + return "experimental/" + Hashing.sha256().hashBytes(bytes.toByteArray()); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected participant identity encoding failure", + impossible); + } + } + + private static void writeString(DataOutputStream output, String value) throws IOException { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + if (encoded.length == 0 || encoded.length > 1024) { + throw new IllegalArgumentException("Archive manifest string is invalid"); + } + output.writeInt(encoded.length); + output.write(encoded); + } + + private static String readString(DataInputStream input, String error) throws IOException { + int length = input.readInt(); + if (length <= 0 || length > 1024 || length > input.available() - Integer.BYTES) { + throw new ArchivePersistenceException(error); + } + byte[] encoded = new byte[length]; + input.readFully(encoded); + return new String(encoded, StandardCharsets.UTF_8); + } + private static final class BaseIdentity { + private final String scopeIdentity; private final long epoch; private final byte[] hash; private final List participants; - private BaseIdentity(long epoch, byte[] hash, List participants) { + private BaseIdentity(String scopeIdentity, long epoch, byte[] hash, + List participants) { + this.scopeIdentity = scopeIdentity; this.epoch = epoch; this.hash = Arrays.copyOf(hash, hash.length); this.participants = new ArrayList<>(participants); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java new file mode 100644 index 00000000000..f9d97acb016 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java @@ -0,0 +1,133 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** Approved archive participant identity with stable Store IDs and reserved tombstones. */ +final class ArchiveParticipantDescriptor { + + static final String FORMAT_ID = "archive-state/exact-26-abi-tombstone/v1"; + static final int ABI_STORE_ID = 1; + + private static final ArchiveParticipantDescriptor CURRENT = + new ArchiveParticipantDescriptor(); + + private final Map activeByStoreId; + private final Map tombstonesByStoreId; + private final Set activeDatabases; + private final Set excludedDatabases; + private final List participants; + + private ArchiveParticipantDescriptor() { + LinkedHashMap stores = new LinkedHashMap<>(); + stores.put(2, "accountid-index"); + stores.put(3, "account-index"); + stores.put(4, "account"); + stores.put(5, "account-asset"); + stores.put(6, "asset-issue"); + stores.put(7, "asset-issue-v2"); + stores.put(8, "code"); + stores.put(9, "contract-state"); + stores.put(10, "contract"); + stores.put(11, "DelegatedResourceAccountIndex"); + stores.put(12, "DelegatedResource"); + stores.put(13, "delegation"); + stores.put(14, "properties"); + stores.put(15, "exchange"); + stores.put(16, "exchange-v2"); + stores.put(17, "market_account"); + stores.put(18, "market_order"); + stores.put(19, "market_pair_price_to_order"); + stores.put(20, "market_pair_to_price"); + stores.put(21, "proposal"); + stores.put(22, "storage-row"); + stores.put(23, "votes"); + stores.put(24, "witness_schedule"); + stores.put(25, "witness"); + stores.put(26, "nullifier"); + stores.put(27, "IncrementalMerkleTree"); + activeByStoreId = Collections.unmodifiableMap(stores); + + LinkedHashMap tombstones = new LinkedHashMap<>(); + tombstones.put(ABI_STORE_ID, "abi"); + tombstonesByStoreId = Collections.unmodifiableMap(tombstones); + + activeDatabases = Collections.unmodifiableSet( + new LinkedHashSet<>(activeByStoreId.values())); + excludedDatabases = Collections.unmodifiableSet( + new LinkedHashSet<>(tombstonesByStoreId.values())); + List sorted = new ArrayList<>(activeDatabases); + Collections.sort(sorted); + participants = Collections.unmodifiableList(sorted); + validateStoreIds(); + } + + static ArchiveParticipantDescriptor current() { + return CURRENT; + } + + Set getActiveDatabases() { + return activeDatabases; + } + + Set getExcludedDatabases() { + return excludedDatabases; + } + + List getParticipants() { + return participants; + } + + Map getTombstonesByStoreId() { + return tombstonesByStoreId; + } + + int getStoreId(String dbName) { + for (Map.Entry entry : activeByStoreId.entrySet()) { + if (entry.getValue().equals(dbName)) { + return entry.getKey(); + } + } + for (Map.Entry entry : tombstonesByStoreId.entrySet()) { + if (entry.getValue().equals(dbName)) { + return entry.getKey(); + } + } + throw new IllegalArgumentException("Unknown archive database: " + dbName); + } + + void requireExactParticipants(Collection actual) { + List sorted = new ArrayList<>(Objects.requireNonNull(actual, "actual")); + Collections.sort(sorted); + if (!participants.equals(sorted)) { + throw new ArchivePersistenceException( + "Archive participant descriptor does not match " + FORMAT_ID); + } + } + + private void validateStoreIds() { + Set allIds = new LinkedHashSet<>(activeByStoreId.keySet()); + allIds.addAll(tombstonesByStoreId.keySet()); + List expected = new ArrayList<>(); + for (int storeId = 1; storeId <= 27; storeId++) { + expected.add(storeId); + } + if (!allIds.equals(new LinkedHashSet<>(expected)) + || activeDatabases.size() != 26 + || !tombstonesByStoreId.equals( + Collections.singletonMap(ABI_STORE_ID, "abi")) + || !Collections.disjoint(activeDatabases, excludedDatabases) + || !activeDatabases.containsAll( + Arrays.asList("asset-issue", "asset-issue-v2"))) { + throw new IllegalStateException("Invalid exact-26 archive participant descriptor"); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java index 25014f73bae..4472896621e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java @@ -24,9 +24,7 @@ public ArchiveParticipantMutationBatchCollector() { public ArchiveParticipantMutationBatchCollector( AccountAssetForwardProjector accountAssetProjector) { this.accountAssetProjector = accountAssetProjector; - List expected = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); - Collections.sort(expected); - participants = Collections.unmodifiableList(expected); + participants = ArchiveParticipantDescriptor.current().getParticipants(); } public ArchiveParticipantMutationBatch collect(HistoryCommitMarker committedTarget, diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java index f5ebe0f18a8..654dacec998 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveStoreScope.java @@ -11,38 +11,13 @@ /** Explicit classification of every database registered with {@code SnapshotManager}. */ public final class ArchiveStoreScope { - private static final Set STATE_DATABASES = immutableSet( - "accountid-index", - "account-index", - "account", - "account-asset", - "asset-issue", - "asset-issue-v2", - "code", - "contract-state", - "contract", - "DelegatedResourceAccountIndex", - "DelegatedResource", - "delegation", - "properties", - "exchange", - "exchange-v2", - "market_account", - "market_order", - "market_pair_price_to_order", - "market_pair_to_price", - "proposal", - "storage-row", - "votes", - "witness_schedule", - "witness", - "nullifier", - "IncrementalMerkleTree"); + private static final ArchiveParticipantDescriptor DESCRIPTOR = + ArchiveParticipantDescriptor.current(); + private static final Set STATE_DATABASES = DESCRIPTOR.getActiveDatabases(); + private static final Set EXCLUDED_DATABASES = DESCRIPTOR.getExcludedDatabases(); - // Candidate store ID 1 is reserved for abi and must never be reused by state history. - private static final Set EXCLUDED_DATABASES = immutableSet("abi"); - - private static final Set NON_STATE_DATABASES = immutableSet( + private static final Set NON_STATE_DATABASES = Collections.unmodifiableSet( + new LinkedHashSet<>(Arrays.asList( "account-trace", "accountTrie", "balance-trace", @@ -55,7 +30,7 @@ public final class ArchiveStoreScope { "trans-cache", "transactionHistoryStore", "transactionRetStore", - "tree-block-index"); + "tree-block-index"))); private ArchiveStoreScope() { } @@ -105,8 +80,4 @@ public static void validate(Collection databases) { "Archive state scope has unclassified Chainbase dbName(s): " + unknown); } } - - private static Set immutableSet(String... values) { - return Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(values))); - } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java index 15fe91fd712..fcf7d85a268 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java @@ -16,9 +16,7 @@ final class ArchiveTargetMutationPlanBuilder { private final List participants; ArchiveTargetMutationPlanBuilder() { - List expected = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); - Collections.sort(expected); - participants = Collections.unmodifiableList(expected); + participants = ArchiveParticipantDescriptor.current().getParticipants(); } ArchiveTargetMutationPlan build(HistoryCommitMarker committedTarget, diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceipt.java b/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceipt.java index 84221639d9f..f90e0927e51 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceipt.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceipt.java @@ -25,9 +25,7 @@ public DurableHistoryMarkerRangeReceipt(ArchiveHistoryWriter writer, int maxMark throw new IllegalArgumentException("maxMarkers must be positive"); } this.maxMarkers = maxMarkers; - List expected = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); - Collections.sort(expected); - participants = Collections.unmodifiableList(expected); + participants = ArchiveParticipantDescriptor.current().getParticipants(); } public List seal(FrozenBatch batch) { From 64f8710dc101578ae8c0dee25fb21de7fb513832 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 22:11:56 +0800 Subject: [PATCH 024/161] test(chainbase): reject stale archive scopes --- .../ArchiveParticipantDescriptorTest.java | 93 +++++++++++++++++++ .../ArchiveTargetMutationPlanBuilderTest.java | 15 +++ 2 files changed, 108 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java new file mode 100644 index 00000000000..fc07e236495 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java @@ -0,0 +1,93 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.common.hash.Hashing; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ArchiveParticipantDescriptorTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void definesExact26WithStableAbiTombstoneAndLegacyAsset() { + ArchiveParticipantDescriptor descriptor = ArchiveParticipantDescriptor.current(); + + assertEquals(26, descriptor.getParticipants().size()); + assertEquals(ArchiveParticipantDescriptor.ABI_STORE_ID, + descriptor.getStoreId("abi")); + assertEquals(6, descriptor.getStoreId("asset-issue")); + assertEquals(7, descriptor.getStoreId("asset-issue-v2")); + assertEquals("abi", descriptor.getTombstonesByStoreId().get(1)); + assertTrue(descriptor.getParticipants().contains("asset-issue")); + assertTrue(descriptor.getParticipants().contains("asset-issue-v2")); + assertFalse(descriptor.getParticipants().contains("abi")); + assertEquals("archive-state/exact-26-abi-tombstone/v1", + ArchiveParticipantDescriptor.FORMAT_ID); + } + + @Test + public void rejectsOldExact27AndV2OnlyExact25ParticipantSets() { + ArchiveParticipantDescriptor descriptor = ArchiveParticipantDescriptor.current(); + List exact27 = new ArrayList<>(descriptor.getParticipants()); + exact27.add("abi"); + List exact25 = new ArrayList<>(descriptor.getParticipants()); + exact25.remove("asset-issue"); + + assertThrows(ArchivePersistenceException.class, + () -> descriptor.requireExactParticipants(exact27)); + assertThrows(ArchivePersistenceException.class, + () -> descriptor.requireExactParticipants(exact25)); + descriptor.requireExactParticipants(descriptor.getParticipants()); + } + + @Test + public void manifestBindsApprovedScopeAndRejectsLegacyVersion() throws Exception { + List participants = ArchiveParticipantDescriptor.current().getParticipants(); + Path archive = temporaryFolder.newFolder("exact-26-manifest").toPath(); + ArchiveBaseManifest manifest = new ArchiveBaseManifest(archive, participants); + manifest.ensureBase(meta(1)); + + byte[] encoded = Files.readAllBytes(archive.resolve("MANIFEST")); + assertEquals(2, ByteBuffer.wrap(encoded, Integer.BYTES, Short.BYTES).getShort()); + new ArchiveBaseManifest(archive, participants); + + List oldExact27 = new ArrayList<>(participants); + oldExact27.add("abi"); + Collections.sort(oldExact27); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveBaseManifest(archive, oldExact27)); + + ByteBuffer.wrap(encoded).putShort(Integer.BYTES, (short) 1); + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + ByteBuffer.wrap(encoded, encoded.length - Integer.BYTES, Integer.BYTES) + .putInt(Hashing.crc32c().hashBytes(payload).asInt()); + Files.write(archive.resolve("MANIFEST"), encoded); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveBaseManifest(archive, participants)); + } + + private static BlockSnapshotMeta meta(long epoch) { + return new BlockSnapshotMeta(epoch, epoch, hash((int) epoch), + hash((int) epoch - 1), epoch * 1_000L); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java index 6fb6db7cee5..b063326fe55 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java @@ -80,6 +80,21 @@ public void rejectsTargetIdentityAndExactParticipantSetMismatch() { assertThrows(ArchivePersistenceException.class, () -> new ArchiveTargetMutationPlanBuilder().build(incompleteTarget, new ArchiveParticipantMutationBatch(incompleteTarget, Collections.emptyList()))); + + List oldExact27 = new ArrayList<>(participants()); + oldExact27.add("abi"); + Collections.sort(oldExact27); + HistoryCommitMarker oldTarget = marker(1, oldExact27); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveTargetMutationPlanBuilder().build(oldTarget, + new ArchiveParticipantMutationBatch(oldTarget, Collections.emptyList()))); + + List v2OnlyExact25 = new ArrayList<>(participants()); + v2OnlyExact25.remove("asset-issue"); + HistoryCommitMarker v2OnlyTarget = marker(1, v2OnlyExact25); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveTargetMutationPlanBuilder().build(v2OnlyTarget, + new ArchiveParticipantMutationBatch(v2OnlyTarget, Collections.emptyList()))); } @Test From ced9da7967008686230bc82ac95a9bff7400921e Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 22:29:53 +0800 Subject: [PATCH 025/161] refactor(chainbase): centralize archive scope identity --- .../core/db2/archive/ArchiveBaseManifest.java | 16 +--------------- .../archive/ArchiveParticipantDescriptor.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java index 4f7d8e66c50..6a7c633c0ed 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java @@ -151,21 +151,7 @@ private static BaseIdentity decode(byte[] encoded) throws IOException { } private static String scopeIdentity(List participants) { - if (ArchiveParticipantDescriptor.current().getParticipants().equals(participants)) { - return ArchiveParticipantDescriptor.FORMAT_ID; - } - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - DataOutputStream output = new DataOutputStream(bytes); - for (String participant : participants) { - writeString(output, participant); - } - output.flush(); - return "experimental/" + Hashing.sha256().hashBytes(bytes.toByteArray()); - } catch (IOException impossible) { - throw new IllegalStateException("Unexpected participant identity encoding failure", - impossible); - } + return ArchiveParticipantDescriptor.scopeIdentity(participants); } private static void writeString(DataOutputStream output, String value) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java index f9d97acb016..139ac4445e6 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java @@ -1,5 +1,8 @@ package org.tron.core.db2.archive; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -86,6 +89,18 @@ List getParticipants() { return participants; } + static String scopeIdentity(List participants) { + if (current().getParticipants().equals(participants)) { + return FORMAT_ID; + } + Hasher hasher = Hashing.sha256().newHasher(); + for (String participant : participants) { + byte[] encoded = participant.getBytes(StandardCharsets.UTF_8); + hasher.putInt(encoded.length).putBytes(encoded); + } + return "experimental/" + hasher.hash(); + } + Map getTombstonesByStoreId() { return tombstonesByStoreId; } From 44e7255ebd5fe9aac2bef422b41a54a4515c7205 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 22:29:59 +0800 Subject: [PATCH 026/161] feat(chainbase): bind archive progress scope --- .../db2/archive/ArchiveProgressEnvelope.java | 17 +++++++++ .../archive/ArchiveProgressEnvelopeCodec.java | 22 ++++++++---- .../archive/ArchiveProgressEnvelopeTest.java | 35 +++++++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java index 92514ff3700..c7a720b137c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelope.java @@ -23,6 +23,7 @@ public enum Kind { private final byte[] payloadDigest; private final byte[] mutationPlanDigest; private final List participants; + private final String scopeIdentity; public ArchiveProgressEnvelope(Kind kind, String participant, long epoch, byte[] blockHash, byte[] batchId, byte[] payloadDigest, List participants) { @@ -32,6 +33,13 @@ public ArchiveProgressEnvelope(Kind kind, String participant, long epoch, byte[] public ArchiveProgressEnvelope(Kind kind, String participant, long epoch, byte[] blockHash, byte[] batchId, byte[] payloadDigest, byte[] mutationPlanDigest, List participants) { + this(kind, participant, epoch, blockHash, batchId, payloadDigest, mutationPlanDigest, + participants, ArchiveParticipantDescriptor.scopeIdentity(participants)); + } + + ArchiveProgressEnvelope(Kind kind, String participant, long epoch, byte[] blockHash, + byte[] batchId, byte[] payloadDigest, byte[] mutationPlanDigest, + List participants, String scopeIdentity) { this.kind = Objects.requireNonNull(kind, "kind"); if (epoch < 0) { throw new IllegalArgumentException("Archive progress epoch must be non-negative"); @@ -43,6 +51,10 @@ public ArchiveProgressEnvelope(Kind kind, String participant, long epoch, byte[] this.mutationPlanDigest = mutationPlanDigest == null ? null : exactBytes(mutationPlanDigest, 32, "mutationPlanDigest"); this.participants = validateParticipants(participants); + if (scopeIdentity == null || scopeIdentity.isEmpty()) { + throw new IllegalArgumentException("Archive scope identity must not be empty"); + } + this.scopeIdentity = scopeIdentity; if (kind != Kind.PARTICIPANT_PROGRESS) { if (participant != null) { throw new IllegalArgumentException("Global archive progress must not name one participant"); @@ -90,6 +102,10 @@ public List getParticipants() { return participants; } + public String getScopeIdentity() { + return scopeIdentity; + } + public void requireIdentity(Kind expectedKind, String expectedParticipant, long expectedEpoch, byte[] expectedBlockHash, byte[] expectedBatchId, byte[] expectedPayloadDigest, List expectedParticipants) { @@ -105,6 +121,7 @@ public void requireIdentity(Kind expectedKind, String expectedParticipant, long || !Arrays.equals(batchId, expectedBatchId) || !Arrays.equals(payloadDigest, expectedPayloadDigest) || !Arrays.equals(mutationPlanDigest, expectedMutationPlanDigest) + || !scopeIdentity.equals(ArchiveParticipantDescriptor.scopeIdentity(expectedParticipants)) || !participants.equals(expectedParticipants)) { throw new ArchivePersistenceException("Archive progress identity mismatch"); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java index 535f901d1be..66cee42c666 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeCodec.java @@ -18,8 +18,7 @@ public final class ArchiveProgressEnvelopeCodec { private static final int MAGIC = 0x54415047; // TAPG - private static final short VERSION_1 = 1; - private static final short VERSION_2 = 2; + private static final short VERSION = 3; private static final int HEADER_LENGTH = 12; private static final int MAX_FIELD_LENGTH = 1024; private static final int MAX_PARTICIPANTS = 1024; @@ -31,14 +30,16 @@ public byte[] encode(ArchiveProgressEnvelope envelope) { DataOutputStream output = new DataOutputStream(bytes); output.writeInt(MAGIC); byte[] mutationPlanDigest = envelope.getMutationPlanDigest(); - output.writeShort(mutationPlanDigest == null ? VERSION_1 : VERSION_2); + output.writeShort(VERSION); output.writeByte(kindCode(envelope.getKind())); output.writeByte(0); output.writeInt(0); + writeString(output, envelope.getScopeIdentity()); output.writeLong(envelope.getEpoch()); output.write(envelope.getBlockHash()); output.write(envelope.getBatchId()); output.write(envelope.getPayloadDigest()); + output.writeBoolean(mutationPlanDigest != null); if (mutationPlanDigest != null) { output.write(mutationPlanDigest); } @@ -80,18 +81,23 @@ public ArchiveProgressEnvelope decode(byte[] encoded) { DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); int magic = input.readInt(); short version = input.readShort(); - if (magic != MAGIC || version != VERSION_1 && version != VERSION_2) { + if (magic != MAGIC || version != VERSION) { throw new IllegalArgumentException("Unsupported archive progress envelope header"); } Kind kind = decodeKind(input.readUnsignedByte()); if (input.readUnsignedByte() != 0 || input.readInt() != encoded.length) { throw new IllegalArgumentException("Unsupported archive progress envelope header"); } + String scopeIdentity = readString(input, false); long epoch = input.readLong(); byte[] blockHash = readExact(input, 32); byte[] batchId = readExact(input, 16); byte[] payloadDigest = readExact(input, 32); - byte[] mutationPlanDigest = version == VERSION_2 ? readExact(input, 32) : null; + int hasMutationPlanDigest = input.readUnsignedByte(); + if (hasMutationPlanDigest > 1) { + throw new IllegalArgumentException("Archive progress plan digest marker is invalid"); + } + byte[] mutationPlanDigest = hasMutationPlanDigest == 1 ? readExact(input, 32) : null; String participant = readString(input, true); int count = input.readInt(); if (count <= 0 || count > MAX_PARTICIPANTS) { @@ -104,8 +110,12 @@ public ArchiveProgressEnvelope decode(byte[] encoded) { if (input.available() != Integer.BYTES) { throw new IllegalArgumentException("Archive progress envelope payload mismatch"); } + String expectedScope = ArchiveParticipantDescriptor.scopeIdentity(participants); + if (!scopeIdentity.equals(expectedScope)) { + throw new IllegalArgumentException("Archive progress scope identity mismatch"); + } return new ArchiveProgressEnvelope(kind, participant.isEmpty() ? null : participant, epoch, - blockHash, batchId, payloadDigest, mutationPlanDigest, participants); + blockHash, batchId, payloadDigest, mutationPlanDigest, participants, scopeIdentity); } catch (EOFException truncated) { throw new IllegalArgumentException("Archive progress envelope is truncated", truncated); } catch (IOException invalid) { diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java index 51bd110ad74..7f4516a7834 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveProgressEnvelopeTest.java @@ -5,6 +5,8 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.google.common.hash.Hashing; +import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -40,6 +42,32 @@ public void deterministicallyRoundTripsCheckpointParticipantAndReaderProgress() ArchiveProgressEnvelope bound = new ArchiveProgressEnvelope(Kind.APPLY_CHECKPOINT, null, 10, bytes(32, 1), bytes(16, 2), bytes(32, 3), bytes(32, 4), PARTICIPANTS); assertEnvelope(bound, codec.decode(codec.encode(bound))); + assertEquals(ArchiveParticipantDescriptor.scopeIdentity(PARTICIPANTS), + codec.decode(first).getScopeIdentity()); + + List approvedParticipants = ArchiveParticipantDescriptor.current().getParticipants(); + ArchiveProgressEnvelope approved = new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, + 10, bytes(32, 1), bytes(16, 2), bytes(32, 3), approvedParticipants); + assertEquals(ArchiveParticipantDescriptor.FORMAT_ID, + codec.decode(codec.encode(approved)).getScopeIdentity()); + } + + @Test + public void rejectsLegacyVersionAndSameParticipantsWithDifferentScope() { + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + byte[] legacy = codec.encode(checkpoint(10, 1)); + ByteBuffer.wrap(legacy).putShort(4, (short) 2); + refreshChecksum(legacy); + assertThrows(IllegalArgumentException.class, () -> codec.decode(legacy)); + + ArchiveProgressEnvelope substituted = new ArchiveProgressEnvelope( + Kind.APPLY_CHECKPOINT, null, 10, bytes(32, 1), bytes(16, 2), bytes(32, 3), null, + PARTICIPANTS, "experimental/substituted-scope"); + assertThrows(IllegalArgumentException.class, + () -> codec.decode(codec.encode(substituted))); + assertThrows(ArchivePersistenceException.class, + () -> substituted.requireIdentity(Kind.APPLY_CHECKPOINT, null, 10, bytes(32, 1), + bytes(16, 2), bytes(32, 3), PARTICIPANTS)); } @Test @@ -148,6 +176,13 @@ private static void assertEnvelope(ArchiveProgressEnvelope expected, assertArrayEquals(expected.getBatchId(), actual.getBatchId()); assertArrayEquals(expected.getPayloadDigest(), actual.getPayloadDigest()); assertArrayEquals(expected.getMutationPlanDigest(), actual.getMutationPlanDigest()); + assertEquals(expected.getScopeIdentity(), actual.getScopeIdentity()); assertEquals(expected.getParticipants(), actual.getParticipants()); } + + private static void refreshChecksum(byte[] encoded) { + int payloadLength = encoded.length - Integer.BYTES; + int checksum = Hashing.crc32c().hashBytes(encoded, 0, payloadLength).asInt(); + ByteBuffer.wrap(encoded, payloadLength, Integer.BYTES).putInt(checksum); + } } From 2b6ef62f31777055ff304eb4acfd410ce76cb7be Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 22:30:06 +0800 Subject: [PATCH 027/161] feat(chainbase): bind serving generation scope --- .../LatestStateGenerationCoordinator.java | 4 ++ .../PersistentServingKeyIndexCatalog.java | 1 + .../PersistentServingKeyIndexGeneration.java | 40 +++++++++++---- .../archive/ServingKeyIndexGeneration.java | 10 ++++ ...rsistentServingKeyIndexGenerationTest.java | 51 +++++++++++++++++++ 5 files changed, 95 insertions(+), 11 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java index 6037bdcb022..c1ac022be45 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java @@ -159,6 +159,8 @@ private ArchiveProgressEnvelope readAuthority() throws IOException { ArchiveProgressEnvelope authority = Objects.requireNonNull(authorityReader.read(), "reader-visible authority"); if (authority.getKind() != Kind.READER_VISIBLE + || !ArchiveParticipantDescriptor.scopeIdentity(participants) + .equals(authority.getScopeIdentity()) || !participants.equals(authority.getParticipants())) { throw new ArchivePersistenceException("Invalid reader-visible generation authority"); } @@ -193,6 +195,7 @@ private static boolean sameAuthority(ArchiveProgressEnvelope left, && Arrays.equals(left.getBlockHash(), right.getBlockHash()) && Arrays.equals(left.getBatchId(), right.getBatchId()) && Arrays.equals(left.getPayloadDigest(), right.getPayloadDigest()) + && left.getScopeIdentity().equals(right.getScopeIdentity()) && left.getParticipants().equals(right.getParticipants()); } @@ -275,6 +278,7 @@ private synchronized void validateServing(PersistentServingKeyIndexGeneration se || authority.getEpoch() != serving.getIndexedThrough() || !Arrays.equals(authority.getBlockHash(), serving.getHeadHash()) || !Arrays.equals(sourceIdentityDigest, serving.getLatestSourceIdentityDigest()) + || !authority.getScopeIdentity().equals(serving.getScopeIdentity()) || !authority.getParticipants().equals(serving.getParticipatingDatabases())) { throw new IllegalArgumentException( "Serving generation does not match latest-state candidate"); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java index 1162474196a..98b360d321d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java @@ -317,6 +317,7 @@ private static void validateReaderVisibility(PersistentServingKeyIndexGeneration ArchiveProgressEnvelope readerVisible) { Objects.requireNonNull(readerVisible, "readerVisible"); if (readerVisible.getKind() != ArchiveProgressEnvelope.Kind.READER_VISIBLE + || !readerVisible.getScopeIdentity().equals(generation.getScopeIdentity()) || !readerVisible.getParticipants().equals(generation.getParticipatingDatabases()) || generation.getIndexedThrough() > readerVisible.getEpoch() || generation.getIndexedThrough() == readerVisible.getEpoch() diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java index 25e9409a0fa..9b8a90ae084 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java @@ -33,8 +33,7 @@ public final class PersistentServingKeyIndexGeneration implements ServingKeyIndex { private static final int MAGIC = 0x534b4947; // SKIG - private static final short VERSION = 2; - private static final short LEGACY_VERSION = 1; + private static final short VERSION = 3; private static final int MAX_MANIFEST_SIZE = 1024 * 1024; private static final byte DATA_PREFIX = 1; private static final byte[] PRESENT = new byte[]{1}; @@ -83,6 +82,7 @@ public static PersistentServingKeyIndexGeneration build(Path directory, String g Objects.requireNonNull(committed, "committed"); Objects.requireNonNull(reader, "reader"); List participants = sortedParticipants(participatingDatabases); + String scopeIdentity = ArchiveParticipantDescriptor.scopeIdentity(participants); if (generationId == null || generationId.isEmpty() || baseEpoch < 0) { throw new IllegalArgumentException("Invalid serving generation identity"); } @@ -96,6 +96,7 @@ public static PersistentServingKeyIndexGeneration build(Path directory, String g MessageDigest sourceDigest = sha256(); updateLong(sourceDigest, baseEpoch); sourceDigest.update(baseHash); + updateStringDigest(sourceDigest, scopeIdentity); updateParticipantDigest(sourceDigest, participants); long previousEpoch = baseEpoch; long previousBlock = baseEpoch; @@ -133,8 +134,8 @@ public static PersistentServingKeyIndexGeneration build(Path directory, String g buildOptions.close(); } - Descriptor descriptor = new Descriptor(generationId, baseEpoch, previousEpoch, previousHash, - sourceDigest.digest(), latestSourceIdentityDigest, participants, keyChanges); + Descriptor descriptor = new Descriptor(scopeIdentity, generationId, baseEpoch, previousEpoch, + previousHash, sourceDigest.digest(), latestSourceIdentityDigest, participants, keyChanges); persistDescriptor(directory, descriptor); HistorySegmentStore.syncDirectory(directory); return open(directory); @@ -213,6 +214,10 @@ public List getParticipatingDatabases() { return descriptor.participants; } + public String getScopeIdentity() { + return descriptor.scopeIdentity; + } + public long getKeyChangeCount() { return descriptor.keyChanges; } @@ -357,6 +362,12 @@ private static void updateParticipantDigest(MessageDigest digest, List d } } + private static void updateStringDigest(MessageDigest digest, String value) { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + updateLong(digest, encoded.length); + digest.update(encoded); + } + private static void updateLong(MessageDigest digest, long value) { digest.update(ByteBuffer.allocate(Long.BYTES).putLong(value).array()); } @@ -414,6 +425,7 @@ private static byte[] encodeDescriptor(Descriptor descriptor) { output.writeInt(MAGIC); output.writeShort(VERSION); output.writeShort(0); + output.writeUTF(descriptor.scopeIdentity); output.writeUTF(descriptor.generationId); output.writeLong(descriptor.indexedFrom); output.writeLong(descriptor.indexedThrough); @@ -450,9 +462,10 @@ private static Descriptor decodeDescriptor(byte[] encoded) { throw new IllegalArgumentException("Unsupported serving index manifest"); } short version = input.readShort(); - if (version != VERSION && version != LEGACY_VERSION || input.readShort() != 0) { + if (version != VERSION || input.readShort() != 0) { throw new IllegalArgumentException("Unsupported serving index manifest"); } + String scopeIdentity = input.readUTF(); String generationId = input.readUTF(); long from = input.readLong(); long through = input.readLong(); @@ -461,9 +474,7 @@ private static Descriptor decodeDescriptor(byte[] encoded) { input.readFully(headHash); input.readFully(sourceDigest); byte[] latestSourceIdentityDigest = new byte[32]; - if (version >= VERSION) { - input.readFully(latestSourceIdentityDigest); - } + input.readFully(latestSourceIdentityDigest); long keyChanges = input.readLong(); int count = input.readInt(); if (generationId.isEmpty() || from < 0 || through < from || keyChanges < 0 @@ -477,14 +488,19 @@ private static Descriptor decodeDescriptor(byte[] encoded) { if (input.available() != Integer.BYTES) { throw new IllegalArgumentException("Serving index manifest payload mismatch"); } - return new Descriptor(generationId, from, through, headHash, sourceDigest, - latestSourceIdentityDigest, sortedParticipants(participants), keyChanges); + List sorted = sortedParticipants(participants); + if (!scopeIdentity.equals(ArchiveParticipantDescriptor.scopeIdentity(sorted))) { + throw new IllegalArgumentException("Serving index manifest scope identity mismatch"); + } + return new Descriptor(scopeIdentity, generationId, from, through, headHash, sourceDigest, + latestSourceIdentityDigest, sorted, keyChanges); } catch (IOException invalid) { throw new IllegalArgumentException("Serving index manifest is truncated", invalid); } } private static final class Descriptor { + private final String scopeIdentity; private final String generationId; private final long indexedFrom; private final long indexedThrough; @@ -494,9 +510,11 @@ private static final class Descriptor { private final List participants; private final long keyChanges; - private Descriptor(String generationId, long indexedFrom, long indexedThrough, + private Descriptor(String scopeIdentity, String generationId, long indexedFrom, + long indexedThrough, byte[] headHash, byte[] sourceDigest, byte[] latestSourceIdentityDigest, List participants, long keyChanges) { + this.scopeIdentity = scopeIdentity; this.generationId = generationId; this.indexedFrom = indexedFrom; this.indexedThrough = indexedThrough; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java index 7695434f3db..cd2bae398e6 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java @@ -87,6 +87,8 @@ public static ServingKeyIndexGeneration rebuild(String generationId, long baseEp List participatingDatabases = expectedParticipatingDatabases == null ? null : sortedParticipants(expectedParticipatingDatabases); if (participatingDatabases != null) { + updateStringDigest(sourceDigest, + ArchiveParticipantDescriptor.scopeIdentity(participatingDatabases)); updateParticipantDigest(sourceDigest, participatingDatabases); } @@ -103,6 +105,8 @@ public static ServingKeyIndexGeneration rebuild(String generationId, long baseEp if (participatingDatabases == null) { participatingDatabases = marker.getDatabases(); validateParticipantSet(participatingDatabases); + updateStringDigest(sourceDigest, + ArchiveParticipantDescriptor.scopeIdentity(participatingDatabases)); updateParticipantDigest(sourceDigest, participatingDatabases); } else if (!participatingDatabases.equals(marker.getDatabases())) { throw new IllegalArgumentException( @@ -349,6 +353,12 @@ private static void updateParticipantDigest(MessageDigest digest, List d } } + private static void updateStringDigest(MessageDigest digest, String value) { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + updateLong(digest, encoded.length); + digest.update(encoded); + } + private static void validateParticipantSet(List databases) { if (databases.isEmpty()) { throw new IllegalArgumentException("Serving index participant set must not be empty"); diff --git a/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java b/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java index 32f2b03db1e..02a65d1b87f 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java @@ -6,7 +6,9 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.google.common.hash.Hashing; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -54,6 +56,8 @@ public void persistsExactKeyChangesAndSourceIdentityAcrossReopen() throws Except assertArrayEquals(hash(3), generation.getHeadHash()); assertArrayEquals(expectedDigest, generation.getAuthoritativePrefixDigest()); assertArrayEquals(hash(77), generation.getLatestSourceIdentityDigest()); + assertEquals(ArchiveParticipantDescriptor.scopeIdentity(PARTICIPANTS), + generation.getScopeIdentity()); assertTrue(generation.isLatestSourceIdentityBound()); assertEquals(4, generation.getKeyChangeCount()); assertEquals(1, change(generation, "account", bytes("hot"), 0, 3)); @@ -67,6 +71,8 @@ public void persistsExactKeyChangesAndSourceIdentityAcrossReopen() throws Except try (PersistentServingKeyIndexGeneration reopened = PersistentServingKeyIndexGeneration.open(generationPath)) { + assertEquals(ArchiveParticipantDescriptor.scopeIdentity(PARTICIPANTS), + reopened.getScopeIdentity()); assertArrayEquals(hash(77), reopened.getLatestSourceIdentityDigest()); assertEquals(3, change(reopened, "account", bytes("hot"), 2, 3)); assertThrows(UnsupportedOperationException.class, @@ -75,6 +81,45 @@ public void persistsExactKeyChangesAndSourceIdentityAcrossReopen() throws Except } } + @Test + public void rejectsLegacyAndSubstitutedGenerationManifestScope() throws Exception { + Path root = temporaryFolder.newFolder("generation-scope").toPath(); + Path approvedPath = root.resolve("approved-generation"); + try (PersistentServingKeyIndexGeneration approved = + PersistentServingKeyIndexGeneration.build(approvedPath, "approved", 0, hash(0), + Collections.emptyList(), location -> { + throw new AssertionError("empty prefix must not read an index record"); + }, ArchiveParticipantDescriptor.current().getParticipants())) { + assertEquals(ArchiveParticipantDescriptor.FORMAT_ID, approved.getScopeIdentity()); + } + try (Fixture fixture = new Fixture(root.resolve("authoritative"))) { + fixture.append(1, group("account", bytes("key"))); + fixture.sync(); + Path generationPath = root.resolve("generation"); + try (PersistentServingKeyIndexGeneration ignored = + PersistentServingKeyIndexGeneration.build(generationPath, "generation", 0, hash(0), + fixture.markers, fixture.index::read, PARTICIPANTS)) { + // Close before mutating the durable manifest. + } + Path manifest = generationPath.resolve("generation.meta"); + byte[] valid = Files.readAllBytes(manifest); + + byte[] legacy = Arrays.copyOf(valid, valid.length); + ByteBuffer.wrap(legacy).putShort(4, (short) 2); + refreshChecksum(legacy); + Files.write(manifest, legacy); + assertThrows(ArchivePersistenceException.class, + () -> PersistentServingKeyIndexGeneration.open(generationPath)); + + byte[] substituted = Arrays.copyOf(valid, valid.length); + substituted[10] ^= 1; + refreshChecksum(substituted); + Files.write(manifest, substituted); + assertThrows(ArchivePersistenceException.class, + () -> PersistentServingKeyIndexGeneration.open(generationPath)); + } + } + @Test public void catalogPinsOldGenerationUntilLastReaderReleasesIt() throws Exception { Path root = temporaryFolder.newFolder("catalog").toPath(); @@ -466,6 +511,12 @@ private static byte[] hash(int suffix) { return hash; } + private static void refreshChecksum(byte[] encoded) { + int payloadLength = encoded.length - Integer.BYTES; + int checksum = Hashing.crc32c().hashBytes(encoded, 0, payloadLength).asInt(); + ByteBuffer.wrap(encoded, payloadLength, Integer.BYTES).putInt(checksum); + } + private static final class Fixture implements AutoCloseable { private final HistoryIndexStore index; private final List markers = new ArrayList<>(); From 53e00c291039ae58964942bdec0c59ace3f4f8b7 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 22:47:55 +0800 Subject: [PATCH 028/161] feat(chainbase): add P66 asset archive codec --- .../db2/archive/P66AccountAssetCodec.java | 233 ++++++++++++++++++ .../db2/archive/P66AccountAssetCodecTest.java | 170 +++++++++++++ 2 files changed, 403 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/P66AccountAssetCodecTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java new file mode 100644 index 00000000000..c424a1273e8 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java @@ -0,0 +1,233 @@ +package org.tron.core.db2.archive; + +import com.google.protobuf.InvalidProtocolBufferException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.BlockChangeView.PostValue; +import org.tron.protos.Protocol.Account; + +/** Versioned standalone codec for the P66-dependent Account and AccountAsset layout. */ +public final class P66AccountAssetCodec { + + public static final String FORMAT_ID = "archive-state/p66-account-asset/v1"; + private static final int ADDRESS_LENGTH = 21; + private static final int BALANCE_LENGTH = Long.BYTES; + + public enum Phase { + P66_OFF, + P66_ACTIVATION, + P66_ON; + + private boolean directAssetsEnabled() { + return this != P66_OFF; + } + } + + /** Validates and converts one raw execution Account into its target archive representation. */ + public byte[] canonicalizeAccount(Phase phase, byte[] physicalAccountKey, + byte[] rawAccountValue) { + Objects.requireNonNull(phase, "phase"); + byte[] accountKey = requireAddress(physicalAccountKey); + Objects.requireNonNull(rawAccountValue, "rawAccountValue"); + Account account = parseAccount(rawAccountValue); + requireAccountAddress(accountKey, account); + if (!phase.directAssetsEnabled()) { + if (account.getAssetOptimized()) { + throw new ArchivePersistenceException( + "P66-off Account must not use the optimized asset layout"); + } + return Arrays.copyOf(rawAccountValue, rawAccountValue.length); + } + return account.toBuilder() + .setAssetOptimized(true) + .clearAsset() + .clearAssetV2() + .build() + .toByteArray(); + } + + /** Encodes one direct-row post state; zero is represented only as ABSENT. */ + public AssetRow encodeAssetRow(Phase phase, byte[] accountAddress, String tokenId, + long balance) { + Objects.requireNonNull(phase, "phase"); + if (!phase.directAssetsEnabled()) { + throw new ArchivePersistenceException("P66-off state must not contain direct asset rows"); + } + byte[] address = requireAddress(accountAddress); + byte[] token = requireTokenId(tokenId); + byte[] key = ByteBuffer.allocate(address.length + token.length) + .put(address) + .put(token) + .array(); + PostValue value = balance == 0 ? PostValue.absent() + : PostValue.present(ByteBuffer.allocate(BALANCE_LENGTH).putLong(balance).array()); + return new AssetRow(key, value); + } + + /** Decodes a PRESENT direct row and rejects the non-canonical stored zero representation. */ + public DecodedAssetRow decodePresentAssetRow(byte[] physicalRawKey, byte[] rawValue) { + KeyIdentity identity = decodeKey(physicalRawKey); + if (rawValue == null || rawValue.length != BALANCE_LENGTH) { + throw new ArchivePersistenceException( + "AccountAsset value must be exactly eight bytes"); + } + long balance = ByteBuffer.wrap(rawValue).getLong(); + if (balance == 0) { + throw new ArchivePersistenceException( + "AccountAsset zero balance must be encoded as ABSENT"); + } + return new DecodedAssetRow(identity.address, identity.tokenId, balance); + } + + /** Validates one canonical account plus its sorted direct-row post mutations. */ + public void requireCanonicalLayout(Phase phase, byte[] physicalAccountKey, + byte[] canonicalAccountValue, List directRows) { + Objects.requireNonNull(phase, "phase"); + byte[] accountKey = requireAddress(physicalAccountKey); + Account account = parseAccount(canonicalAccountValue); + requireAccountAddress(accountKey, account); + List rows = new ArrayList<>(Objects.requireNonNull(directRows, "directRows")); + if (rows.contains(null)) { + throw new ArchivePersistenceException("Canonical AccountAsset rows contain null"); + } + if (!phase.directAssetsEnabled()) { + if (account.getAssetOptimized() || !rows.isEmpty()) { + throw new ArchivePersistenceException("P66-off durable layout is mixed"); + } + return; + } + if (!account.getAssetOptimized() || !account.getAssetMap().isEmpty() + || !account.getAssetV2Map().isEmpty()) { + throw new ArchivePersistenceException("P66-on durable Account layout is mixed"); + } + byte[] previous = null; + for (AssetRow row : rows) { + byte[] key = row.getPhysicalRawKey(); + KeyIdentity identity = decodeKey(key); + if (!Arrays.equals(accountKey, identity.address) + || previous != null && BlockReverseDiff.compareUnsigned(previous, key) >= 0) { + throw new ArchivePersistenceException( + "Canonical AccountAsset rows must be address-bound, unique, and sorted"); + } + if (row.getPostValue().isPresent()) { + decodePresentAssetRow(key, row.getPostValue().getValue()); + } + previous = key; + } + } + + private static KeyIdentity decodeKey(byte[] physicalRawKey) { + if (physicalRawKey == null || physicalRawKey.length <= ADDRESS_LENGTH) { + throw new ArchivePersistenceException("AccountAsset physical key is too short"); + } + byte[] address = Arrays.copyOf(physicalRawKey, ADDRESS_LENGTH); + byte[] token = Arrays.copyOfRange(physicalRawKey, ADDRESS_LENGTH, physicalRawKey.length); + requireCanonicalTokenBytes(token); + return new KeyIdentity(address, new String(token, StandardCharsets.US_ASCII)); + } + + private static byte[] requireAddress(byte[] address) { + if (address == null || address.length != ADDRESS_LENGTH) { + throw new ArchivePersistenceException("Account address must be exactly 21 bytes"); + } + return Arrays.copyOf(address, address.length); + } + + private static byte[] requireTokenId(String tokenId) { + if (tokenId == null) { + throw new ArchivePersistenceException("AccountAsset token ID is missing"); + } + byte[] encoded = tokenId.getBytes(StandardCharsets.US_ASCII); + if (!tokenId.equals(new String(encoded, StandardCharsets.US_ASCII))) { + throw new ArchivePersistenceException("AccountAsset token ID must be ASCII decimal"); + } + requireCanonicalTokenBytes(encoded); + return encoded; + } + + private static void requireCanonicalTokenBytes(byte[] token) { + if (token.length == 0 || token.length > 1 && token[0] == '0') { + throw new ArchivePersistenceException("AccountAsset token ID is not canonical decimal"); + } + for (byte value : token) { + if (value < '0' || value > '9') { + throw new ArchivePersistenceException("AccountAsset token ID is not canonical decimal"); + } + } + } + + private static Account parseAccount(byte[] value) { + if (value == null) { + throw new ArchivePersistenceException("Account value is missing"); + } + try { + return Account.parseFrom(value); + } catch (InvalidProtocolBufferException invalid) { + throw new ArchivePersistenceException("Account value is not valid protobuf", invalid); + } + } + + private static void requireAccountAddress(byte[] physicalKey, Account account) { + if (!Arrays.equals(physicalKey, account.getAddress().toByteArray())) { + throw new ArchivePersistenceException("Account protobuf address does not match physical key"); + } + } + + public static final class AssetRow { + private final byte[] physicalRawKey; + private final PostValue postValue; + + public AssetRow(byte[] physicalRawKey, PostValue postValue) { + this.physicalRawKey = Arrays.copyOf( + Objects.requireNonNull(physicalRawKey, "physicalRawKey"), physicalRawKey.length); + this.postValue = Objects.requireNonNull(postValue, "postValue"); + } + + public byte[] getPhysicalRawKey() { + return Arrays.copyOf(physicalRawKey, physicalRawKey.length); + } + + public PostValue getPostValue() { + return postValue; + } + } + + public static final class DecodedAssetRow { + private final byte[] accountAddress; + private final String tokenId; + private final long balance; + + private DecodedAssetRow(byte[] accountAddress, String tokenId, long balance) { + this.accountAddress = Arrays.copyOf(accountAddress, accountAddress.length); + this.tokenId = tokenId; + this.balance = balance; + } + + public byte[] getAccountAddress() { + return Arrays.copyOf(accountAddress, accountAddress.length); + } + + public String getTokenId() { + return tokenId; + } + + public long getBalance() { + return balance; + } + } + + private static final class KeyIdentity { + private final byte[] address; + private final String tokenId; + + private KeyIdentity(byte[] address, String tokenId) { + this.address = address; + this.tokenId = tokenId; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/P66AccountAssetCodecTest.java b/framework/src/test/java/org/tron/core/db2/archive/P66AccountAssetCodecTest.java new file mode 100644 index 00000000000..ec1a0a8e756 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/P66AccountAssetCodecTest.java @@ -0,0 +1,170 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; +import org.tron.common.utils.ByteArray; +import org.tron.core.db2.archive.BlockChangeView.PostValue; +import org.tron.core.db2.archive.P66AccountAssetCodec.AssetRow; +import org.tron.core.db2.archive.P66AccountAssetCodec.DecodedAssetRow; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; +import org.tron.protos.Protocol.Account; + +public class P66AccountAssetCodecTest { + + private final P66AccountAssetCodec codec = new P66AccountAssetCodec(); + + @Test + public void p66OffPreservesExactAccountBytesAndRejectsDirectRows() throws Exception { + byte[] address = address(1); + Account account = account(address).toBuilder() + .putAsset("legacy-name", 7L) + .putAssetV2("1000001", 11L) + .build(); + byte[] raw = account.toByteArray(); + + byte[] canonical = codec.canonicalizeAccount(Phase.P66_OFF, address, raw); + assertArrayEquals(raw, canonical); + assertFalse(Account.parseFrom(canonical).getAssetOptimized()); + codec.requireCanonicalLayout(Phase.P66_OFF, address, canonical, + Collections.emptyList()); + assertThrows(ArchivePersistenceException.class, + () -> codec.encodeAssetRow(Phase.P66_OFF, address, "1000001", 11L)); + + byte[] optimized = account.toBuilder().setAssetOptimized(true).build().toByteArray(); + assertThrows(ArchivePersistenceException.class, + () -> codec.canonicalizeAccount(Phase.P66_OFF, address, optimized)); + } + + @Test + public void activationAndOnCanonicalizeAccountAndRowsDeterministically() throws Exception { + byte[] address = address(2); + Account raw = account(address).toBuilder() + .setBalance(99L) + .putAsset("legacy-name", 5L) + .putAssetV2("1000001", 17L) + .build(); + + byte[] activation = codec.canonicalizeAccount(Phase.P66_ACTIVATION, address, + raw.toByteArray()); + byte[] on = codec.canonicalizeAccount(Phase.P66_ON, address, raw.toByteArray()); + assertArrayEquals(activation, on); + Account canonical = Account.parseFrom(activation); + assertTrue(canonical.getAssetOptimized()); + assertTrue(canonical.getAssetMap().isEmpty()); + assertTrue(canonical.getAssetV2Map().isEmpty()); + assertEquals(99L, canonical.getBalance()); + assertEquals("1a154100000000000000000000000000000000000000022063e00301", + ByteArray.toHexString(activation)); + + AssetRow present = codec.encodeAssetRow(Phase.P66_ACTIVATION, address, "1000001", 17L); + AssetRow absent = codec.encodeAssetRow(Phase.P66_ACTIVATION, address, "1000002", 0L); + assertTrue(present.getPostValue().isPresent()); + assertArrayEquals(ByteBuffer.allocate(Long.BYTES).putLong(17L).array(), + present.getPostValue().getValue()); + assertFalse(absent.getPostValue().isPresent()); + codec.requireCanonicalLayout(Phase.P66_ACTIVATION, address, activation, + Arrays.asList(present, absent)); + codec.requireCanonicalLayout(Phase.P66_ON, address, on, + Arrays.asList(present, absent)); + } + + @Test + public void decodesSignedBalanceAndDefensivelyOwnsBytes() { + byte[] address = address(3); + AssetRow row = codec.encodeAssetRow(Phase.P66_ON, address, "0", -9L); + byte[] key = row.getPhysicalRawKey(); + byte[] value = row.getPostValue().getValue(); + DecodedAssetRow decoded = codec.decodePresentAssetRow(key, value); + assertArrayEquals(address, decoded.getAccountAddress()); + assertEquals("0", decoded.getTokenId()); + assertEquals(-9L, decoded.getBalance()); + + key[0] ^= 1; + value[0] ^= 1; + assertArrayEquals(address, decoded.getAccountAddress()); + assertEquals(-9L, decoded.getBalance()); + assertArrayEquals(address, Arrays.copyOf(row.getPhysicalRawKey(), address.length)); + } + + @Test + public void rejectsMalformedAccountKeyTokenValueAndStoredZero() { + byte[] address = address(4); + byte[] raw = account(address).toByteArray(); + assertThrows(ArchivePersistenceException.class, + () -> codec.canonicalizeAccount(Phase.P66_OFF, new byte[20], raw)); + assertThrows(ArchivePersistenceException.class, + () -> codec.canonicalizeAccount(Phase.P66_OFF, address(5), raw)); + assertThrows(ArchivePersistenceException.class, + () -> codec.canonicalizeAccount(Phase.P66_OFF, address, new byte[]{-1, -1})); + assertThrows(ArchivePersistenceException.class, + () -> codec.encodeAssetRow(Phase.P66_ON, address, "01", 1L)); + assertThrows(ArchivePersistenceException.class, + () -> codec.encodeAssetRow(Phase.P66_ON, address, "1a", 1L)); + assertThrows(ArchivePersistenceException.class, + () -> codec.decodePresentAssetRow(address, new byte[Long.BYTES])); + byte[] key = concat(address, "1000001"); + assertThrows(ArchivePersistenceException.class, + () -> codec.decodePresentAssetRow(key, new byte[7])); + assertThrows(ArchivePersistenceException.class, + () -> codec.decodePresentAssetRow(key, new byte[Long.BYTES])); + } + + @Test + public void canonicalLayoutRejectsMixedUnsortedDuplicateAndForeignRows() { + byte[] address = address(6); + byte[] raw = account(address).toBuilder().putAssetV2("1000001", 1L).build().toByteArray(); + byte[] canonical = codec.canonicalizeAccount(Phase.P66_ON, address, raw); + AssetRow first = codec.encodeAssetRow(Phase.P66_ON, address, "1000001", 1L); + AssetRow second = codec.encodeAssetRow(Phase.P66_ON, address, "1000002", 2L); + + assertThrows(ArchivePersistenceException.class, + () -> codec.requireCanonicalLayout(Phase.P66_ON, address, raw, + Collections.emptyList())); + assertThrows(ArchivePersistenceException.class, + () -> codec.requireCanonicalLayout(Phase.P66_OFF, address, raw, + Collections.singletonList(first))); + assertThrows(ArchivePersistenceException.class, + () -> codec.requireCanonicalLayout(Phase.P66_ON, address, canonical, + Arrays.asList(second, first))); + assertThrows(ArchivePersistenceException.class, + () -> codec.requireCanonicalLayout(Phase.P66_ON, address, canonical, + Arrays.asList(first, first))); + AssetRow foreign = codec.encodeAssetRow(Phase.P66_ON, address(7), "1000003", 3L); + assertThrows(ArchivePersistenceException.class, + () -> codec.requireCanonicalLayout(Phase.P66_ON, address, canonical, + Collections.singletonList(foreign))); + AssetRow malformedAbsent = new AssetRow(concat(address, "01"), PostValue.absent()); + assertThrows(ArchivePersistenceException.class, + () -> codec.requireCanonicalLayout(Phase.P66_ON, address, canonical, + Collections.singletonList(malformedAbsent))); + } + + private static Account account(byte[] address) { + return Account.newBuilder().setAddress(ByteString.copyFrom(address)).build(); + } + + private static byte[] address(int suffix) { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = (byte) suffix; + return address; + } + + private static byte[] concat(byte[] address, String token) { + byte[] tokenBytes = token.getBytes(StandardCharsets.US_ASCII); + return ByteBuffer.allocate(address.length + tokenBytes.length) + .put(address) + .put(tokenBytes) + .array(); + } +} From 5f1f74679d23e105fd4a361ef819f61b33145630 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 23:10:47 +0800 Subject: [PATCH 029/161] refactor(chainbase): unify P66 asset projection codec --- .../archive/AccountAssetArchiveProjector.java | 67 +++++++++++-------- ...AccountAssetBlockProjectionBridgeTest.java | 52 ++++++++++---- .../SnapshotOldValueCollectorTest.java | 24 +++++-- 3 files changed, 94 insertions(+), 49 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java index 943f9a6d4a2..ba2423b5e86 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java @@ -1,9 +1,6 @@ package org.tron.core.db2.archive; -import com.google.common.primitives.Bytes; -import com.google.common.primitives.Longs; import com.google.protobuf.InvalidProtocolBufferException; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -13,6 +10,9 @@ import java.util.Set; import java.util.TreeSet; import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; +import org.tron.core.db2.archive.P66AccountAssetCodec.AssetRow; +import org.tron.core.db2.archive.P66AccountAssetCodec.DecodedAssetRow; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.common.WrappedByteArray; import org.tron.protos.Protocol.Account; @@ -24,6 +24,7 @@ public final class AccountAssetArchiveProjector { public static final String ACCOUNT_DB = "account"; public static final String ACCOUNT_ASSET_DB = "account-asset"; + private final P66AccountAssetCodec codec = new P66AccountAssetCodec(); Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue rawPost, boolean targetAssetOptimizationEnabled, @@ -41,6 +42,11 @@ Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue r Map physicalSnapshot = copyPhysicalAssets(accountKey, oldPhysicalAssetsForAddress == null ? Collections.emptyMap() : oldPhysicalAssetsForAddress); + if (!physicalSnapshot.isEmpty() + && (oldAccount == null || !oldAccount.getAssetOptimized())) { + throw new ArchivePersistenceException( + "Unoptimized Account must not have old physical AccountAsset rows"); + } Map oldAssets = physicalAssets(accountKey, oldAccount, oldAccount != null && oldAccount.getAssetOptimized(), physicalSnapshot); @@ -66,10 +72,19 @@ Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue r } OldValue canonicalOld = oldAccount == null ? OldValue.absent() - : OldValue.present(canonicalAccount(oldAccount, oldAccount.getAssetOptimized())); + : OldValue.present(canonicalAccount(accountKey, oldAccount, + oldAccount.getAssetOptimized())); BlockChangeView.PostValue canonicalPost = postAccount == null ? BlockChangeView.PostValue.absent() - : BlockChangeView.PostValue.present(canonicalAccount(postAccount, projectPost)); + : BlockChangeView.PostValue.present(canonicalAccount(accountKey, postAccount, projectPost)); + if (canonicalPost.isPresent()) { + List rows = new ArrayList<>(forwardAssets.size()); + for (AssetMutation mutation : forwardAssets) { + rows.add(new AssetRow(mutation.getPhysicalRawKey(), mutation.getPostValue())); + } + codec.requireCanonicalLayout(phase(postAccount, projectPost), + accountKey, canonicalPost.getValue(), rows); + } return new Projection(canonicalOld, canonicalPost, reverseAssets, forwardAssets); } @@ -91,8 +106,8 @@ private Map copyPhysicalAssets(byte[] accountKey, throw new ArchivePersistenceException("Old physical AccountAsset input contains null"); } byte[] physicalKey = key.getBytes(); - if (physicalKey.length <= accountKey.length - || !startsWith(physicalKey, accountKey)) { + DecodedAssetRow decoded = codec.decodePresentAssetRow(physicalKey, value); + if (!Arrays.equals(decoded.getAccountAddress(), accountKey)) { throw new ArchivePersistenceException( "Old physical AccountAsset input does not belong to changed Account"); } @@ -101,15 +116,6 @@ private Map copyPhysicalAssets(byte[] accountKey, return copy; } - private boolean startsWith(byte[] value, byte[] prefix) { - for (int i = 0; i < prefix.length; i++) { - if (value[i] != prefix[i]) { - return false; - } - } - return true; - } - private Map physicalAssets(byte[] accountKey, Account account, boolean projected, Map physicalSnapshot) { Map result = new HashMap<>(); @@ -121,27 +127,29 @@ private Map physicalAssets(byte[] accountKey, Account WrappedByteArray.copyOf(key.getBytes()), Arrays.copyOf(value, value.length))); } account.getAssetV2Map().forEach((token, balance) -> { - WrappedByteArray key = WrappedByteArray.copyOf(Bytes.concat(accountKey, - token.getBytes(StandardCharsets.UTF_8))); - if (balance == 0) { + AssetRow encoded = codec.encodeAssetRow(account.getAssetOptimized() + ? Phase.P66_ON : Phase.P66_ACTIVATION, + accountKey, token, balance); + WrappedByteArray key = WrappedByteArray.copyOf(encoded.getPhysicalRawKey()); + if (!encoded.getPostValue().isPresent()) { result.remove(key); } else { - result.put(key, Longs.toByteArray(balance)); + result.put(key, encoded.getPostValue().getValue()); } }); return result; } - private byte[] canonicalAccount(Account account, boolean projected) { + private byte[] canonicalAccount(byte[] accountKey, Account account, boolean projected) { + return codec.canonicalizeAccount(phase(account, projected), accountKey, + account.toByteArray()); + } + + private Phase phase(Account account, boolean projected) { if (!projected) { - return account.toByteArray(); + return Phase.P66_OFF; } - return account.toBuilder() - .setAssetOptimized(true) - .clearAsset() - .clearAssetV2() - .build() - .toByteArray(); + return account.getAssetOptimized() ? Phase.P66_ON : Phase.P66_ACTIVATION; } private Account parse(byte[] value) { @@ -151,7 +159,8 @@ private Account parse(byte[] value) { try { return Account.parseFrom(value); } catch (InvalidProtocolBufferException e) { - throw new IllegalStateException("Invalid account value while projecting archive state", e); + throw new ArchivePersistenceException( + "Invalid account value while projecting archive state", e); } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java b/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java index 708ebc6077e..54e7539aa63 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java @@ -46,8 +46,8 @@ public class AccountAssetBlockProjectionBridgeTest extends BaseMethodTest { public void sharesExactAccountProjectionAcrossDeterministicReverseAndForwardBuilders() { BlockSnapshotMeta meta = meta(1); HistoryCommitMarker marker = marker(meta); - byte[] updateKey = bytes(2, 1); - byte[] deleteKey = bytes(2, 2); + byte[] updateKey = accountKey(1); + byte[] deleteKey = accountKey(2); byte[] updateAsset = assetKey(updateKey, "1000001"); byte[] deleteAsset = assetKey(deleteKey, "1000002"); Account oldUpdate = optimizedAccount(updateKey); @@ -103,7 +103,7 @@ public void sharesExactAccountProjectionAcrossDeterministicReverseAndForwardBuil public void rejectsActivationIdentityAndCoverageBeforeAnyPhysicalRead() { BlockSnapshotMeta meta = meta(2); HistoryCommitMarker marker = marker(meta); - byte[] accountKey = bytes(2, 3); + byte[] accountKey = accountKey(3); Account old = optimizedAccount(accountKey); AccountAssetStore assetStore = mock(AccountAssetStore.class); AccountAssetBlockProjectionBridge bridge = bridge(assetStore); @@ -142,8 +142,8 @@ public void rejectsActivationIdentityAndCoverageBeforeAnyPhysicalRead() { public void projectionFailurePublishesNoPartialResultAndAllowsFreshRetry() { BlockSnapshotMeta meta = meta(4); HistoryCommitMarker marker = marker(meta); - byte[] validKey = bytes(2, 4); - byte[] invalidKey = bytes(2, 5); + byte[] validKey = accountKey(4); + byte[] invalidKey = accountKey(5); Account validOld = optimizedAccount(validKey); Account validPost = validOld.toBuilder().putAssetV2("1000003", 30L).build(); AccountAssetStore assetStore = mock(AccountAssetStore.class); @@ -156,7 +156,7 @@ public void projectionFailurePublishesNoPartialResultAndAllowsFreshRetry() { databases.get("account").put(validKey, validPost.toByteArray()); databases.get("account").put(invalidKey, bytes(3, 99)); }); - assertThrows(IllegalStateException.class, + assertThrows(ArchivePersistenceException.class, () -> bridge.prepare(failing, TargetAssetOptimization.forTarget(meta, true))); @@ -174,13 +174,25 @@ public void projectionFailurePublishesNoPartialResultAndAllowsFreshRetry() { @Test public void physicalInputFailurePublishesNothingAndFreshPrepareCanRetry() { BlockSnapshotMeta meta = meta(24); - byte[] accountKey = bytes(2, 24); + byte[] accountKey = accountKey(24); Account old = optimizedAccount(accountKey); - boolean[] fail = {true}; + int[] failureMode = {1}; AccountAssetOldPhysicalAssetsSource source = key -> { - if (fail[0]) { + if (failureMode[0] == 1) { throw new IllegalStateException("injected physical input failure"); } + if (failureMode[0] == 2) { + return Collections.singletonMap( + WrappedByteArray.copyOf(assetKey(key, "01000001")), Longs.toByteArray(1L)); + } + if (failureMode[0] == 3) { + return Collections.singletonMap( + WrappedByteArray.copyOf(assetKey(key, "1000001")), new byte[7]); + } + if (failureMode[0] == 4) { + return Collections.singletonMap( + WrappedByteArray.copyOf(assetKey(key, "1000001")), Longs.toByteArray(0L)); + } return Collections.emptyMap(); }; AccountAssetBlockProjectionBridge bridge = new AccountAssetBlockProjectionBridge( @@ -196,7 +208,12 @@ public void physicalInputFailurePublishesNothingAndFreshPrepareCanRetry() { () -> bridge.prepare(view, activation)); assertTrue(failure.getMessage().contains("old physical AccountAsset input")); - fail[0] = false; + for (int mode = 2; mode <= 4; mode++) { + failureMode[0] = mode; + assertThrows(ArchivePersistenceException.class, + () -> bridge.prepare(view, activation)); + } + failureMode[0] = 0; PreparedBlockProjection prepared = bridge.prepare(view, activation); assertEquals(meta, prepared.getReverseDiff().getMeta()); prepared.abort(); @@ -207,7 +224,7 @@ public void physicalInputFailurePublishesNothingAndFreshPrepareCanRetry() { public void resolvesActivationBlockFromProposalSixtySixAndFeedsSharedBridge() { BlockSnapshotMeta meta = meta(5); HistoryCommitMarker marker = marker(meta); - byte[] accountKey = bytes(2, 6); + byte[] accountKey = accountKey(6); byte[] physicalAsset = assetKey(accountKey, "1000005"); Account old = optimizedAccount(accountKey); Account post = old.toBuilder().putAssetV2("1000005", 50L).build(); @@ -242,7 +259,7 @@ public void resolvesActivationBlockFromProposalSixtySixAndFeedsSharedBridge() { public void inheritsUnchangedProposalSixtySixWithoutUsingProposalFiftyThree() { BlockSnapshotMeta meta = meta(6); HistoryCommitMarker marker = marker(meta); - byte[] accountKey = bytes(2, 7); + byte[] accountKey = accountKey(7); Account raw = Account.newBuilder() .setAddress(ByteString.copyFrom(accountKey)) .putAssetV2("1000006", 60L) @@ -272,7 +289,7 @@ public void inheritsUnchangedProposalSixtySixWithoutUsingProposalFiftyThree() { public void rejectsMissingCorruptSubstitutedAndReorgActivationBeforePrefix() { BlockSnapshotMeta meta = meta(7); HistoryCommitMarker marker = marker(meta); - byte[] accountKey = bytes(2, 8); + byte[] accountKey = accountKey(8); Account old = optimizedAccount(accountKey); AccountAssetStore assetStore = mock(AccountAssetStore.class); AccountAssetBlockProjectionBridge bridge = bridge(assetStore); @@ -328,7 +345,7 @@ public void rejectsMissingCorruptSubstitutedAndReorgActivationBeforePrefix() { public void rejectsWrongMarkerWithoutConsumingPreparedProjectionAndSealsExactlyOnce() { BlockSnapshotMeta meta = meta(9); HistoryCommitMarker marker = marker(meta); - byte[] accountKey = bytes(2, 9); + byte[] accountKey = accountKey(9); Account account = optimizedAccount(accountKey); AccountAssetStore assetStore = mock(AccountAssetStore.class); when(assetStore.prefixQuery(any(byte[].class))).thenReturn(Collections.emptyMap()); @@ -637,6 +654,13 @@ private static Account optimizedAccount(byte[] accountKey) { .build(); } + private static byte[] accountKey(int suffix) { + byte[] key = new byte[21]; + key[0] = 0x41; + key[20] = (byte) suffix; + return key; + } + private static byte[] assetKey(byte[] accountKey, String token) { byte[] tokenBytes = token.getBytes(java.nio.charset.StandardCharsets.UTF_8); byte[] key = Arrays.copyOf(accountKey, accountKey.length + tokenBytes.length); diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index d8eae24567c..c70eeab9b64 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -291,7 +291,7 @@ public void matchesReferenceStateForRandomBlockOperations() { @Test public void projectsAccountAssetTransitionBeforeRootMerge() { - byte[] address = bytes("account-address"); + byte[] address = archiveAddress(1); byte[] token = bytes("1000001"); Account oldAccount = Account.newBuilder() .setAddress(ByteString.copyFrom(address)) @@ -340,7 +340,7 @@ public void projectsAccountAssetTransitionBeforeRootMerge() { @Test public void projectsOldPhysicalAssetValueForOptimizedAccount() { - byte[] address = bytes("optimized-address"); + byte[] address = archiveAddress(2); byte[] token = bytes("1000002"); byte[] assetKey = Bytes.concat(address, token); Account oldAccount = Account.newBuilder() @@ -382,7 +382,7 @@ public void projectsOldPhysicalAssetValueForOptimizedAccount() { @Test public void sharedAccountAssetProjectionUsesOneSnapshotAndStableForwardOrder() { - byte[] address = bytes("shared-projection-address"); + byte[] address = archiveAddress(3); byte[] firstKey = Bytes.concat(address, bytes("1000001")); byte[] secondKey = Bytes.concat(address, bytes("1000002")); Account oldAccount = Account.newBuilder() @@ -424,7 +424,7 @@ public void sharedAccountAssetProjectionUsesOneSnapshotAndStableForwardOrder() { @Test public void pureProjectionRequiresAndCopiesExplicitOldPhysicalAssets() { - byte[] address = bytes("pure-input-address"); + byte[] address = archiveAddress(4); byte[] assetKey = Bytes.concat(address, bytes("1000009")); Account optimized = Account.newBuilder() .setAddress(ByteString.copyFrom(address)) @@ -458,7 +458,7 @@ public void pureProjectionRequiresAndCopiesExplicitOldPhysicalAssets() { @Test public void targetAssetOptimizationOverridesLegacySupplierAndCoversDelete() { - byte[] address = bytes("target-activation-address"); + byte[] address = archiveAddress(5); byte[] assetKey = Bytes.concat(address, bytes("1000003")); Account rawPost = Account.newBuilder() .setAddress(ByteString.copyFrom(address)) @@ -480,6 +480,11 @@ public void targetAssetOptimizationOverridesLegacySupplierAndCoversDelete() { Collections.emptyMap()); assertArrayEquals(rawPost.toByteArray(), disabled.postAccount.getValue()); assertTrue(disabled.forwardAssets.isEmpty()); + Map mixedPhysical = new HashMap<>(); + mixedPhysical.put(WrappedByteArray.copyOf(assetKey), Longs.toByteArray(300L)); + assertThrows(ArchivePersistenceException.class, + () -> projector.project(address, null, + BlockChangeView.PostValue.present(rawPost.toByteArray()), true, mixedPhysical)); Account optimizedOld = rawPost.toBuilder() .setAssetOptimized(true) @@ -497,7 +502,7 @@ public void targetAssetOptimizationOverridesLegacySupplierAndCoversDelete() { @Test public void sharedProjectionUsesOuterFinalViewAfterNestedMergeAndRevoke() { - byte[] address = bytes("nested-account-address"); + byte[] address = archiveAddress(6); byte[] assetKey = Bytes.concat(address, bytes("1000004")); Account oldAccount = Account.newBuilder() .setAddress(ByteString.copyFrom(address)) @@ -1198,6 +1203,13 @@ private static byte[] bytes(String value) { return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); } + private static byte[] archiveAddress(int suffix) { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = (byte) suffix; + return address; + } + private static byte[] hash(int suffix) { byte[] hash = new byte[32]; hash[31] = (byte) suffix; From 99e7805bb58bf8e8fd5153cf181cc13a16e3ce62 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 20 Aug 2026 23:31:50 +0800 Subject: [PATCH 030/161] feat(chainbase): bind p66 phase to mutation plans --- .../AccountAssetBlockProjectionBridge.java | 30 +++++-- .../AccountAssetForwardMutationManifest.java | 16 +++- .../AccountAssetForwardMutationRecorder.java | 7 +- .../AccountAssetTargetActivationResolver.java | 13 ++- .../ArchiveBlockForwardMutationCapture.java | 5 +- .../ArchiveParticipantMutationBatch.java | 25 +++++- ...hiveParticipantMutationBatchCollector.java | 24 +++++- .../ArchiveTargetApplyCoordinator.java | 6 +- .../archive/ArchiveTargetMutationPlan.java | 21 ++++- .../ArchiveTargetMutationPlanBuilder.java | 6 +- .../ArchiveTargetMutationPlanCodec.java | 40 ++++++++- ...AccountAssetBlockProjectionBridgeTest.java | 20 ++++- ...chiveBlockForwardMutationRecoveryTest.java | 12 +-- ...ParticipantMutationBatchCollectorTest.java | 86 ++++++++++--------- ...ArchiveParticipantRecoveryStorageTest.java | 7 +- .../ArchiveTargetApplyCoordinatorTest.java | 5 +- .../ArchiveTargetMutationPlanBuilderTest.java | 41 +++++++-- .../ArchiveTargetMutationPlanFileTest.java | 48 +++++++++-- .../SnapshotOldValueCollectorTest.java | 3 +- 19 files changed, 321 insertions(+), 94 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java index 6c1e8f0af67..d10c914c945 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java @@ -10,6 +10,7 @@ import org.tron.core.db2.archive.BlockChangeView.Change; import org.tron.core.db2.archive.BlockChangeView.DatabaseChanges; import org.tron.core.db2.archive.BlockChangeView.PostValue; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.common.WrappedByteArray; /** @@ -75,7 +76,8 @@ public PreparedBlockProjection prepare(BlockChangeView view, } BlockReverseDiff reverse = new BlockReverseDiff(input.getMeta(), groups); - return new PreparedBlockProjection(input, participants, reverse, forwardEntries); + return new PreparedBlockProjection(input, participants, reverse, forwardEntries, + targetActivation.getPhase()); } private void validateBeforeProjection(BlockChangeView view, @@ -107,15 +109,19 @@ private static boolean sameLogicalValue(OldValue oldValue, PostValue postValue) /** Target-bound proposal-66 state; mismatched identity is rejected before any Store read. */ public static final class TargetAssetOptimization { private final BlockSnapshotMeta meta; - private final boolean enabled; + private final Phase phase; - private TargetAssetOptimization(BlockSnapshotMeta meta, boolean enabled) { + private TargetAssetOptimization(BlockSnapshotMeta meta, Phase phase) { this.meta = Objects.requireNonNull(meta, "meta"); - this.enabled = enabled; + this.phase = Objects.requireNonNull(phase, "phase"); } public static TargetAssetOptimization forTarget(BlockSnapshotMeta meta, boolean enabled) { - return new TargetAssetOptimization(meta, enabled); + return forTarget(meta, enabled ? Phase.P66_ON : Phase.P66_OFF); + } + + static TargetAssetOptimization forTarget(BlockSnapshotMeta meta, Phase phase) { + return new TargetAssetOptimization(meta, phase); } BlockSnapshotMeta getMeta() { @@ -123,7 +129,11 @@ BlockSnapshotMeta getMeta() { } boolean isEnabled() { - return enabled; + return phase != Phase.P66_OFF; + } + + Phase getPhase() { + return phase; } } @@ -134,15 +144,17 @@ public static final class PreparedBlockProjection { private BlockChangeView view; private BlockReverseDiff reverseDiff; private List forwardEntries; + private final Phase targetPhase; private State state = State.PREPARED; private PreparedBlockProjection(BlockChangeView view, List participants, - BlockReverseDiff reverseDiff, List forwardEntries) { + BlockReverseDiff reverseDiff, List forwardEntries, Phase targetPhase) { this.view = Objects.requireNonNull(view, "view"); this.meta = view.getMeta(); this.participants = Collections.unmodifiableList(new ArrayList<>(participants)); this.reverseDiff = Objects.requireNonNull(reverseDiff, "reverseDiff"); this.forwardEntries = Collections.unmodifiableList(new ArrayList<>(forwardEntries)); + this.targetPhase = Objects.requireNonNull(targetPhase, "targetPhase"); } public synchronized BlockReverseDiff getReverseDiff() { @@ -174,13 +186,13 @@ public synchronized ArchiveBlockForwardPayload sealPayload(HistoryCommitMarker m synchronized AccountAssetForwardMutationManifest previewSeal(HistoryCommitMarker marker) { HistoryCommitMarker target = validateMarker(marker); - return new AccountAssetForwardMutationManifest(target, forwardEntries); + return new AccountAssetForwardMutationManifest(target, targetPhase, forwardEntries); } synchronized ArchiveBlockForwardPayload previewSealPayload(HistoryCommitMarker marker) { HistoryCommitMarker target = validateMarker(marker); return new ArchiveBlockForwardPayload(target, view, - new AccountAssetForwardMutationManifest(target, forwardEntries)); + new AccountAssetForwardMutationManifest(target, targetPhase, forwardEntries)); } synchronized HistoryCommitMarker validateMarker(HistoryCommitMarker marker) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java index a08944a3205..8ec8ff10dc2 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java @@ -10,22 +10,28 @@ import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; import org.tron.core.db2.archive.AccountAssetForwardProjector.Projection; import org.tron.core.db2.archive.BlockChangeView.PostValue; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; /** Immutable one-shot account projection input bound to one exact committed target. */ public final class AccountAssetForwardMutationManifest implements AccountAssetForwardProjector { private final byte[] encodedTarget; + private final String formatId; + private final Phase targetPhase; private final TreeMap entries = new TreeMap<>(); private final TreeSet consumed = new TreeSet<>(); private boolean begun; private boolean completed; - public AccountAssetForwardMutationManifest(HistoryCommitMarker target, List entries) { + public AccountAssetForwardMutationManifest(HistoryCommitMarker target, Phase targetPhase, + List entries) { HistoryCommitMarker expectedTarget = Objects.requireNonNull(target, "target"); if (!expectedTarget.getDatabases().equals(sortedParticipants())) { throw new IllegalArgumentException("Manifest target must cover exact VERSIONED_STATE set"); } encodedTarget = new HistoryCommitMarkerCodec().encode(expectedTarget); + formatId = P66AccountAssetCodec.FORMAT_ID; + this.targetPhase = Objects.requireNonNull(targetPhase, "targetPhase"); for (Entry entry : Objects.requireNonNull(entries, "entries")) { if (entry == null) { throw new IllegalArgumentException("Manifest contains null entry"); @@ -37,6 +43,14 @@ public AccountAssetForwardMutationManifest(HistoryCommitMarker target, List changedAccountPhysicalKeys) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java index 43f3ac70bbf..8df6dfeea36 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java @@ -8,12 +8,14 @@ import org.tron.core.db2.archive.AccountAssetForwardMutationManifest.Entry; import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; import org.tron.core.db2.archive.BlockChangeView.PostValue; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; /** Collects explicit execution-time AccountAsset events for one target without Store reads. */ public final class AccountAssetForwardMutationRecorder { private final BlockSnapshotMeta targetMeta; private final ArchiveBlockForwardMutationLimits limits; + private final Phase targetPhase; private final TreeMap accounts = new TreeMap<>(); private int accountCount; private int assetMutationCount; @@ -21,8 +23,9 @@ public final class AccountAssetForwardMutationRecorder { private boolean sealed; public AccountAssetForwardMutationRecorder(BlockSnapshotMeta targetMeta, - ArchiveBlockForwardMutationLimits limits) { + Phase targetPhase, ArchiveBlockForwardMutationLimits limits) { this.targetMeta = Objects.requireNonNull(targetMeta, "targetMeta"); + this.targetPhase = Objects.requireNonNull(targetPhase, "targetPhase"); this.limits = Objects.requireNonNull(limits, "limits"); } @@ -83,7 +86,7 @@ public synchronized AccountAssetForwardMutationManifest seal(HistoryCommitMarker account.canonicalAccountPostValue, new ArrayList<>(account.assets.values()))); } AccountAssetForwardMutationManifest manifest = - new AccountAssetForwardMutationManifest(committedTarget, entries); + new AccountAssetForwardMutationManifest(committedTarget, targetPhase, entries); sealed = true; clearPayload(); return manifest; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java index a06263284cf..a310e00603c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java @@ -8,6 +8,7 @@ import org.tron.core.db2.archive.BlockChangeView.Change; import org.tron.core.db2.archive.BlockChangeView.DatabaseChanges; import org.tron.core.db2.archive.BlockChangeView.PostValue; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; /** Resolves proposal-66 activation from the target block's exact properties post view. */ public final class AccountAssetTargetActivationResolver { @@ -39,6 +40,7 @@ public TargetAssetOptimization resolve(BlockSnapshotMeta target, BlockChangeView throw new ArchivePersistenceException("Missing properties block view"); } + byte[] previous = properties.getPrevious(PROPOSAL_66_PHYSICAL_KEY); byte[] value = null; boolean changed = false; for (Change change : properties.getChanges()) { @@ -55,9 +57,16 @@ public TargetAssetOptimization resolve(BlockSnapshotMeta target, BlockChangeView } } if (!changed) { - value = properties.getPrevious(PROPOSAL_66_PHYSICAL_KEY); + value = previous; } - return TargetAssetOptimization.forTarget(expectedTarget, decode(value)); + boolean previousEnabled = decode(previous); + boolean targetEnabled = decode(value); + if (previousEnabled && !targetEnabled) { + throw new ArchivePersistenceException("Proposal-66 property must not regress"); + } + Phase phase = !targetEnabled ? Phase.P66_OFF + : previousEnabled ? Phase.P66_ON : Phase.P66_ACTIVATION; + return TargetAssetOptimization.forTarget(expectedTarget, phase); } static byte[] proposal66PhysicalKey() { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java index 7d38550bb6b..9aef9739a12 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java @@ -2,6 +2,7 @@ import java.util.Objects; import org.tron.core.db2.archive.BlockChangeView.PostValue; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; /** One-shot owner of a block's explicit AccountAsset events, post-state view, and output batch. */ public final class ArchiveBlockForwardMutationCapture { @@ -12,9 +13,9 @@ public final class ArchiveBlockForwardMutationCapture { private State state = State.OPEN; public ArchiveBlockForwardMutationCapture(BlockSnapshotMeta targetMeta, - ArchiveBlockForwardMutationLimits limits) { + Phase targetPhase, ArchiveBlockForwardMutationLimits limits) { this.targetMeta = Objects.requireNonNull(targetMeta, "targetMeta"); - accountAssetRecorder = new AccountAssetForwardMutationRecorder(targetMeta, + accountAssetRecorder = new AccountAssetForwardMutationRecorder(targetMeta, targetPhase, Objects.requireNonNull(limits, "limits")); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java index d875d3af941..57d736eb875 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; /** Immutable producer payload for one committed target's exact physical participant mutations. */ public final class ArchiveParticipantMutationBatch { @@ -13,15 +14,29 @@ public final class ArchiveParticipantMutationBatch { private final byte[] blockHash; private final byte[] batchId; private final byte[] historyPayloadDigest; + private final String accountAssetFormatId; + private final Phase targetPhase; private final List participants; private final List mutations; - public ArchiveParticipantMutationBatch(HistoryCommitMarker target, List mutations) { + public ArchiveParticipantMutationBatch(HistoryCommitMarker target, Phase targetPhase, + List mutations) { + this(target, P66AccountAssetCodec.FORMAT_ID, targetPhase, mutations); + } + + ArchiveParticipantMutationBatch(HistoryCommitMarker target, String accountAssetFormatId, + Phase targetPhase, List mutations) { HistoryCommitMarker checkedTarget = Objects.requireNonNull(target, "target"); targetEpoch = checkedTarget.getMeta().getEpoch(); blockHash = checkedTarget.getMeta().getBlockHash(); batchId = checkedTarget.getBatchId(); historyPayloadDigest = checkedTarget.getHistoryLocation().getBodyDigest(); + this.accountAssetFormatId = Objects.requireNonNull(accountAssetFormatId, + "accountAssetFormatId"); + if (accountAssetFormatId.isEmpty()) { + throw new IllegalArgumentException("AccountAsset transition format must not be empty"); + } + this.targetPhase = Objects.requireNonNull(targetPhase, "targetPhase"); participants = Collections.unmodifiableList( new ArrayList<>(checkedTarget.getDatabases())); List copy = new ArrayList<>(Objects.requireNonNull(mutations, "mutations")); @@ -47,6 +62,14 @@ byte[] getHistoryPayloadDigest() { return Arrays.copyOf(historyPayloadDigest, historyPayloadDigest.length); } + String getAccountAssetFormatId() { + return accountAssetFormatId; + } + + Phase getTargetPhase() { + return targetPhase; + } + List getParticipants() { return participants; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java index 4472896621e..595e5df6eae 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java @@ -10,19 +10,36 @@ import org.tron.core.db2.archive.BlockChangeView.Change; import org.tron.core.db2.archive.BlockChangeView.DatabaseChanges; import org.tron.core.db2.archive.BlockChangeView.PostValue; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; /** Converts one immutable block post-state view into a target-bound physical mutation batch. */ public final class ArchiveParticipantMutationBatchCollector { private final AccountAssetForwardProjector accountAssetProjector; + private final String accountAssetFormatId; + private final Phase targetPhase; private final List participants; - public ArchiveParticipantMutationBatchCollector() { - this(null); + public ArchiveParticipantMutationBatchCollector(Phase targetPhase) { + this(P66AccountAssetCodec.FORMAT_ID, targetPhase, null); } public ArchiveParticipantMutationBatchCollector( + AccountAssetForwardMutationManifest manifest) { + this(Objects.requireNonNull(manifest, "manifest").getFormatId(), manifest.getTargetPhase(), + manifest); + } + + public ArchiveParticipantMutationBatchCollector(Phase targetPhase, AccountAssetForwardProjector accountAssetProjector) { + this(P66AccountAssetCodec.FORMAT_ID, targetPhase, accountAssetProjector); + } + + private ArchiveParticipantMutationBatchCollector(String accountAssetFormatId, + Phase targetPhase, AccountAssetForwardProjector accountAssetProjector) { + this.accountAssetFormatId = Objects.requireNonNull(accountAssetFormatId, + "accountAssetFormatId"); + this.targetPhase = Objects.requireNonNull(targetPhase, "targetPhase"); this.accountAssetProjector = accountAssetProjector; participants = ArchiveParticipantDescriptor.current().getParticipants(); } @@ -57,7 +74,8 @@ public ArchiveParticipantMutationBatch collect(HistoryCommitMarker committedTarg if (accountAssetProjector != null) { accountAssetProjector.complete(); } - return new ArchiveParticipantMutationBatch(target, mutations); + return new ArchiveParticipantMutationBatch(target, accountAssetFormatId, targetPhase, + mutations); } private void collectAccount(Change change, List mutations) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java index 1c189665164..f0e51b324bd 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java @@ -10,6 +10,7 @@ import java.util.Objects; import java.util.TreeMap; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; /** Advances one standalone normal target through C, mixed D, latest refresh, and R. */ @@ -68,13 +69,14 @@ public ArchiveTargetApplyCoordinator(HistoryCommitStore history, Path checkpoint this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); } - public void apply(long targetEpoch, + public void apply(long targetEpoch, Phase targetPhase, Map> mutationPlans, ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { HistoryCommitMarker target = validateTarget(targetEpoch); Map> plans = validatePlans(mutationPlans); ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlan( - progress(Kind.APPLY_CHECKPOINT, null, target, null), plans); + progress(Kind.APPLY_CHECKPOINT, null, target, null), + P66AccountAssetCodec.FORMAT_ID, Objects.requireNonNull(targetPhase, "targetPhase"), plans); apply(target, plan, refresh); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlan.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlan.java index 0ee4436263b..a67ae76dfa7 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlan.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlan.java @@ -7,17 +7,26 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; /** Immutable target H identity plus exact per-participant business mutations. */ final class ArchiveTargetMutationPlan { private final ArchiveProgressEnvelope target; + private final String accountAssetFormatId; + private final Phase targetPhase; private final Map> mutations; - ArchiveTargetMutationPlan(ArchiveProgressEnvelope target, - Map> mutations) { + ArchiveTargetMutationPlan(ArchiveProgressEnvelope target, String accountAssetFormatId, + Phase targetPhase, Map> mutations) { this.target = Objects.requireNonNull(target, "target"); + this.accountAssetFormatId = Objects.requireNonNull(accountAssetFormatId, + "accountAssetFormatId"); + if (accountAssetFormatId.isEmpty()) { + throw new IllegalArgumentException("AccountAsset transition format must not be empty"); + } + this.targetPhase = Objects.requireNonNull(targetPhase, "targetPhase"); if (target.getKind() != Kind.APPLY_CHECKPOINT || target.getParticipant() != null) { throw new IllegalArgumentException("Mutation plan target must be a global checkpoint"); } @@ -56,6 +65,14 @@ ArchiveProgressEnvelope getTarget() { return target; } + String getAccountAssetFormatId() { + return accountAssetFormatId; + } + + Phase getTargetPhase() { + return targetPhase; + } + List getMutations(String participant) { List values = mutations.get(participant); if (values == null) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java index fcf7d85a268..701877dc3cd 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java @@ -29,6 +29,9 @@ ArchiveTargetMutationPlan build(HistoryCommitMarker committedTarget, throw new ArchivePersistenceException( "Participant mutation batch does not contain the exact VERSIONED_STATE set"); } + if (!P66AccountAssetCodec.FORMAT_ID.equals(input.getAccountAssetFormatId())) { + throw new ArchivePersistenceException("Unsupported AccountAsset transition format"); + } Map> grouped = new LinkedHashMap<>(); for (String participant : participants) { grouped.put(participant, new ArrayList<>()); @@ -49,7 +52,8 @@ ArchiveTargetMutationPlan build(HistoryCommitMarker committedTarget, Kind.APPLY_CHECKPOINT, null, target.getMeta().getEpoch(), target.getMeta().getBlockHash(), target.getBatchId(), target.getHistoryLocation().getBodyDigest(), participants); - return new ArchiveTargetMutationPlan(targetEnvelope, grouped); + return new ArchiveTargetMutationPlan(targetEnvelope, input.getAccountAssetFormatId(), + input.getTargetPhase(), grouped); } private void requireTargetIdentity(HistoryCommitMarker target, diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanCodec.java index 4c9a7bcda84..3c48b83925c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanCodec.java @@ -8,21 +8,24 @@ import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; /** Checksummed bounded codec for one durable target mutation plan. */ final class ArchiveTargetMutationPlanCodec { private static final int MAGIC = 0x54414d50; // TAMP - private static final short VERSION = 1; + private static final short VERSION = 2; private static final int HEADER_LENGTH = 12; private static final int MAX_PARTICIPANTS = 1024; private static final int MAX_MUTATIONS = 1_000_000; private static final int MAX_FIELD_LENGTH = 64 * 1024 * 1024; + private static final int MAX_IDENTITY_LENGTH = 256; static final int MAX_ENCODED_LENGTH = 128 * 1024 * 1024; private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); @@ -35,6 +38,8 @@ byte[] encode(ArchiveTargetMutationPlan plan) { output.writeShort(VERSION); output.writeShort(0); output.writeInt(0); + writeIdentity(output, plan.getAccountAssetFormatId()); + writeIdentity(output, plan.getTargetPhase().name()); writeBytes(output, target); output.writeInt(plan.getTarget().getParticipants().size()); for (String participant : plan.getTarget().getParticipants()) { @@ -87,6 +92,16 @@ ArchiveTargetMutationPlan decode(byte[] encoded) { || input.readInt() != encoded.length) { throw new IllegalArgumentException("Unsupported mutation-plan header"); } + String formatId = readIdentity(input); + if (!P66AccountAssetCodec.FORMAT_ID.equals(formatId)) { + throw new IllegalArgumentException("Unsupported AccountAsset transition format"); + } + Phase targetPhase; + try { + targetPhase = Phase.valueOf(readIdentity(input)); + } catch (IllegalArgumentException invalidPhase) { + throw new IllegalArgumentException("Unsupported AccountAsset target phase", invalidPhase); + } ArchiveProgressEnvelope target = progressCodec.decode(readBytes(input)); int participantCount = input.readInt(); if (participantCount <= 0 || participantCount > MAX_PARTICIPANTS @@ -115,7 +130,7 @@ ArchiveTargetMutationPlan decode(byte[] encoded) { if (input.available() != Integer.BYTES) { throw new IllegalArgumentException("Mutation-plan payload mismatch"); } - return new ArchiveTargetMutationPlan(target, mutations); + return new ArchiveTargetMutationPlan(target, formatId, targetPhase, mutations); } catch (EOFException truncated) { throw new IllegalArgumentException("Mutation plan is truncated", truncated); } catch (IOException invalid) { @@ -135,6 +150,27 @@ private static void writeBytes(DataOutputStream output, byte[] value) throws IOE output.write(value); } + private static void writeIdentity(DataOutputStream output, String value) throws IOException { + byte[] encoded = value.getBytes(StandardCharsets.US_ASCII); + if (!value.equals(new String(encoded, StandardCharsets.US_ASCII)) + || encoded.length == 0 || encoded.length > MAX_IDENTITY_LENGTH) { + throw new IllegalArgumentException("Mutation-plan identity is invalid"); + } + writeBytes(output, encoded); + } + + private static String readIdentity(DataInputStream input) throws IOException { + byte[] encoded = readBytes(input); + if (encoded.length == 0 || encoded.length > MAX_IDENTITY_LENGTH) { + throw new IllegalArgumentException("Mutation-plan identity is invalid"); + } + String value = new String(encoded, StandardCharsets.US_ASCII); + if (!Arrays.equals(encoded, value.getBytes(StandardCharsets.US_ASCII))) { + throw new IllegalArgumentException("Mutation-plan identity is not ASCII"); + } + return value; + } + private static byte[] readBytes(DataInputStream input) throws IOException { int length = input.readInt(); if (length < 0 || length > MAX_FIELD_LENGTH diff --git a/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java b/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java index 54e7539aa63..2a0dce1038c 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java @@ -31,6 +31,7 @@ import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.PreparedBlockProjection; import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.TargetAssetOptimization; import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.Flusher; import org.tron.core.db2.common.WrappedByteArray; @@ -248,9 +249,11 @@ public void resolvesActivationBlockFromProposalSixtySixAndFeedsSharedBridge() { TargetAssetOptimization activation = resolver.resolve(meta, view); PreparedBlockProjection result = bridge.prepare(view, activation); assertTrue(activation.isEnabled()); + assertEquals(Phase.P66_ACTIVATION, activation.getPhase()); + ArchiveTargetMutationPlan plan = plan(marker, view, result.seal(marker)); + assertEquals(Phase.P66_ACTIVATION, plan.getTargetPhase()); assertArrayEquals(Longs.toByteArray(50L), - plan(marker, view, result.seal(marker)) - .getMutations("account-asset").get(0).getValue()); + plan.getMutations("account-asset").get(0).getValue()); } verify(assetStore, times(1)).prefixQuery(any(byte[].class)); } @@ -278,7 +281,9 @@ public void inheritsUnchangedProposalSixtySixWithoutUsingProposalFiftyThree() { TargetAssetOptimization activation = resolver.resolve(meta, view); PreparedBlockProjection result = bridge.prepare(view, activation); assertFalse(activation.isEnabled()); + assertEquals(Phase.P66_OFF, activation.getPhase()); ArchiveTargetMutationPlan plan = plan(marker, view, result.seal(marker)); + assertEquals(Phase.P66_OFF, plan.getTargetPhase()); assertArrayEquals(raw.toByteArray(), plan.getMutations("account").get(0).getValue()); assertEquals(0, plan.getMutations("account-asset").size()); } @@ -338,6 +343,14 @@ public void rejectsMissingCorruptSubstitutedAndReorgActivationBeforePrefix() { assertThrows(ArchivePersistenceException.class, () -> bridge.prepare(view, resolver.resolve(meta(8), view))); } + try (Fixture regressed = new Fixture(participants())) { + regressed.rootPut("properties", proposal66Key(), ByteArray.fromLong(1L)); + BlockChangeView view = regressed.capture(meta, + databases -> databases.get("properties").put( + proposal66Key(), ByteArray.fromLong(0L))); + assertThrows(ArchivePersistenceException.class, + () -> resolver.resolve(meta, view)); + } verify(assetStore, never()).prefixQuery(any(byte[].class)); } @@ -471,7 +484,8 @@ public void sealedForwardPayloadCarriesExactViewAndRejectsMixedIdentity() { () -> prepared.sealPayload(marker(firstMeta))); AccountAssetForwardMutationManifest manifest = - new AccountAssetForwardMutationManifest(marker(firstMeta), Collections.emptyList()); + new AccountAssetForwardMutationManifest(marker(firstMeta), Phase.P66_OFF, + Collections.emptyList()); assertThrows(ArchivePersistenceException.class, () -> new ArchiveBlockForwardPayload(marker(firstMeta), secondView, manifest)); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java index a9a80b7e9a0..12bce03a60b 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java @@ -24,6 +24,7 @@ import org.tron.core.db2.ISession; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; import org.tron.core.db2.archive.ArchiveTargetApplyCoordinator.Stage; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.Flusher; import org.tron.core.db2.common.WrappedByteArray; @@ -91,8 +92,8 @@ public void captureBatchRecoversOnlyRemainingParticipantsFromDurablePlan() throw ArchiveParticipantMutationBatch batch; try (ViewFixture viewFixture = new ViewFixture()) { ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture( - target.getMeta(), new ArchiveBlockForwardMutationLimits( - 10, 10, 1024, 1024, 1024 * 1024)); + target.getMeta(), Phase.P66_ON, + new ArchiveBlockForwardMutationLimits(10, 10, 1024, 1024, 1024 * 1024)); capture.recordAccount(target.getMeta(), accountKey, BlockChangeView.PostValue.present(rawAccount), BlockChangeView.PostValue.present(canonicalAccount)); @@ -370,8 +371,8 @@ private static ArchiveParticipantMutationBatch capture(HistoryCommitMarker targe byte[] assetValue, boolean deleteAsset, byte[] proposalKey, byte[] proposalValue) { try (ViewFixture viewFixture = new ViewFixture()) { ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture( - target.getMeta(), new ArchiveBlockForwardMutationLimits( - 10, 10, 1024, 1024, 1024 * 1024)); + target.getMeta(), Phase.P66_ON, + new ArchiveBlockForwardMutationLimits(10, 10, 1024, 1024, 1024 * 1024)); capture.recordAccount(target.getMeta(), accountKey, BlockChangeView.PostValue.present(rawAccount), BlockChangeView.PostValue.present(canonicalAccount)); @@ -392,7 +393,8 @@ private static ArchiveParticipantMutationBatch capture(HistoryCommitMarker targe private static ArchiveParticipantMutationBatch captureEmpty(HistoryCommitMarker target) { try (ViewFixture viewFixture = new ViewFixture()) { ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture( - target.getMeta(), new ArchiveBlockForwardMutationLimits(0, 0, 0, 0, 0)); + target.getMeta(), Phase.P66_ON, + new ArchiveBlockForwardMutationLimits(0, 0, 0, 0, 0)); BlockChangeView view = viewFixture.capture(target.getMeta(), databases -> { }); capture.attach(view); return capture.seal(target); diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java index 32b9d99de39..98e9268c930 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java @@ -21,6 +21,7 @@ import org.tron.core.db2.archive.AccountAssetForwardMutationManifest.Entry; import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; import org.tron.core.db2.archive.AccountAssetForwardProjector.Projection; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.Flusher; import org.tron.core.db2.common.WrappedByteArray; @@ -50,7 +51,7 @@ public void collectsExactPostPutDeleteAndEmptyDeterministically() { databases.get("proposal").put(bytes(2, 3), bytes(1, 3)); }); ArchiveParticipantMutationBatchCollector collector = - new ArchiveParticipantMutationBatchCollector(); + new ArchiveParticipantMutationBatchCollector(Phase.P66_ON); ArchiveTargetMutationPlan firstPlan = new ArchiveTargetMutationPlanBuilder().build(marker, collector.collect(marker, firstView)); ArchiveTargetMutationPlan secondPlan = new ArchiveTargetMutationPlanBuilder().build(marker, @@ -77,7 +78,7 @@ public void accountMutationRequiresExplicitNoScanForwardProjection() { BlockChangeView view = fixture.capture(meta, databases -> databases.get("account").put(accountKey, rawAccount)); assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector().collect(marker, view)); + () -> new ArchiveParticipantMutationBatchCollector(Phase.P66_ON).collect(marker, view)); AccountAssetForwardProjector projector = (key, post) -> { assertArrayEquals(accountKey, key); assertArrayEquals(rawAccount, post.getValue()); @@ -86,7 +87,8 @@ public void accountMutationRequiresExplicitNoScanForwardProjection() { new AssetMutation(assetPut, BlockChangeView.PostValue.present(new byte[0])))); }; ArchiveParticipantMutationBatch batch = - new ArchiveParticipantMutationBatchCollector(projector).collect(marker, view); + new ArchiveParticipantMutationBatchCollector(Phase.P66_ON, projector) + .collect(marker, view); ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, batch); assertArrayEquals(canonicalAccount, @@ -108,7 +110,7 @@ public void rejectsViewIdentityCoverageAndMissingProjectionResult() { BlockChangeView view = exact.capture(meta, databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector().collect( + () -> new ArchiveParticipantMutationBatchCollector(Phase.P66_ON).collect( marker(meta(2), participants()), view)); } @@ -116,14 +118,14 @@ public void rejectsViewIdentityCoverageAndMissingProjectionResult() { BlockChangeView view = incomplete.capture(meta, databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector().collect(marker, view)); + () -> new ArchiveParticipantMutationBatchCollector(Phase.P66_ON).collect(marker, view)); } try (Fixture account = new Fixture(participants())) { BlockChangeView view = account.capture(meta, databases -> databases.get("account").put(bytes(1, 1), bytes(1, 2))); assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector((key, post) -> null) + () -> new ArchiveParticipantMutationBatchCollector(Phase.P66_ON, (key, post) -> null) .collect(marker, view)); } } @@ -153,7 +155,7 @@ public void manifestCollectsAccountCreateUpdateDeleteAndExactAssetStates() { databases.get("account").delete(deleteKey); }); AccountAssetForwardMutationManifest manifest = - new AccountAssetForwardMutationManifest(marker, Arrays.asList( + new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, Arrays.asList( entry(createKey, rawCreate, canonicalCreate, new AssetMutation(createAsset, BlockChangeView.PostValue.present(new byte[0]))), @@ -192,18 +194,19 @@ public void manifestRejectsMissingExtraTargetAndRawValueMismatch() { BlockChangeView view = fixture.capture(meta, databases -> databases.get("account").put(accountKey, rawAccount)); AccountAssetForwardMutationManifest missing = - new AccountAssetForwardMutationManifest(marker, Collections.emptyList()); + new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, Collections.emptyList()); assertThrows(ArchivePersistenceException.class, () -> new ArchiveParticipantMutationBatchCollector(missing).collect(marker, view)); - AccountAssetForwardMutationManifest extra = new AccountAssetForwardMutationManifest(marker, - Arrays.asList(entry(accountKey, rawAccount, rawAccount), - entry(bytes(2, 9), bytes(3, 9), bytes(3, 9)))); + AccountAssetForwardMutationManifest extra = + new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, + Arrays.asList(entry(accountKey, rawAccount, rawAccount), + entry(bytes(2, 9), bytes(3, 9), bytes(3, 9)))); assertThrows(ArchivePersistenceException.class, () -> new ArchiveParticipantMutationBatchCollector(extra).collect(marker, view)); AccountAssetForwardMutationManifest wrongRaw = - new AccountAssetForwardMutationManifest(marker, + new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, Collections.singletonList(entry(accountKey, bytes(3, 8), rawAccount))); assertThrows(ArchivePersistenceException.class, () -> new ArchiveParticipantMutationBatchCollector(wrongRaw).collect(marker, view)); @@ -215,7 +218,7 @@ public void manifestRejectsMissingExtraTargetAndRawValueMismatch() { BlockChangeView otherView = fixture.capture(otherMeta, databases -> databases.get("account").put(accountKey, rawAccount)); AccountAssetForwardMutationManifest wrongTarget = - new AccountAssetForwardMutationManifest(marker, + new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, Collections.singletonList(entry(accountKey, rawAccount, rawAccount))); assertThrows(ArchivePersistenceException.class, () -> new ArchiveParticipantMutationBatchCollector(wrongTarget) @@ -230,9 +233,10 @@ public void manifestRejectsDuplicateCrossAccountAndUnusedEntries() { byte[] rawAccount = bytes(3, 2); Entry entry = entry(accountKey, rawAccount, rawAccount); assertThrows(IllegalArgumentException.class, - () -> new AccountAssetForwardMutationManifest(marker, Arrays.asList(entry, entry))); + () -> new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, + Arrays.asList(entry, entry))); assertThrows(IllegalArgumentException.class, - () -> new AccountAssetForwardMutationManifest(marker, + () -> new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, Collections.singletonList(null))); assertThrows(IllegalArgumentException.class, () -> entry(accountKey, rawAccount, rawAccount, @@ -243,7 +247,8 @@ public void manifestRejectsDuplicateCrossAccountAndUnusedEntries() { new AssetMutation(bytes(3, 7), BlockChangeView.PostValue.absent()))); AccountAssetForwardMutationManifest singleUse = - new AccountAssetForwardMutationManifest(marker, Collections.singletonList(entry)); + new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, + Collections.singletonList(entry)); singleUse.begin(marker, Collections.singletonList(accountKey)); singleUse.project(accountKey, BlockChangeView.PostValue.present(rawAccount)); assertThrows(ArchivePersistenceException.class, @@ -251,7 +256,8 @@ public void manifestRejectsDuplicateCrossAccountAndUnusedEntries() { singleUse.complete(); AccountAssetForwardMutationManifest unused = - new AccountAssetForwardMutationManifest(marker, Collections.singletonList(entry)); + new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, + Collections.singletonList(entry)); unused.begin(marker, Collections.singletonList(accountKey)); assertThrows(ArchivePersistenceException.class, unused::complete); } @@ -267,7 +273,7 @@ public void recorderSealsUnorderedEventsIntoExactAccountAndAssetMutations() { byte[] deletedAsset = assetKey(updateKey, 2); byte[] removedAccountAsset = assetKey(deleteKey, 1); AccountAssetForwardMutationRecorder recorder = - new AccountAssetForwardMutationRecorder(meta, limits()); + new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); recorder.recordAssetDelete(meta, deleteKey, removedAccountAsset); recorder.recordAssetPut(meta, updateKey, emptyAsset, new byte[0]); @@ -308,9 +314,9 @@ public void recorderCanonicalizesDifferentEventOrders() { byte[] firstAsset = assetKey(accountKey, 1); byte[] secondAsset = assetKey(accountKey, 2); AccountAssetForwardMutationRecorder first = - new AccountAssetForwardMutationRecorder(meta, limits()); + new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); AccountAssetForwardMutationRecorder second = - new AccountAssetForwardMutationRecorder(meta, limits()); + new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); first.recordAssetDelete(meta, accountKey, secondAsset); first.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), @@ -343,7 +349,7 @@ public void recorderRejectsTargetDuplicatesIncompleteAndPostSealWrites() { byte[] assetKey = assetKey(accountKey, 1); AccountAssetForwardMutationRecorder wrongTarget = - new AccountAssetForwardMutationRecorder(meta, limits()); + new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); assertThrows(ArchivePersistenceException.class, () -> wrongTarget.recordAccount(otherMeta, accountKey, BlockChangeView.PostValue.present(rawAccount), @@ -351,7 +357,7 @@ public void recorderRejectsTargetDuplicatesIncompleteAndPostSealWrites() { assertThrows(ArchivePersistenceException.class, () -> wrongTarget.seal(otherMarker)); AccountAssetForwardMutationRecorder duplicates = - new AccountAssetForwardMutationRecorder(meta, limits()); + new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); duplicates.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), BlockChangeView.PostValue.present(rawAccount)); assertThrows(ArchivePersistenceException.class, @@ -365,12 +371,12 @@ public void recorderRejectsTargetDuplicatesIncompleteAndPostSealWrites() { () -> duplicates.recordAssetPut(meta, accountKey, bytes(3, 7), bytes(1, 3))); AccountAssetForwardMutationRecorder incomplete = - new AccountAssetForwardMutationRecorder(meta, limits()); + new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); incomplete.recordAssetDelete(meta, accountKey, assetKey); assertThrows(ArchivePersistenceException.class, () -> incomplete.seal(marker)); AccountAssetForwardMutationRecorder sealed = - new AccountAssetForwardMutationRecorder(meta, limits()); + new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); sealed.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), BlockChangeView.PostValue.present(rawAccount)); sealed.seal(marker); @@ -393,7 +399,7 @@ public void recorderDefensivelyTransfersPayloadBeforeCommittedMarkerExists() { byte[] expectedAssetKey = Arrays.copyOf(assetKey, assetKey.length); byte[] expectedAssetValue = Arrays.copyOf(assetValue, assetValue.length); AccountAssetForwardMutationRecorder recorder = - new AccountAssetForwardMutationRecorder(meta, limits()); + new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); recorder.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), BlockChangeView.PostValue.present(canonicalAccount)); @@ -428,7 +434,7 @@ public void blockCaptureOwnsViewRecorderAndBatchAsOneShot() { byte[] canonicalAccount = bytes(3, 3); byte[] assetKey = assetKey(accountKey, 1); ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, limits()); + new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, limits()); capture.recordAssetPut(meta, accountKey, assetKey, new byte[0]); capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), BlockChangeView.PostValue.present(canonicalAccount)); @@ -460,7 +466,7 @@ public void blockCapturePreconditionFailuresRemainRetryableBeforeManifestConsump HistoryCommitMarker marker = marker(meta, participants()); HistoryCommitMarker otherMarker = marker(otherMeta, participants()); ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, limits()); + new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, limits()); assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); try (Fixture exact = new Fixture(participants()); @@ -485,7 +491,7 @@ public void blockCaptureCoverageFailureConsumesOwnershipAndBecomesTerminal() { byte[] accountKey = bytes(2, 1); byte[] rawAccount = bytes(3, 2); ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, limits()); + new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, limits()); try (Fixture fixture = new Fixture(participants())) { BlockChangeView view = fixture.capture(meta, @@ -511,7 +517,7 @@ public void blockCaptureAbortBeforeAttachReleasesPayloadAndRejectsEveryTerminalA byte[] accountKey = bytes(2, 1); byte[] assetKey = assetKey(accountKey, 1); ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, limits()); + new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, limits()); capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(bytes(3, 2)), BlockChangeView.PostValue.present(bytes(3, 3))); capture.recordAssetPut(meta, accountKey, assetKey, bytes(3, 4)); @@ -543,7 +549,7 @@ public void blockCaptureAbortAfterAttachReleasesViewAndPayload() { HistoryCommitMarker marker = marker(meta, participants()); byte[] accountKey = bytes(2, 1); ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, limits()); + new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, limits()); capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), BlockChangeView.PostValue.absent()); @@ -577,7 +583,7 @@ public void captureLimitsAcceptExactBoundaryWithDeleteAndPresentEmpty() { ArchiveBlockForwardMutationLimits exact = new ArchiveBlockForwardMutationLimits(1, 2, 3, 1, 13); ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, exact); + new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, exact); capture.recordAssetPut(meta, accountKey, emptyAsset, new byte[0]); capture.recordAssetDelete(meta, accountKey, deletedAsset); capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), @@ -603,33 +609,33 @@ public void captureLimitsRejectEveryDimensionAndNegativeConfiguration() { () -> new ArchiveBlockForwardMutationLimits(-1, 1, 1, 1, 1)); AccountAssetForwardMutationRecorder accounts = new AccountAssetForwardMutationRecorder(meta, - new ArchiveBlockForwardMutationLimits(0, 1, 3, 1, 10)); + Phase.P66_ON, new ArchiveBlockForwardMutationLimits(0, 1, 3, 1, 10)); assertThrows(ArchivePersistenceException.class, () -> accounts.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), BlockChangeView.PostValue.absent())); AccountAssetForwardMutationRecorder assets = new AccountAssetForwardMutationRecorder(meta, - new ArchiveBlockForwardMutationLimits(1, 0, 3, 1, 10)); + Phase.P66_ON, new ArchiveBlockForwardMutationLimits(1, 0, 3, 1, 10)); assets.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), BlockChangeView.PostValue.absent()); assertThrows(ArchivePersistenceException.class, () -> assets.recordAssetDelete(meta, accountKey, assetKey)); AccountAssetForwardMutationRecorder keys = new AccountAssetForwardMutationRecorder(meta, - new ArchiveBlockForwardMutationLimits(1, 1, 1, 1, 10)); + Phase.P66_ON, new ArchiveBlockForwardMutationLimits(1, 1, 1, 1, 10)); assertThrows(ArchivePersistenceException.class, () -> keys.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), BlockChangeView.PostValue.absent())); AccountAssetForwardMutationRecorder values = new AccountAssetForwardMutationRecorder(meta, - new ArchiveBlockForwardMutationLimits(1, 1, 3, 0, 10)); + Phase.P66_ON, new ArchiveBlockForwardMutationLimits(1, 1, 3, 0, 10)); assertThrows(ArchivePersistenceException.class, () -> values.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(bytes(1, 2)), BlockChangeView.PostValue.present(bytes(1, 3)))); AccountAssetForwardMutationRecorder total = new AccountAssetForwardMutationRecorder(meta, - new ArchiveBlockForwardMutationLimits(1, 1, 3, 1, 3)); + Phase.P66_ON, new ArchiveBlockForwardMutationLimits(1, 1, 3, 1, 3)); assertThrows(ArchivePersistenceException.class, () -> total.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(bytes(1, 2)), @@ -644,7 +650,7 @@ public void captureLimitRejectionAndDuplicatesDoNotConsumeReservation() { byte[] firstAsset = assetKey(accountKey, 1); byte[] secondAsset = assetKey(accountKey, 2); ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture(meta, - new ArchiveBlockForwardMutationLimits(1, 2, 3, 1, 10)); + Phase.P66_ON, new ArchiveBlockForwardMutationLimits(1, 2, 3, 1, 10)); assertThrows(ArchivePersistenceException.class, () -> capture.recordAccount(meta, accountKey, @@ -677,7 +683,7 @@ public void captureLimitsReserveAttachedViewAtomicallyAndAllowRetry() { BlockSnapshotMeta meta = meta(19); HistoryCommitMarker marker = marker(meta, participants()); ArchiveBlockForwardMutationCapture total = new ArchiveBlockForwardMutationCapture(meta, - new ArchiveBlockForwardMutationLimits(0, 0, 3, 3, 3)); + Phase.P66_ON, new ArchiveBlockForwardMutationLimits(0, 0, 3, 3, 3)); try (Fixture fixture = new Fixture(participants())) { BlockChangeView tooLarge = fixture.capture(meta, databases -> databases.get("code").put(bytes(2, 1), bytes(2, 2))); @@ -689,9 +695,9 @@ public void captureLimitsReserveAttachedViewAtomicallyAndAllowRetry() { } ArchiveBlockForwardMutationCapture key = new ArchiveBlockForwardMutationCapture(meta, - new ArchiveBlockForwardMutationLimits(0, 0, 1, 2, 10)); + Phase.P66_ON, new ArchiveBlockForwardMutationLimits(0, 0, 1, 2, 10)); ArchiveBlockForwardMutationCapture value = new ArchiveBlockForwardMutationCapture(meta, - new ArchiveBlockForwardMutationLimits(0, 0, 2, 1, 10)); + Phase.P66_ON, new ArchiveBlockForwardMutationLimits(0, 0, 2, 1, 10)); try (Fixture fixture = new Fixture(participants())) { BlockChangeView keyTooLarge = fixture.capture(meta, databases -> databases.get("code").put(bytes(2, 1), new byte[0])); diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java index c9cba1e83cf..766271ca017 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; public class ArchiveParticipantRecoveryStorageTest { @@ -118,7 +119,8 @@ private static ArchiveTargetMutationPlan storePlan(Path checkpointPath, mutations.put("account", mutation("account", 2, 2)); mutations.put("account-asset", mutation("account-asset", 2, 2)); ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlan( - global(Kind.APPLY_CHECKPOINT, marker), mutations); + global(Kind.APPLY_CHECKPOINT, marker), P66AccountAssetCodec.FORMAT_ID, + Phase.P66_ON, mutations); new ArchiveTargetMutationPlanFile(checkpointPath).store(plan); return plan; } @@ -131,7 +133,8 @@ private static void storeSubstitutedPlan(Path checkpointPath, HistoryCommitMarke mutations.put("account-asset", Collections.singletonList( ArchiveParticipantMutation.put(bytes("replayed"), bytes("substituted-asset")))); new ArchiveTargetMutationPlanFile(checkpointPath).store(new ArchiveTargetMutationPlan( - global(Kind.APPLY_CHECKPOINT, marker), mutations)); + global(Kind.APPLY_CHECKPOINT, marker), P66AccountAssetCodec.FORMAT_ID, + Phase.P66_ON, mutations)); } private static void assertRecoveryFails(Path archive, Path checkpointPath, diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java index d9ac7e931de..1b6559358af 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java @@ -23,6 +23,7 @@ import org.junit.rules.TemporaryFolder; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; import org.tron.core.db2.archive.ArchiveTargetApplyCoordinator.Stage; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; public class ArchiveTargetApplyCoordinatorTest { @@ -47,7 +48,7 @@ public void appliesCheckpointParticipantsRefreshAndReaderInOrder() throws Except try (HistoryCommitStore history = fixture.openHistory()) { ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, barrier); - coordinator.apply(1, plans(), () -> { + coordinator.apply(1, Phase.P66_ON, plans(), () -> { assertTrue(insideBarrier.get()); assertEquals(1, fixture.account.loadProgress().getEpoch()); assertEquals(1, fixture.asset.loadProgress().getEpoch()); @@ -86,7 +87,7 @@ public void everyDurableStageFailureConvergesThroughFreshRecovery() throws Excep throw new IOException("injected during publication"); } }, (stage, path) -> failPlanStage(point, stage)); - assertThrows(IOException.class, () -> coordinator.apply(1, plans(), () -> { + assertThrows(IOException.class, () -> coordinator.apply(1, Phase.P66_ON, plans(), () -> { refreshes.incrementAndGet(); if (point == FailurePoint.DURING_REFRESH) { throw new IOException("injected during refresh"); diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java index b063326fe55..b1f656cc8b4 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java @@ -19,6 +19,7 @@ import org.junit.rules.TemporaryFolder; import org.tron.core.db2.archive.ArchiveParticipantMutationBatch.Mutation; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; public class ArchiveTargetMutationPlanBuilderTest { @@ -31,7 +32,7 @@ public void canonicalizesExactPhysicalMutationsAndOwnsInputBytes() { byte[] key = bytes(3, 3); byte[] value = bytes(2, 7); ArchiveParticipantMutationBatch first = new ArchiveParticipantMutationBatch(target, - Arrays.asList(Mutation.delete("storage-row", bytes(3, 2)), + Phase.P66_ON, Arrays.asList(Mutation.delete("storage-row", bytes(3, 2)), Mutation.put("account", key, value), Mutation.put("account", bytes(3, 1), new byte[0]))); key[0] = 99; @@ -46,7 +47,7 @@ public void canonicalizesExactPhysicalMutationsAndOwnsInputBytes() { assertEquals(participants(), new ArrayList<>(plan.getMutations().keySet())); ArchiveParticipantMutationBatch reordered = new ArchiveParticipantMutationBatch(target, - Arrays.asList(Mutation.put("account", bytes(3, 1), new byte[0]), + Phase.P66_ON, Arrays.asList(Mutation.put("account", bytes(3, 1), new byte[0]), Mutation.put("account", bytes(3, 3), bytes(2, 7)), Mutation.delete("storage-row", bytes(3, 2)))); assertArrayEquals(plan.digest(), @@ -62,7 +63,7 @@ public void rejectsUnknownDerivedAndDuplicatePhysicalKeys() { Mutation.delete("accountTrie", bytes(1, 1)))); assertThrows(IllegalArgumentException.class, () -> new ArchiveTargetMutationPlanBuilder().build(target, - new ArchiveParticipantMutationBatch(target, Arrays.asList( + new ArchiveParticipantMutationBatch(target, Phase.P66_ON, Arrays.asList( Mutation.put("account", bytes(1, 1), bytes(1, 2)), Mutation.delete("account", bytes(1, 1)))))); } @@ -71,7 +72,7 @@ public void rejectsUnknownDerivedAndDuplicatePhysicalKeys() { public void rejectsTargetIdentityAndExactParticipantSetMismatch() { HistoryCommitMarker target = marker(1, participants()); ArchiveParticipantMutationBatch batch = new ArchiveParticipantMutationBatch(target, - Collections.emptyList()); + Phase.P66_ON, Collections.emptyList()); assertThrows(ArchivePersistenceException.class, () -> new ArchiveTargetMutationPlanBuilder().build(marker(2, participants()), batch)); @@ -79,7 +80,8 @@ public void rejectsTargetIdentityAndExactParticipantSetMismatch() { HistoryCommitMarker incompleteTarget = marker(1, incomplete); assertThrows(ArchivePersistenceException.class, () -> new ArchiveTargetMutationPlanBuilder().build(incompleteTarget, - new ArchiveParticipantMutationBatch(incompleteTarget, Collections.emptyList()))); + new ArchiveParticipantMutationBatch(incompleteTarget, Phase.P66_ON, + Collections.emptyList()))); List oldExact27 = new ArrayList<>(participants()); oldExact27.add("abi"); @@ -87,14 +89,35 @@ public void rejectsTargetIdentityAndExactParticipantSetMismatch() { HistoryCommitMarker oldTarget = marker(1, oldExact27); assertThrows(ArchivePersistenceException.class, () -> new ArchiveTargetMutationPlanBuilder().build(oldTarget, - new ArchiveParticipantMutationBatch(oldTarget, Collections.emptyList()))); + new ArchiveParticipantMutationBatch(oldTarget, Phase.P66_ON, + Collections.emptyList()))); List v2OnlyExact25 = new ArrayList<>(participants()); v2OnlyExact25.remove("asset-issue"); HistoryCommitMarker v2OnlyTarget = marker(1, v2OnlyExact25); assertThrows(ArchivePersistenceException.class, () -> new ArchiveTargetMutationPlanBuilder().build(v2OnlyTarget, - new ArchiveParticipantMutationBatch(v2OnlyTarget, Collections.emptyList()))); + new ArchiveParticipantMutationBatch(v2OnlyTarget, Phase.P66_ON, + Collections.emptyList()))); + } + + @Test + public void bindsAccountAssetFormatAndPhaseIntoPlanDigest() { + HistoryCommitMarker target = marker(1, participants()); + List mutations = Collections.singletonList( + Mutation.put("account", bytes(3, 1), bytes(2, 2))); + ArchiveTargetMutationPlan activation = new ArchiveTargetMutationPlanBuilder().build(target, + new ArchiveParticipantMutationBatch(target, Phase.P66_ACTIVATION, mutations)); + ArchiveTargetMutationPlan enabled = new ArchiveTargetMutationPlanBuilder().build(target, + new ArchiveParticipantMutationBatch(target, Phase.P66_ON, mutations)); + + assertEquals(P66AccountAssetCodec.FORMAT_ID, activation.getAccountAssetFormatId()); + assertEquals(Phase.P66_ACTIVATION, activation.getTargetPhase()); + assertFalse(Arrays.equals(activation.digest(), enabled.digest())); + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveTargetMutationPlanBuilder().build(target, + new ArchiveParticipantMutationBatch(target, "legacy-format", Phase.P66_ON, + mutations))); } @Test @@ -121,7 +144,7 @@ archive, new HistoryCommitMarkerCodec())) { history.commitAll(Arrays.asList(zero, one)); ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, checkpointPath, engines, readerPath, participants, action -> action.run()); - coordinator.apply(new ArchiveParticipantMutationBatch(one, Arrays.asList( + coordinator.apply(new ArchiveParticipantMutationBatch(one, Phase.P66_ON, Arrays.asList( Mutation.put("account", bytes(2, 1), new byte[0]), Mutation.delete("storage-row", bytes(2, 2)))), () -> { }); } @@ -141,7 +164,7 @@ archive, new HistoryCommitMarkerCodec())) { private static void assertBuildFails(HistoryCommitMarker target, List mutations) { assertThrows(ArchivePersistenceException.class, () -> new ArchiveTargetMutationPlanBuilder().build(target, - new ArchiveParticipantMutationBatch(target, mutations))); + new ArchiveParticipantMutationBatch(target, Phase.P66_ON, mutations))); } private static List participants() { diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java index 6d66e6eee1d..2e42d0255e5 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java @@ -6,7 +6,10 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -19,6 +22,7 @@ import org.junit.rules.TemporaryFolder; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; import org.tron.core.db2.archive.ArchiveTargetMutationPlanFile.Stage; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; public class ArchiveTargetMutationPlanFileTest { @@ -35,21 +39,51 @@ public void codecRoundTripsExactPutDeleteAndEmptyValues() { new ArchiveTargetMutationPlanCodec().encode(plan)); assertEquals(1, decoded.getTarget().getEpoch()); + assertEquals(P66AccountAssetCodec.FORMAT_ID, decoded.getAccountAssetFormatId()); + assertEquals(Phase.P66_ON, decoded.getTargetPhase()); assertEquals(PARTICIPANTS, decoded.getTarget().getParticipants()); assertArrayEquals(bytes(3, 1), decoded.getMutations("account").get(0).getKey()); assertArrayEquals(new byte[0], decoded.getMutations("account").get(0).getValue()); assertNull(decoded.getMutations("account-asset").get(0).getValue()); assertArrayEquals(plan.digest(), decoded.digest()); + assertEquals("e4bd9becc28e2afae72429f5630b3bd04bbe55fe9ae651809667bc5d5ad43bbf", + BaseEncoding.base16().lowerCase().encode(plan.digest())); Map> substituted = new LinkedHashMap<>( plan.getMutations()); substituted.put("account", Collections.singletonList( ArchiveParticipantMutation.put(bytes(3, 1), bytes(1, 9)))); ArchiveTargetMutationPlan replacement = new ArchiveTargetMutationPlan( - plan.getTarget(), substituted); + plan.getTarget(), P66AccountAssetCodec.FORMAT_ID, Phase.P66_ON, substituted); assertFalse(Arrays.equals(plan.digest(), replacement.digest())); } + @Test + public void digestBindsFormatAndPhaseAndLegacyVersionFailsClosed() { + ArchiveTargetMutationPlan canonical = plan(1); + ArchiveTargetMutationPlan activation = new ArchiveTargetMutationPlan( + canonical.getTarget(), P66AccountAssetCodec.FORMAT_ID, Phase.P66_ACTIVATION, + canonical.getMutations()); + ArchiveTargetMutationPlan substitutedFormat = new ArchiveTargetMutationPlan( + canonical.getTarget(), "archive-state/p66-account-asset/legacy", + Phase.P66_ON, canonical.getMutations()); + + assertFalse(Arrays.equals(canonical.digest(), activation.digest())); + assertFalse(Arrays.equals(canonical.digest(), substitutedFormat.digest())); + ArchiveTargetMutationPlanCodec codec = new ArchiveTargetMutationPlanCodec(); + assertEquals(Phase.P66_ACTIVATION, + codec.decode(codec.encode(activation)).getTargetPhase()); + assertThrows(IllegalArgumentException.class, + () -> codec.decode(codec.encode(substitutedFormat))); + + byte[] legacy = codec.encode(canonical); + ByteBuffer.wrap(legacy).putShort(Integer.BYTES, (short) 1); + byte[] payload = Arrays.copyOf(legacy, legacy.length - Integer.BYTES); + ByteBuffer.wrap(legacy, legacy.length - Integer.BYTES, Integer.BYTES) + .putInt(Hashing.crc32c().hashBytes(payload).asInt()); + assertThrows(IllegalArgumentException.class, () -> codec.decode(legacy)); + } + @Test public void atomicFaultExposesOnlyOldOrNewPlan() throws Exception { Path checkpoint = temporaryFolder.newFolder("atomic").toPath().resolve("checkpoint.progress"); @@ -104,14 +138,17 @@ public void canonicalizesContainerOrderAndRejectsDuplicatePhysicalKeys() { ArchiveParticipantMutation.put(bytes(3, 3), bytes(1, 3)))); second.put("account-asset", Collections.singletonList( ArchiveParticipantMutation.delete(bytes(3, 2)))); - assertArrayEquals(new ArchiveTargetMutationPlan(target, first).digest(), - new ArchiveTargetMutationPlan(target, second).digest()); + assertArrayEquals(new ArchiveTargetMutationPlan(target, P66AccountAssetCodec.FORMAT_ID, + Phase.P66_ON, first).digest(), + new ArchiveTargetMutationPlan(target, P66AccountAssetCodec.FORMAT_ID, + Phase.P66_ON, second).digest()); second.put("account", Arrays.asList( ArchiveParticipantMutation.put(bytes(3, 1), bytes(1, 1)), ArchiveParticipantMutation.delete(bytes(3, 1)))); assertThrows(IllegalArgumentException.class, - () -> new ArchiveTargetMutationPlan(target, second)); + () -> new ArchiveTargetMutationPlan(target, P66AccountAssetCodec.FORMAT_ID, + Phase.P66_ON, second)); } private static ArchiveTargetMutationPlan plan(long epoch) { @@ -123,7 +160,8 @@ epoch, bytes(32, (int) epoch), bytes(16, (int) epoch + 10), ArchiveParticipantMutation.put(bytes(3, 1), new byte[0]))); mutations.put("account-asset", Collections.singletonList( ArchiveParticipantMutation.delete(bytes(3, 2)))); - return new ArchiveTargetMutationPlan(target, mutations); + return new ArchiveTargetMutationPlan(target, P66AccountAssetCodec.FORMAT_ID, + Phase.P66_ON, mutations); } private static byte[] bytes(int length, int value) { diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index c70eeab9b64..6ca7b9ea63d 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -39,6 +39,7 @@ import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.Flusher; import org.tron.core.db2.common.WrappedByteArray; @@ -1174,7 +1175,7 @@ private static AccountAssetBlockProjectionBridge.PreparedBlockProjection sealRea throw new ArchivePersistenceException("Prepared block projection target mismatch"); } return new ArchiveBlockForwardPayload(target, view, - new AccountAssetForwardMutationManifest(target, Collections.emptyList())); + new AccountAssetForwardMutationManifest(target, Phase.P66_ON, Collections.emptyList())); }); return projection; } From 144854774255f510da87dec129592de95096aefa Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 15:17:01 +0800 Subject: [PATCH 031/161] feat(chainbase): integrate archive startup recovery Bind the approved exact-27 archive format to startup admission, native participant recovery, runtime ownership, and request-scoped historical AccountAsset reads.\n\nKeep archive startup default-off and fail closed before normal producers when the base or authority set is incomplete. --- .../ArchiveAuthorityHandleSources.java | 130 +++++ .../ArchiveAuthoritySnapshotCollector.java | 177 ++++++ .../archive/ArchiveAuthoritySourceBundle.java | 138 +++++ .../core/db2/archive/ArchiveBaseManifest.java | 49 +- .../ArchiveFormatAdmissionValidator.java | 234 ++++++++ .../db2/archive/ArchiveHistoryWriter.java | 31 +- .../archive/ArchiveParticipantDescriptor.java | 17 +- .../ArchiveParticipantRecoveryStorage.java | 5 + .../core/db2/archive/ArchiveReadContext.java | 18 + .../core/db2/archive/ArchiveReadSnapshot.java | 6 + .../archive/ArchiveReaderHeadPublisher.java | 7 +- .../archive/ArchiveReaderPublicationGate.java | 8 +- .../ArchiveRecoveryAuthorityScanner.java | 12 +- .../db2/archive/ArchiveRuntimeAttachment.java | 30 + .../db2/archive/ArchiveRuntimeQueryGate.java | 117 ++++ .../ArchiveTargetApplyCoordinator.java | 8 +- .../db2/archive/AsyncArchiveHistorySink.java | 4 +- .../archive/CommittedHistoryAuthority.java | 13 + .../archive/DurableBlockReverseDiffSink.java | 2 +- ...=> DurableHistoryMarkerRangeEvidence.java} | 22 +- ...HistoricalAccountAssetBalanceResolver.java | 167 ++++++ .../HistoricalAccountAssetPrefixResolver.java | 230 ++++++++ .../core/db2/archive/HistoryCommitStore.java | 12 +- .../core/db2/archive/HistoryCoverage.java | 41 ++ .../db2/archive/P66AccountAssetCodec.java | 40 +- .../db2/archive/StateArchiveRuntimeOwner.java | 262 +++++++++ .../tron/core/db2/core/SnapshotManager.java | 77 ++- .../main/java/org/tron/core/db/Manager.java | 65 ++- .../core/db/StateArchiveBasePreflight.java | 45 ++ .../db/StateArchiveBasePreflightTest.java | 150 +++++ ...AccountAssetBlockProjectionBridgeTest.java | 16 +- .../ArchiveAuthorityHandleSourcesTest.java | 226 ++++++++ ...ArchiveAuthoritySnapshotCollectorTest.java | 210 +++++++ ...chiveBlockForwardMutationRecoveryTest.java | 178 +++++- .../ArchiveFormatAdmissionValidatorTest.java | 312 +++++++++++ .../db2/archive/ArchiveHistoryWriterTest.java | 74 +++ .../ArchiveParticipantDescriptorTest.java | 30 +- .../db2/archive/ArchiveReadSnapshotTest.java | 220 +++++++- .../archive/ArchiveRuntimeQueryGateTest.java | 162 ++++++ .../ArchiveTargetApplyCoordinatorTest.java | 305 +++++++++- .../ArchiveTargetMutationPlanBuilderTest.java | 8 +- .../ArchiveTargetMutationPlanFileTest.java | 84 +++ .../archive/AsyncArchiveHistorySinkTest.java | 10 +- .../CommittedHistoryAuthorityTest.java | 69 +++ ...urableHistoryMarkerRangeEvidenceTest.java} | 62 +-- ...oricalAccountAssetBalanceResolverTest.java | 526 ++++++++++++++++++ .../SnapshotOldValueCollectorTest.java | 153 ++++- ...eArchiveManagerStartupIntegrationTest.java | 289 ++++++++++ .../archive/StateArchiveRuntimeOwnerTest.java | 217 ++++++++ 49 files changed, 5062 insertions(+), 206 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSources.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthoritySnapshotCollector.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthoritySourceBundle.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveFormatAdmissionValidator.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGate.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/CommittedHistoryAuthority.java rename chainbase/src/main/java/org/tron/core/db2/archive/{DurableHistoryMarkerRangeReceipt.java => DurableHistoryMarkerRangeEvidence.java} (84%) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolver.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetPrefixResolver.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoryCoverage.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java create mode 100644 framework/src/main/java/org/tron/core/db/StateArchiveBasePreflight.java create mode 100644 framework/src/test/java/org/tron/core/db/StateArchiveBasePreflightTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthoritySnapshotCollectorTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveFormatAdmissionValidatorTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGateTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/CommittedHistoryAuthorityTest.java rename framework/src/test/java/org/tron/core/db2/archive/{DurableHistoryMarkerRangeReceiptTest.java => DurableHistoryMarkerRangeEvidenceTest.java} (72%) create mode 100644 framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolverTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSources.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSources.java new file mode 100644 index 00000000000..2e94a0d06d0 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSources.java @@ -0,0 +1,130 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.tron.core.db2.archive.ArchiveAuthoritySnapshotCollector.HistorySource; +import org.tron.core.db2.archive.ArchiveAuthoritySnapshotCollector.LatestSource; +import org.tron.core.db2.archive.ArchiveAuthoritySnapshotCollector.ProgressSource; +import org.tron.core.db2.archive.ArchiveAuthoritySnapshotCollector.ServingSource; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestStateFactory; + +/** Read-only collector adapters over already-opened archive authority handles. */ +public final class ArchiveAuthorityHandleSources + implements HistorySource, ProgressSource, ServingSource, LatestSource { + + private final CommittedHistoryAuthority history; + private final ArchiveTargetMutationPlanFile planFile; + private final ArchiveProgressFile checkpointFile; + private final ArchiveProgressFile readerFile; + private final Map participantSources; + private final PersistentServingKeyIndexCatalog catalog; + private final PinnedLatestStateFactory latestFactory; + + public ArchiveAuthorityHandleSources(CommittedHistoryAuthority history, Path checkpointPath, + Map participantSources, + Path readerVisiblePath, PersistentServingKeyIndexCatalog catalog, + PinnedLatestStateFactory latestFactory) { + this.history = Objects.requireNonNull(history, "history"); + this.planFile = new ArchiveTargetMutationPlanFile( + Objects.requireNonNull(checkpointPath, "checkpointPath")); + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + this.checkpointFile = new ArchiveProgressFile(checkpointPath, codec); + this.readerFile = new ArchiveProgressFile( + Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"), codec); + this.participantSources = exactParticipantSources(participantSources); + this.catalog = Objects.requireNonNull(catalog, "catalog"); + this.latestFactory = Objects.requireNonNull(latestFactory, "latestFactory"); + } + + @Override + public HistoryCoverage coverage() { + return history.coverage(); + } + + @Override + public HistoryCommitMarker first() { + long firstEpoch = history.firstEpoch(); + return firstEpoch < 0 ? null : history.get(firstEpoch); + } + + @Override + public HistoryCommitMarker head() { + return history.head(); + } + + @Override + public boolean mutationPlanPresent() throws IOException { + return planFile.loadIfPresent() != null; + } + + @Override + public ArchiveProgressEnvelope applyCheckpoint() throws IOException { + return checkpointFile.load(); + } + + @Override + public Map participantProgress() throws IOException { + Map loaded = new LinkedHashMap<>(); + for (Map.Entry entry + : participantSources.entrySet()) { + loaded.put(entry.getKey(), entry.getValue().loadProgress()); + } + return loaded; + } + + @Override + public ArchiveProgressEnvelope readerVisible() throws IOException { + return readerFile.load(); + } + + @Override + public ArchiveAuthoritySourceBundle.ServingGenerationSnapshot current() throws IOException { + ArchiveProgressEnvelope reader = readerFile.load(); + try (PersistentServingKeyIndexGeneration generation = catalog.pin(reader)) { + return snapshot(generation); + } + } + + @Override + public byte[] sourceIdentityDigest() throws IOException { + ArchiveProgressEnvelope reader = readerFile.load(); + try (PersistentServingKeyIndexGeneration generation = catalog.pin(reader); + PinnedLatestState latest = latestFactory.pin(generation)) { + if (latest.getBlockNumber() != generation.getIndexedThrough() + || !Arrays.equals(latest.getBlockHash(), generation.getHeadHash())) { + throw new ArchivePersistenceException( + "Pinned latest source does not match serving generation head"); + } + byte[] digest = latest.getSourceIdentityDigest(); + return digest == null ? null : Arrays.copyOf(digest, digest.length); + } + } + + private static ArchiveAuthoritySourceBundle.ServingGenerationSnapshot snapshot( + PersistentServingKeyIndexGeneration generation) { + return new ArchiveAuthoritySourceBundle.ServingGenerationSnapshot( + generation.getScopeIdentity(), generation.getParticipatingDatabases(), + generation.getIndexedFrom(), generation.getIndexedThrough(), generation.getHeadHash(), + generation.getAuthoritativePrefixDigest(), generation.getLatestSourceIdentityDigest()); + } + + private static Map exactParticipantSources( + Map actual) { + TreeMap sorted = new TreeMap<>(); + Objects.requireNonNull(actual, "participantSources").forEach(sorted::put); + List participants = ArchiveParticipantDescriptor.current().getParticipants(); + if (!new ArrayList<>(sorted.keySet()).equals(participants) || sorted.containsValue(null)) { + throw new IllegalArgumentException("Archive participant source set is not exact-27"); + } + return Collections.unmodifiableMap(new LinkedHashMap<>(sorted)); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthoritySnapshotCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthoritySnapshotCollector.java new file mode 100644 index 00000000000..05bba52197a --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthoritySnapshotCollector.java @@ -0,0 +1,177 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** Collects one drift-checked, read-only snapshot of archive startup authorities. */ +public final class ArchiveAuthoritySnapshotCollector { + + private final HistorySource history; + private final ProgressSource progress; + private final ServingSource serving; + private final LatestSource latest; + + public ArchiveAuthoritySnapshotCollector(HistorySource history, ProgressSource progress, + ServingSource serving, LatestSource latest) { + this.history = Objects.requireNonNull(history, "history"); + this.progress = Objects.requireNonNull(progress, "progress"); + this.serving = Objects.requireNonNull(serving, "serving"); + this.latest = Objects.requireNonNull(latest, "latest"); + } + + /** + * Reads mutable boundary authorities twice and rejects the complete result if any changed. + * Source implementations must be read-only; this class never opens or repairs storage. + */ + public ArchiveAuthoritySourceBundle collect() throws IOException { + HistoryCoverage coverageBefore = required(history.coverage(), "history coverage"); + HistoryCommitMarker first = required(history.first(), "first history marker"); + HistoryCommitMarker headBefore = required(history.head(), "history head"); + ArchiveProgressEnvelope readerBefore = required(progress.readerVisible(), "reader visible"); + ArchiveAuthoritySourceBundle.ServingGenerationSnapshot servingBefore = required( + serving.current(), "serving generation"); + byte[] latestBefore = required(latest.sourceIdentityDigest(), + "latest source identity digest"); + + boolean planBefore = progress.mutationPlanPresent(); + ArchiveProgressEnvelope checkpoint = required(progress.applyCheckpoint(), + "apply checkpoint"); + Map participantProgress = exactParticipantProgress( + progress.participantProgress()); + + boolean planAfter = progress.mutationPlanPresent(); + byte[] latestAfter = required(latest.sourceIdentityDigest(), + "latest source identity digest"); + ArchiveAuthoritySourceBundle.ServingGenerationSnapshot servingAfter = required( + serving.current(), "serving generation"); + ArchiveProgressEnvelope readerAfter = required(progress.readerVisible(), "reader visible"); + HistoryCommitMarker headAfter = required(history.head(), "history head"); + HistoryCoverage coverageAfter = required(history.coverage(), "history coverage"); + + if (planBefore != planAfter + || !sameCoverage(coverageBefore, coverageAfter) + || !sameMarker(headBefore, headAfter) + || !sameProgress(readerBefore, readerAfter) + || !sameServing(servingBefore, servingAfter) + || !Arrays.equals(latestBefore, latestAfter)) { + throw new ArchivePersistenceException( + "Archive authority changed while collecting startup snapshot"); + } + return new ArchiveAuthoritySourceBundle(planBefore, coverageBefore, first, headBefore, + checkpoint, participantProgress, readerBefore, servingBefore, latestBefore); + } + + private static boolean sameCoverage(HistoryCoverage left, HistoryCoverage right) { + return left.getFirstEpoch() == right.getFirstEpoch() + && left.getRecordCount() == right.getRecordCount() + && left.getHeadEpoch() == right.getHeadEpoch() + && Arrays.equals(left.getHeadHash(), right.getHeadHash()); + } + + private static Map exactParticipantProgress( + Map actual) { + required(actual, "participant progress"); + List participants = ArchiveParticipantDescriptor.current().getParticipants(); + if (!actual.keySet().equals(new LinkedHashSet<>(participants))) { + throw new ArchivePersistenceException("Participant progress set is not exact-27"); + } + Map copy = new LinkedHashMap<>(); + for (String participant : participants) { + copy.put(participant, required(actual.get(participant), participant + " progress")); + } + return copy; + } + + private static boolean sameMarker(HistoryCommitMarker left, HistoryCommitMarker right) { + return left.getMeta().equals(right.getMeta()) + && left.getPreviousEpoch() == right.getPreviousEpoch() + && sameHistoryLocation(left.getHistoryLocation(), right.getHistoryLocation()) + && sameIndexLocation(left.getIndexLocation(), right.getIndexLocation()) + && Arrays.equals(left.getBatchId(), right.getBatchId()) + && left.getDatabases().equals(right.getDatabases()); + } + + private static boolean sameHistoryLocation(HistoryLocation left, HistoryLocation right) { + return left.getSegmentId() == right.getSegmentId() + && left.getOffset() == right.getOffset() + && left.getRecordLength() == right.getRecordLength() + && left.getBodyChecksum() == right.getBodyChecksum() + && Arrays.equals(left.getBodyDigest(), right.getBodyDigest()); + } + + private static boolean sameIndexLocation(HistoryIndexLocation left, + HistoryIndexLocation right) { + return left.getOffset() == right.getOffset() + && left.getRecordLength() == right.getRecordLength() + && Arrays.equals(left.getDigest(), right.getDigest()); + } + + private static boolean sameProgress(ArchiveProgressEnvelope left, + ArchiveProgressEnvelope right) { + return left.getKind() == right.getKind() + && Objects.equals(left.getParticipant(), right.getParticipant()) + && left.getEpoch() == right.getEpoch() + && Arrays.equals(left.getBlockHash(), right.getBlockHash()) + && Arrays.equals(left.getBatchId(), right.getBatchId()) + && Arrays.equals(left.getPayloadDigest(), right.getPayloadDigest()) + && Arrays.equals(left.getMutationPlanDigest(), right.getMutationPlanDigest()) + && left.getParticipants().equals(right.getParticipants()) + && left.getScopeIdentity().equals(right.getScopeIdentity()); + } + + private static boolean sameServing( + ArchiveAuthoritySourceBundle.ServingGenerationSnapshot left, + ArchiveAuthoritySourceBundle.ServingGenerationSnapshot right) { + return Objects.equals(left.getScopeIdentity(), right.getScopeIdentity()) + && Objects.equals(left.getParticipants(), right.getParticipants()) + && left.getIndexedFromEpoch() == right.getIndexedFromEpoch() + && left.getIndexedThroughEpoch() == right.getIndexedThroughEpoch() + && Arrays.equals(left.getHeadHash(), right.getHeadHash()) + && Arrays.equals(left.getAuthoritativePrefixDigest(), + right.getAuthoritativePrefixDigest()) + && Arrays.equals(left.getLatestSourceIdentityDigest(), + right.getLatestSourceIdentityDigest()); + } + + private static T required(T value, String name) { + if (value == null) { + throw new ArchivePersistenceException("Missing archive authority: " + name); + } + return value; + } + + /** Read-only committed-history identities. */ + public interface HistorySource { + HistoryCoverage coverage() throws IOException; + + HistoryCommitMarker first() throws IOException; + + HistoryCommitMarker head() throws IOException; + } + + /** Read-only plan and C/D/R identities. */ + public interface ProgressSource { + boolean mutationPlanPresent() throws IOException; + + ArchiveProgressEnvelope applyCheckpoint() throws IOException; + + Map participantProgress() throws IOException; + + ArchiveProgressEnvelope readerVisible() throws IOException; + } + + /** Read-only current serving generation identity. */ + public interface ServingSource { + ArchiveAuthoritySourceBundle.ServingGenerationSnapshot current() throws IOException; + } + + /** Read-only identity of the pinned latest-state source set. */ + public interface LatestSource { + byte[] sourceIdentityDigest() throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthoritySourceBundle.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthoritySourceBundle.java new file mode 100644 index 00000000000..0958e37cbd0 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveAuthoritySourceBundle.java @@ -0,0 +1,138 @@ +package org.tron.core.db2.archive; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Immutable, read-only snapshots of authorities needed for archive startup admission. + * + *

The bundle deliberately does not know how to open engines or repair files. A future runtime + * adapter must collect every value from already-opened read-only sources before calling the + * validator. + */ +public final class ArchiveAuthoritySourceBundle { + + private final boolean mutationPlanPresent; + private final HistoryCoverage historyCoverage; + private final HistoryCommitMarker firstHistoryMarker; + private final HistoryCommitMarker headHistoryMarker; + private final ArchiveProgressEnvelope applyCheckpoint; + private final Map participantProgress; + private final ArchiveProgressEnvelope readerVisible; + private final ServingGenerationSnapshot servingGeneration; + private final byte[] latestSourceIdentityDigest; + + public ArchiveAuthoritySourceBundle(boolean mutationPlanPresent, + HistoryCoverage historyCoverage, HistoryCommitMarker firstHistoryMarker, + HistoryCommitMarker headHistoryMarker, + ArchiveProgressEnvelope applyCheckpoint, + Map participantProgress, + ArchiveProgressEnvelope readerVisible, ServingGenerationSnapshot servingGeneration, + byte[] latestSourceIdentityDigest) { + this.mutationPlanPresent = mutationPlanPresent; + this.historyCoverage = historyCoverage; + this.firstHistoryMarker = firstHistoryMarker; + this.headHistoryMarker = headHistoryMarker; + this.applyCheckpoint = applyCheckpoint; + this.participantProgress = participantProgress == null ? null + : Collections.unmodifiableMap(new LinkedHashMap<>(participantProgress)); + this.readerVisible = readerVisible; + this.servingGeneration = servingGeneration; + this.latestSourceIdentityDigest = copy(latestSourceIdentityDigest); + } + + boolean isMutationPlanPresent() { + return mutationPlanPresent; + } + + HistoryCoverage getHistoryCoverage() { + return historyCoverage; + } + + HistoryCommitMarker getFirstHistoryMarker() { + return firstHistoryMarker; + } + + HistoryCommitMarker getHeadHistoryMarker() { + return headHistoryMarker; + } + + ArchiveProgressEnvelope getApplyCheckpoint() { + return applyCheckpoint; + } + + Map getParticipantProgress() { + return participantProgress; + } + + ArchiveProgressEnvelope getReaderVisible() { + return readerVisible; + } + + ServingGenerationSnapshot getServingGeneration() { + return servingGeneration; + } + + byte[] getLatestSourceIdentityDigest() { + return copy(latestSourceIdentityDigest); + } + + private static byte[] copy(byte[] value) { + return value == null ? null : Arrays.copyOf(value, value.length); + } + + /** Read-only serving generation/catalog identity, independent of its physical engine. */ + public static final class ServingGenerationSnapshot { + private final String scopeIdentity; + private final List participants; + private final long indexedFromEpoch; + private final long indexedThroughEpoch; + private final byte[] headHash; + private final byte[] authoritativePrefixDigest; + private final byte[] latestSourceIdentityDigest; + + public ServingGenerationSnapshot(String scopeIdentity, List participants, + long indexedFromEpoch, long indexedThroughEpoch, byte[] headHash, + byte[] authoritativePrefixDigest, byte[] latestSourceIdentityDigest) { + this.scopeIdentity = scopeIdentity; + this.participants = participants == null ? null + : Collections.unmodifiableList(new java.util.ArrayList<>(participants)); + this.indexedFromEpoch = indexedFromEpoch; + this.indexedThroughEpoch = indexedThroughEpoch; + this.headHash = copy(headHash); + this.authoritativePrefixDigest = copy(authoritativePrefixDigest); + this.latestSourceIdentityDigest = copy(latestSourceIdentityDigest); + } + + String getScopeIdentity() { + return scopeIdentity; + } + + List getParticipants() { + return participants; + } + + long getIndexedFromEpoch() { + return indexedFromEpoch; + } + + long getIndexedThroughEpoch() { + return indexedThroughEpoch; + } + + byte[] getHeadHash() { + return copy(headHash); + } + + byte[] getAuthoritativePrefixDigest() { + return copy(authoritativePrefixDigest); + } + + byte[] getLatestSourceIdentityDigest() { + return copy(latestSourceIdentityDigest); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java index 6a7c633c0ed..58a7435af06 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java @@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; @@ -37,14 +38,23 @@ final class ArchiveBaseManifest { this.scopeIdentity = scopeIdentity(this.participants); Files.createDirectories(directory); if (Files.exists(path)) { - base = decode(Files.readAllBytes(path)); - if (!scopeIdentity.equals(base.scopeIdentity) - || !this.participants.equals(base.participants)) { - throw new ArchivePersistenceException("Archive manifest participant set mismatch"); - } + base = loadExisting(path, scopeIdentity, this.participants); } } + /** Validates an existing manifest without creating or modifying any filesystem entry. */ + static ExistingBase validateExisting(Path directory, List participants) + throws IOException { + List expectedParticipants = new ArrayList<>(participants); + Path manifest = directory.resolve("MANIFEST"); + if (!Files.isRegularFile(manifest, LinkOption.NOFOLLOW_LINKS)) { + throw new ArchivePersistenceException("Archive manifest is missing or not a regular file"); + } + BaseIdentity existing = loadExisting(manifest, scopeIdentity(expectedParticipants), + expectedParticipants); + return new ExistingBase(existing.epoch, existing.hash); + } + synchronized void ensureBase(BlockSnapshotMeta firstArchivedBlock) throws IOException { long epoch = firstArchivedBlock.getEpoch() - 1; byte[] hash = firstArchivedBlock.getParentHash(); @@ -154,6 +164,16 @@ private static String scopeIdentity(List participants) { return ArchiveParticipantDescriptor.scopeIdentity(participants); } + private static BaseIdentity loadExisting(Path path, String expectedScope, + List expectedParticipants) throws IOException { + BaseIdentity existing = decode(Files.readAllBytes(path)); + if (!expectedScope.equals(existing.scopeIdentity) + || !expectedParticipants.equals(existing.participants)) { + throw new ArchivePersistenceException("Archive manifest participant set mismatch"); + } + return existing; + } + private static void writeString(DataOutputStream output, String value) throws IOException { byte[] encoded = value.getBytes(StandardCharsets.UTF_8); if (encoded.length == 0 || encoded.length > 1024) { @@ -187,4 +207,23 @@ private BaseIdentity(String scopeIdentity, long epoch, byte[] hash, this.participants = new ArrayList<>(participants); } } + + /** Read-only identity returned by validation; it exposes no manifest mutation capability. */ + static final class ExistingBase { + private final long epoch; + private final byte[] hash; + + private ExistingBase(long epoch, byte[] hash) { + this.epoch = epoch; + this.hash = Arrays.copyOf(hash, hash.length); + } + + long getEpoch() { + return epoch; + } + + byte[] getHash() { + return Arrays.copyOf(hash, hash.length); + } + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveFormatAdmissionValidator.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveFormatAdmissionValidator.java new file mode 100644 index 00000000000..9f9a1a19fba --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveFormatAdmissionValidator.java @@ -0,0 +1,234 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +/** Read-only first-stage admission for the current archive base format. */ +public final class ArchiveFormatAdmissionValidator { + + private ArchiveFormatAdmissionValidator() { + } + + /** + * Inspects only directory existence and the base MANIFEST. CURRENT_BASE is deliberately not a + * claim that plan/C/D/R, serving generations, or latest-source authorities are startup-ready. + */ + public static Result inspect(Path archiveDirectory) { + return inspectBase(archiveDirectory).result; + } + + /** + * Compares explicit read-only authority snapshots after validating the current exact-27 base. + * No source is opened, repaired, created, or rewritten by this method. + */ + public static Result inspect(Path archiveDirectory, ArchiveAuthoritySourceBundle authorities) { + BaseInspection baseInspection = inspectBase(archiveDirectory); + if (baseInspection.result.getStatus() != Status.CURRENT_BASE) { + return baseInspection.result; + } + try { + requireReady(baseInspection.base, authorities); + return Result.currentReady(); + } catch (RuntimeException failure) { + return Result.quarantine(Reason.INCOMPLETE_OR_INCONSISTENT_AUTHORITIES, + failure.getMessage()); + } + } + + private static BaseInspection inspectBase(Path archiveDirectory) { + Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + if (!Files.exists(archiveDirectory, LinkOption.NOFOLLOW_LINKS)) { + return BaseInspection.of(Result.emptyNew(), null); + } + if (Files.isSymbolicLink(archiveDirectory) + || !Files.isDirectory(archiveDirectory, LinkOption.NOFOLLOW_LINKS)) { + return BaseInspection.of(Result.quarantine(Reason.INVALID_DIRECTORY, + "Archive path is not a real directory"), null); + } + final boolean empty; + try (Stream entries = Files.list(archiveDirectory)) { + empty = !entries.findAny().isPresent(); + } catch (IOException failure) { + return BaseInspection.of(Result.quarantine(Reason.INSPECTION_FAILED, + failure.getMessage()), null); + } + if (empty) { + return BaseInspection.of(Result.emptyNew(), null); + } + Path manifest = archiveDirectory.resolve("MANIFEST"); + if (!Files.isRegularFile(manifest, LinkOption.NOFOLLOW_LINKS)) { + return BaseInspection.of(Result.quarantine(Reason.NONEMPTY_WITHOUT_MANIFEST, + "Non-empty archive directory has no regular MANIFEST"), null); + } + try { + ArchiveBaseManifest.ExistingBase base = ArchiveBaseManifest.validateExisting(archiveDirectory, + ArchiveParticipantDescriptor.current().getParticipants()); + return BaseInspection.of(Result.currentBase(), base); + } catch (IOException | RuntimeException failure) { + return BaseInspection.of(Result.quarantine(Reason.UNSUPPORTED_OR_CORRUPT_MANIFEST, + failure.getMessage()), null); + } + } + + private static void requireReady(ArchiveBaseManifest.ExistingBase base, + ArchiveAuthoritySourceBundle authorities) { + Objects.requireNonNull(authorities, "authorities"); + if (authorities.isMutationPlanPresent()) { + throw new ArchivePersistenceException("Active mutation plan requires recovery"); + } + HistoryCoverage coverage = Objects.requireNonNull(authorities.getHistoryCoverage(), + "historyCoverage"); + HistoryCommitMarker first = Objects.requireNonNull(authorities.getFirstHistoryMarker(), + "firstHistoryMarker"); + HistoryCommitMarker head = Objects.requireNonNull(authorities.getHeadHistoryMarker(), + "headHistoryMarker"); + List participants = ArchiveParticipantDescriptor.current().getParticipants(); + ArchiveParticipantDescriptor.current().requireExactParticipants(first.getDatabases()); + ArchiveParticipantDescriptor.current().requireExactParticipants(head.getDatabases()); + long expectedRecordCount; + try { + expectedRecordCount = Math.addExact( + Math.subtractExact(coverage.getHeadEpoch(), coverage.getFirstEpoch()), 1); + } catch (ArithmeticException failure) { + throw new ArchivePersistenceException("History coverage range overflows", failure); + } + if (coverage.getFirstEpoch() != first.getMeta().getEpoch() + || coverage.getHeadEpoch() != head.getMeta().getEpoch() + || coverage.getRecordCount() != expectedRecordCount + || !Arrays.equals(coverage.getHeadHash(), head.getMeta().getBlockHash()) + || first.getMeta().getEpoch() != base.getEpoch() + 1 + || first.getPreviousEpoch() != base.getEpoch() + || !Arrays.equals(first.getMeta().getParentHash(), base.getHash()) + || head.getMeta().getEpoch() < first.getMeta().getEpoch()) { + throw new ArchivePersistenceException("History markers do not extend the manifest base"); + } + + ArchiveProgressEnvelope checkpoint = Objects.requireNonNull( + authorities.getApplyCheckpoint(), "applyCheckpoint"); + byte[] planDigest = checkpoint.getMutationPlanDigest(); + requireProgress(checkpoint, ArchiveProgressEnvelope.Kind.APPLY_CHECKPOINT, null, head, + planDigest, participants); + Map progress = Objects.requireNonNull( + authorities.getParticipantProgress(), "participantProgress"); + if (!progress.keySet().equals(new java.util.LinkedHashSet<>(participants))) { + throw new ArchivePersistenceException("Participant progress set is not exact-27"); + } + for (String participant : participants) { + requireProgress(Objects.requireNonNull(progress.get(participant), participant), + ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, participant, head, planDigest, + participants); + } + requireProgress(Objects.requireNonNull(authorities.getReaderVisible(), "readerVisible"), + ArchiveProgressEnvelope.Kind.READER_VISIBLE, null, head, planDigest, participants); + + ArchiveAuthoritySourceBundle.ServingGenerationSnapshot serving = Objects.requireNonNull( + authorities.getServingGeneration(), "servingGeneration"); + byte[] latestSource = exactDigest(authorities.getLatestSourceIdentityDigest(), + "latestSourceIdentityDigest"); + if (!ArchiveParticipantDescriptor.FORMAT_ID.equals(serving.getScopeIdentity()) + || !participants.equals(serving.getParticipants()) + || serving.getIndexedFromEpoch() != base.getEpoch() + || serving.getIndexedThroughEpoch() != head.getMeta().getEpoch() + || !Arrays.equals(serving.getHeadHash(), head.getMeta().getBlockHash()) + || !Arrays.equals(serving.getLatestSourceIdentityDigest(), latestSource)) { + throw new ArchivePersistenceException("Serving generation authority mismatch"); + } + } + + private static void requireProgress(ArchiveProgressEnvelope progress, + ArchiveProgressEnvelope.Kind kind, String participant, HistoryCommitMarker head, + byte[] planDigest, List participants) { + progress.requireIdentity(kind, participant, head.getMeta().getEpoch(), + head.getMeta().getBlockHash(), head.getBatchId(), + head.getHistoryLocation().getBodyDigest(), planDigest, participants); + } + + private static byte[] exactDigest(byte[] digest, String name) { + if (digest == null || digest.length != 32) { + throw new ArchivePersistenceException(name + " must be exactly 32 bytes"); + } + byte[] zero = new byte[32]; + if (Arrays.equals(digest, zero)) { + throw new ArchivePersistenceException(name + " must not be zero"); + } + return digest; + } + + public enum Status { + EMPTY_NEW, + CURRENT_BASE, + CURRENT_READY, + QUARANTINE_REQUIRED + } + + public enum Reason { + NONE, + INVALID_DIRECTORY, + INSPECTION_FAILED, + NONEMPTY_WITHOUT_MANIFEST, + UNSUPPORTED_OR_CORRUPT_MANIFEST, + INCOMPLETE_OR_INCONSISTENT_AUTHORITIES + } + + /** Immutable admission result; CURRENT_READY requires the explicit authority-bundle overload. */ + public static final class Result { + private final Status status; + private final Reason reason; + private final String detail; + + private Result(Status status, Reason reason, String detail) { + this.status = status; + this.reason = reason; + this.detail = detail; + } + + public Status getStatus() { + return status; + } + + public Reason getReason() { + return reason; + } + + public String getDetail() { + return detail; + } + + private static Result emptyNew() { + return new Result(Status.EMPTY_NEW, Reason.NONE, ""); + } + + private static Result currentBase() { + return new Result(Status.CURRENT_BASE, Reason.NONE, ""); + } + + private static Result currentReady() { + return new Result(Status.CURRENT_READY, Reason.NONE, ""); + } + + private static Result quarantine(Reason reason, String detail) { + return new Result(Status.QUARANTINE_REQUIRED, reason, detail == null ? "" : detail); + } + } + + private static final class BaseInspection { + private final Result result; + private final ArchiveBaseManifest.ExistingBase base; + + private BaseInspection(Result result, ArchiveBaseManifest.ExistingBase base) { + this.result = result; + this.base = base; + } + + private static BaseInspection of(Result result, ArchiveBaseManifest.ExistingBase base) { + return new BaseInspection(result, base); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java index d8a2d1ed9db..1742d8659e7 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java @@ -17,7 +17,8 @@ * Ordered history body/index/marker writer. A marker is the durable history boundary H; reader * visibility R is a separate recovery authority and is not yet integrated into this prototype. */ -public final class ArchiveHistoryWriter implements DurableBlockReverseDiffSink, Closeable { +public final class ArchiveHistoryWriter + implements DurableBlockReverseDiffSink, CommittedHistoryAuthority, Closeable { static final int MAX_RESTART_TAIL_RECORDS = 1024; @@ -133,17 +134,37 @@ public synchronized void revert(BlockSnapshotMeta meta) { } public synchronized HistoryCommitMarker committedHead() { - return commits.head(); + return head(); } synchronized HistoryCommitMarker committedMarker(long epoch) { - HistoryCommitMarker marker = commits.get(epoch); + HistoryCommitMarker marker = get(epoch); if (marker == null) { return null; } return commitCodec.decode(commitCodec.encode(marker)); } + @Override + public synchronized HistoryCommitMarker head() { + return commits.head(); + } + + @Override + public synchronized HistoryCommitMarker get(long epoch) { + return commits.get(epoch); + } + + @Override + public synchronized long firstEpoch() { + return commits.firstEpoch(); + } + + @Override + public synchronized HistoryCoverage coverage() { + return commits.coverage(); + } + @Override public synchronized void awaitCommitted(long epoch) { HistoryCommitMarker head = commits.head(); @@ -153,8 +174,8 @@ public synchronized void awaitCommitted(long epoch) { } @Override - public DurableHistoryMarkerRangeReceipt createMarkerRangeReceipt(int maxMarkers) { - return new DurableHistoryMarkerRangeReceipt(this, maxMarkers); + public DurableHistoryMarkerRangeEvidence createMarkerRangeEvidence(int maxMarkers) { + return new DurableHistoryMarkerRangeEvidence(this, maxMarkers); } @Override diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java index 139ac4445e6..d809f073003 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantDescriptor.java @@ -14,10 +14,10 @@ import java.util.Objects; import java.util.Set; -/** Approved archive participant identity with stable Store IDs and reserved tombstones. */ +/** Approved archive participant identity with stable Store IDs. */ final class ArchiveParticipantDescriptor { - static final String FORMAT_ID = "archive-state/exact-26-abi-tombstone/v1"; + static final String FORMAT_ID = "archive-state/exact-27-abi-retained/v1"; static final int ABI_STORE_ID = 1; private static final ArchiveParticipantDescriptor CURRENT = @@ -31,6 +31,7 @@ final class ArchiveParticipantDescriptor { private ArchiveParticipantDescriptor() { LinkedHashMap stores = new LinkedHashMap<>(); + stores.put(ABI_STORE_ID, "abi"); stores.put(2, "accountid-index"); stores.put(3, "account-index"); stores.put(4, "account"); @@ -59,9 +60,7 @@ private ArchiveParticipantDescriptor() { stores.put(27, "IncrementalMerkleTree"); activeByStoreId = Collections.unmodifiableMap(stores); - LinkedHashMap tombstones = new LinkedHashMap<>(); - tombstones.put(ABI_STORE_ID, "abi"); - tombstonesByStoreId = Collections.unmodifiableMap(tombstones); + tombstonesByStoreId = Collections.emptyMap(); activeDatabases = Collections.unmodifiableSet( new LinkedHashSet<>(activeByStoreId.values())); @@ -136,13 +135,13 @@ private void validateStoreIds() { expected.add(storeId); } if (!allIds.equals(new LinkedHashSet<>(expected)) - || activeDatabases.size() != 26 - || !tombstonesByStoreId.equals( - Collections.singletonMap(ABI_STORE_ID, "abi")) + || activeDatabases.size() != 27 + || !tombstonesByStoreId.isEmpty() || !Collections.disjoint(activeDatabases, excludedDatabases) + || !activeDatabases.contains("abi") || !activeDatabases.containsAll( Arrays.asList("asset-issue", "asset-issue-v2"))) { - throw new IllegalStateException("Invalid exact-26 archive participant descriptor"); + throw new IllegalStateException("Invalid exact-27 archive participant descriptor"); } } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java index 37537a40dd6..5268b5f5e27 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java @@ -119,6 +119,11 @@ public RecoverySnapshot scan() throws IOException { return snapshot; } + /** Returns the committed H head observed by this startup recovery session. */ + public HistoryCommitMarker committedHead() { + return history.head(); + } + @Override public void truncateHistoryAndSync(long historyHead) throws IOException { ArchiveTruncationIntent.prepare(archiveDirectory, history, index, bodies, historyHead, diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java index 2265bbcb007..427230d5c59 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java @@ -19,6 +19,10 @@ public final class ArchiveReadContext implements Closeable { private final ArchiveReadSnapshot snapshot; private final Map> adapters; + private final HistoricalAccountAssetBalanceResolver accountAssetResolver = + new HistoricalAccountAssetBalanceResolver(); + private final HistoricalAccountAssetPrefixResolver accountAssetPrefixResolver = + new HistoricalAccountAssetPrefixResolver(); private boolean closed; private ArchiveReadContext(ArchiveReadSnapshot snapshot, @@ -60,6 +64,20 @@ public long getPinnedBlock() { return snapshot.getPinnedBlock(); } + /** Resolves exact Account bytes and one P66-aware token balance from this request snapshot. */ + public synchronized HistoricalAccountAssetBalanceResolver.Result resolveAccountAsset( + byte[] address, String tokenId) throws IOException { + ensureOpen(); + return accountAssetResolver.resolve(snapshot, address, tokenId); + } + + /** Resolves all token balances for exactly one Account under explicit query budgets. */ + public synchronized HistoricalAccountAssetPrefixResolver.Result resolveAccountAssets( + byte[] address, HistoricalAccountAssetPrefixResolver.Limits limits) throws IOException { + ensureOpen(); + return accountAssetPrefixResolver.resolve(snapshot, address, limits); + } + /** Resolves one logical contract slot using contract metadata from this same pinned context. */ public synchronized Optional getStorage(byte[] contractAddress, byte[] logicalSlot) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java index 64013575211..ce3537cc45b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java @@ -129,6 +129,12 @@ public byte[] getPinnedHash() { return Arrays.copyOf(pinnedHash, pinnedHash.length); } + /** Revalidates that all request-owned resources still expose the pinned generation identity. */ + public synchronized void requirePinnedIdentity() { + ensureOpen(); + validateIdentity(targetBlock, pinnedBlock); + } + @Override public synchronized void close() throws IOException { if (closed) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java index 542857d9320..4962e8e29bb 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java @@ -11,16 +11,17 @@ /** Atomically publishes a reader-visible R identity derived from committed history. */ public final class ArchiveReaderHeadPublisher { - private final HistoryCommitStore history; + private final CommittedHistoryAuthority history; private final ArchiveProgressFile progressFile; private final List participants; - public ArchiveReaderHeadPublisher(HistoryCommitStore history, Path path, + public ArchiveReaderHeadPublisher(CommittedHistoryAuthority history, Path path, List participants) { this(history, path, participants, temporary -> { }); } - ArchiveReaderHeadPublisher(HistoryCommitStore history, Path path, List participants, + ArchiveReaderHeadPublisher(CommittedHistoryAuthority history, Path path, + List participants, ArchiveProgressFile.FaultHook faultHook) { this.history = Objects.requireNonNull(history, "history"); this.progressFile = new ArchiveProgressFile(Objects.requireNonNull(path, "path"), diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java index f3c44621077..63961e94685 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java @@ -16,7 +16,7 @@ /** Publishes reader-visible R only after fresh H/C/D identity convergence under one barrier. */ public final class ArchiveReaderPublicationGate { - private final HistoryCommitStore history; + private final CommittedHistoryAuthority history; private final ProgressSource checkpointSource; private final Map participantSources; private final Path readerVisiblePath; @@ -25,7 +25,7 @@ public final class ArchiveReaderPublicationGate { private final List participants; private final ArchiveStateBarrier barrier; - public ArchiveReaderPublicationGate(HistoryCommitStore history, + public ArchiveReaderPublicationGate(CommittedHistoryAuthority history, ProgressSource checkpointSource, Map participantSources, Path readerVisiblePath, List participants, ArchiveStateBarrier barrier) { @@ -33,7 +33,7 @@ public ArchiveReaderPublicationGate(HistoryCommitStore history, temporary -> { }); } - ArchiveReaderPublicationGate(HistoryCommitStore history, + ArchiveReaderPublicationGate(CommittedHistoryAuthority history, ProgressSource checkpointSource, Map participantSources, Path readerVisiblePath, List participants, ArchiveStateBarrier barrier, @@ -56,7 +56,7 @@ public ArchiveReaderPublicationGate(HistoryCommitStore history, this.barrier = Objects.requireNonNull(barrier, "barrier"); } - public static ArchiveReaderPublicationGate forFiles(HistoryCommitStore history, + public static ArchiveReaderPublicationGate forFiles(CommittedHistoryAuthority history, Path checkpointPath, Map participantPaths, Path readerVisiblePath, List participants, ArchiveStateBarrier barrier) { Objects.requireNonNull(checkpointPath, "checkpointPath"); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java index d34c28af95b..6dba5410e5a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java @@ -14,14 +14,14 @@ /** File-backed prototype authority adapters for one fresh validating recovery scan. */ public final class ArchiveRecoveryAuthorityScanner { - private final HistoryCommitStore history; + private final CommittedHistoryAuthority history; private final Path checkpointPath; private final Map participantSources; private final Path readerVisiblePath; private final List participants; private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); - public ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpointPath, + public ArchiveRecoveryAuthorityScanner(CommittedHistoryAuthority history, Path checkpointPath, Map participantPaths, Path readerVisiblePath, List participants) { this.history = Objects.requireNonNull(history, "history"); @@ -40,7 +40,7 @@ public ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpoi this.participantSources = Collections.unmodifiableMap(sources); } - private ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpointPath, + private ArchiveRecoveryAuthorityScanner(CommittedHistoryAuthority history, Path checkpointPath, Map participantBatches, Path readerVisiblePath, List participants, boolean batchAuthority) { this.history = Objects.requireNonNull(history, "history"); @@ -59,7 +59,7 @@ private ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpo this.participantSources = Collections.unmodifiableMap(sources); } - private ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpointPath, + private ArchiveRecoveryAuthorityScanner(CommittedHistoryAuthority history, Path checkpointPath, Map participantSources, Path readerVisiblePath, List participants, byte nativeEngineAuthority) { @@ -78,7 +78,7 @@ private ArchiveRecoveryAuthorityScanner(HistoryCommitStore history, Path checkpo } public static ArchiveRecoveryAuthorityScanner forParticipantBatches( - HistoryCommitStore history, Path checkpointPath, + CommittedHistoryAuthority history, Path checkpointPath, Map participantBatches, Path readerVisiblePath, List participants) { return new ArchiveRecoveryAuthorityScanner(history, checkpointPath, participantBatches, @@ -86,7 +86,7 @@ public static ArchiveRecoveryAuthorityScanner forParticipantBatches( } public static ArchiveRecoveryAuthorityScanner forParticipants( - HistoryCommitStore history, Path checkpointPath, + CommittedHistoryAuthority history, Path checkpointPath, Map participantEngines, Path readerVisiblePath, List participants) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java new file mode 100644 index 00000000000..d94d0ce1e79 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java @@ -0,0 +1,30 @@ +package org.tron.core.db2.archive; + +import java.util.Objects; + +/** Borrowed archive runtime collaborators installed into SnapshotManager as one unit. */ +public final class ArchiveRuntimeAttachment { + + private final OldValueCollector collector; + private final ArchiveBlockProjectionPreparer projectionPreparer; + private final DurableBlockReverseDiffSink sink; + + public ArchiveRuntimeAttachment(OldValueCollector collector, + ArchiveBlockProjectionPreparer projectionPreparer, DurableBlockReverseDiffSink sink) { + this.collector = Objects.requireNonNull(collector, "collector"); + this.projectionPreparer = Objects.requireNonNull(projectionPreparer, "projectionPreparer"); + this.sink = Objects.requireNonNull(sink, "sink"); + } + + public OldValueCollector getCollector() { + return collector; + } + + public ArchiveBlockProjectionPreparer getProjectionPreparer() { + return projectionPreparer; + } + + public DurableBlockReverseDiffSink getSink() { + return sink; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGate.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGate.java new file mode 100644 index 00000000000..76e6d6cbdab --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGate.java @@ -0,0 +1,117 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Objects; + +/** Tracks request-owned archive snapshots while a runtime enters quiescence. */ +public final class ArchiveRuntimeQueryGate implements Closeable { + + public enum State { + RUNNING, + QUIESCING, + CLOSED + } + + private final SnapshotPinSource source; + private State state = State.RUNNING; + private int activeLeases; + + public ArchiveRuntimeQueryGate(ArchiveGenerationCapsule capsule) { + this(Objects.requireNonNull(capsule, "capsule")::pin); + } + + ArchiveRuntimeQueryGate(SnapshotPinSource source) { + this.source = Objects.requireNonNull(source, "source"); + } + + /** Pins and registers one request atomically against the quiesce transition. */ + public synchronized Lease pin(long targetBlock) throws IOException { + if (state != State.RUNNING) { + throw new IllegalStateException("Archive query gate is not running: " + state); + } + ArchiveReadSnapshot snapshot = Objects.requireNonNull(source.pin(targetBlock), "snapshot"); + activeLeases++; + return new Lease(this, snapshot); + } + + /** Stops admission of new requests without waiting for existing leases. */ + public synchronized void quiesce() { + if (state == State.RUNNING) { + state = State.QUIESCING; + } + } + + public synchronized State getState() { + return state; + } + + public synchronized int getActiveLeaseCount() { + return activeLeases; + } + + public synchronized boolean isDrained() { + return activeLeases == 0; + } + + /** Finishes closure only after quiescence has rejected new pins and every lease has drained. */ + @Override + public synchronized void close() { + quiesce(); + if (state == State.CLOSED) { + return; + } + if (activeLeases != 0) { + throw new IllegalStateException( + "Archive query gate still has active leases: " + activeLeases); + } + state = State.CLOSED; + } + + private synchronized void release() { + if (activeLeases <= 0) { + throw new IllegalStateException("Archive query lease count underflow"); + } + activeLeases--; + } + + @FunctionalInterface + interface SnapshotPinSource { + ArchiveReadSnapshot pin(long targetBlock) throws IOException; + } + + /** One request-owned snapshot whose close releases both resources and gate accounting. */ + public static final class Lease implements Closeable { + + private final ArchiveRuntimeQueryGate gate; + private final ArchiveReadSnapshot snapshot; + private boolean closed; + + private Lease(ArchiveRuntimeQueryGate gate, ArchiveReadSnapshot snapshot) { + this.gate = gate; + this.snapshot = snapshot; + } + + public synchronized ArchiveReadSnapshot getSnapshot() { + if (closed) { + throw new IllegalStateException("Archive query lease is closed"); + } + return snapshot; + } + + @Override + public void close() throws IOException { + synchronized (this) { + if (closed) { + return; + } + closed = true; + } + try { + snapshot.close(); + } finally { + gate.release(); + } + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java index f0e51b324bd..b07f4949382 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java @@ -16,7 +16,7 @@ /** Advances one standalone normal target through C, mixed D, latest refresh, and R. */ public final class ArchiveTargetApplyCoordinator { - private final HistoryCommitStore history; + private final CommittedHistoryAuthority history; private final ArchiveProgressFile checkpointFile; private final ArchiveTargetMutationPlanFile mutationPlanFile; private final Map participantEngines; @@ -25,14 +25,14 @@ public final class ArchiveTargetApplyCoordinator { private final ArchiveReaderPublicationGate publicationGate; private final FaultHook faultHook; - public ArchiveTargetApplyCoordinator(HistoryCommitStore history, Path checkpointPath, + public ArchiveTargetApplyCoordinator(CommittedHistoryAuthority history, Path checkpointPath, Map participantEngines, Path readerVisiblePath, List participants, ArchiveStateBarrier barrier) { this(history, checkpointPath, participantEngines, readerVisiblePath, participants, barrier, (stage, participant) -> { }, temporary -> { }, (stage, path) -> { }); } - ArchiveTargetApplyCoordinator(HistoryCommitStore history, Path checkpointPath, + ArchiveTargetApplyCoordinator(CommittedHistoryAuthority history, Path checkpointPath, Map participantEngines, Path readerVisiblePath, List participants, ArchiveStateBarrier barrier, FaultHook faultHook, ArchiveProgressFile.FaultHook publicationFaultHook) { @@ -40,7 +40,7 @@ public ArchiveTargetApplyCoordinator(HistoryCommitStore history, Path checkpoint faultHook, publicationFaultHook, (stage, path) -> { }); } - ArchiveTargetApplyCoordinator(HistoryCommitStore history, Path checkpointPath, + ArchiveTargetApplyCoordinator(CommittedHistoryAuthority history, Path checkpointPath, Map participantEngines, Path readerVisiblePath, List participants, ArchiveStateBarrier barrier, FaultHook faultHook, ArchiveProgressFile.FaultHook publicationFaultHook, diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java b/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java index d86a5d2e153..fa6f44db049 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AsyncArchiveHistorySink.java @@ -117,9 +117,9 @@ public void awaitCommitted(long epoch) { } @Override - public DurableHistoryMarkerRangeReceipt createMarkerRangeReceipt(int maxMarkers) { + public DurableHistoryMarkerRangeEvidence createMarkerRangeEvidence(int maxMarkers) { ensureOperational(); - return new DurableHistoryMarkerRangeReceipt(writer, maxMarkers); + return new DurableHistoryMarkerRangeEvidence(writer, maxMarkers); } /** Releases completed queue bookkeeping after the corresponding disk epoch is durable. */ diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/CommittedHistoryAuthority.java b/chainbase/src/main/java/org/tron/core/db2/archive/CommittedHistoryAuthority.java new file mode 100644 index 00000000000..a7df24b4cfe --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/CommittedHistoryAuthority.java @@ -0,0 +1,13 @@ +package org.tron.core.db2.archive; + +/** Read-only committed State History authority shared by normal and recovery paths. */ +public interface CommittedHistoryAuthority { + + HistoryCommitMarker head(); + + HistoryCommitMarker get(long epoch); + + long firstEpoch(); + + HistoryCoverage coverage(); +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java b/chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java index 7b111eff1d1..abbf4fc7ad4 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/DurableBlockReverseDiffSink.java @@ -5,7 +5,7 @@ public interface DurableBlockReverseDiffSink extends BlockReverseDiffSink { void awaitCommitted(long epoch); - DurableHistoryMarkerRangeReceipt createMarkerRangeReceipt(int maxMarkers); + DurableHistoryMarkerRangeEvidence createMarkerRangeEvidence(int maxMarkers); void releaseThrough(long epoch); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceipt.java b/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeEvidence.java similarity index 84% rename from chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceipt.java rename to chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeEvidence.java index f90e0927e51..9692daf5896 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceipt.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeEvidence.java @@ -7,19 +7,19 @@ import java.util.Objects; import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; -/** Bounded authoritative marker receipt for one exact frozen flush range. */ -public final class DurableHistoryMarkerRangeReceipt { +/** Bounded authoritative marker evidence for one exact frozen flush range. */ +public final class DurableHistoryMarkerRangeEvidence { private final Source source; private final int maxMarkers; private final List participants; private final HistoryCommitMarkerCodec codec = new HistoryCommitMarkerCodec(); - public DurableHistoryMarkerRangeReceipt(ArchiveHistoryWriter writer, int maxMarkers) { + public DurableHistoryMarkerRangeEvidence(ArchiveHistoryWriter writer, int maxMarkers) { this(new WriterSource(writer), maxMarkers); } - DurableHistoryMarkerRangeReceipt(Source source, int maxMarkers) { + DurableHistoryMarkerRangeEvidence(Source source, int maxMarkers) { this.source = Objects.requireNonNull(source, "source"); if (maxMarkers <= 0) { throw new IllegalArgumentException("maxMarkers must be positive"); @@ -45,7 +45,7 @@ public List read(List expectedMetas) { markers.add(marker); } - List receipt = new ArrayList<>(markers.size()); + List evidence = new ArrayList<>(markers.size()); for (int i = 0; i < markers.size(); i++) { HistoryCommitMarker marker = markers.get(i); BlockReverseDiff body = source.readCommitted(marker.getMeta().getEpoch()); @@ -56,25 +56,25 @@ public List read(List expectedMetas) { HistoryCommitMarker reloaded = source.marker(marker.getMeta().getEpoch()); validateMarker(expected.get(i), reloaded); if (!Arrays.equals(codec.encode(marker), codec.encode(reloaded))) { - throw new ArchivePersistenceException("History marker changed while building receipt"); + throw new ArchivePersistenceException("History marker changed while building evidence"); } - receipt.add(codec.decode(codec.encode(reloaded))); + evidence.add(codec.decode(codec.encode(reloaded))); } - return Collections.unmodifiableList(receipt); + return Collections.unmodifiableList(evidence); } private void validateExpectedRange(List expected) { if (expected.isEmpty()) { - throw new ArchivePersistenceException("Marker receipt range must not be empty"); + throw new ArchivePersistenceException("Marker evidence range must not be empty"); } if (expected.size() > maxMarkers) { - throw new ArchivePersistenceException("Marker receipt range exceeds configured bound"); + throw new ArchivePersistenceException("Marker evidence range exceeds configured bound"); } BlockSnapshotMeta previous = null; for (BlockSnapshotMeta meta : expected) { BlockSnapshotMeta current = Objects.requireNonNull(meta, "expectedMeta"); if (previous != null && !isNext(previous, current)) { - throw new ArchivePersistenceException("Expected marker receipt range is not contiguous"); + throw new ArchivePersistenceException("Expected marker evidence range is not contiguous"); } previous = current; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolver.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolver.java new file mode 100644 index 00000000000..9d55cf7d579 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolver.java @@ -0,0 +1,167 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Objects; +import org.tron.common.utils.ByteArray; +import org.tron.core.db2.archive.BlockChangeView.PostValue; +import org.tron.core.db2.archive.P66AccountAssetCodec.AssetRow; +import org.tron.core.db2.archive.P66AccountAssetCodec.DecodedAssetRow; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; +import org.tron.protos.Protocol.Account; + +/** Resolves one historical TRC10 balance from a single pinned archive generation. */ +public final class HistoricalAccountAssetBalanceResolver { + + static final String PROPERTIES_DATABASE = "properties"; + static final String ACCOUNT_DATABASE = "account"; + static final String ACCOUNT_ASSET_DATABASE = "account-asset"; + + private static final byte[] PROPOSAL_66_PHYSICAL_KEY = + "ALLOW_ASSET_OPTIMIZATION".getBytes(StandardCharsets.UTF_8); + + private final P66AccountAssetCodec codec = new P66AccountAssetCodec(); + + public Result resolve(ArchiveReadSnapshot snapshot, byte[] address, String tokenId) + throws IOException { + ArchiveReadSnapshot pinned = Objects.requireNonNull(snapshot, "snapshot"); + requireScopedDatabases(); + pinned.requirePinnedIdentity(); + + byte[] directKey = codec.assetPhysicalKey(address, tokenId); + OldValue propertyValue = pinned.get(PROPERTIES_DATABASE, PROPOSAL_66_PHYSICAL_KEY); + OldValue accountValue = pinned.get(ACCOUNT_DATABASE, address); + OldValue directValue = pinned.get(ACCOUNT_ASSET_DATABASE, directKey); + + pinned.requirePinnedIdentity(); + Phase phase = decodeTargetPhase(propertyValue); + if (!accountValue.isPresent()) { + if (directValue.isPresent()) { + throw new ArchivePersistenceException( + "Historical AccountAsset row has no owning Account"); + } + return Result.absent(pinned.getTargetBlock(), address, tokenId, phase); + } + + Account account = codec.decodeCanonicalAccount(phase, address, accountValue.getValue()); + if (phase == Phase.P66_OFF) { + if (directValue.isPresent()) { + throw new ArchivePersistenceException("P66-off historical layout contains a direct row"); + } + Long balance = account.getAssetV2Map().get(tokenId); + return Result.present(pinned.getTargetBlock(), address, tokenId, phase, + accountValue.getValue(), balance == null ? 0L : balance); + } + + long balance = 0L; + if (directValue.isPresent()) { + AssetRow row = new AssetRow(directKey, PostValue.present(directValue.getValue())); + codec.requireCanonicalLayout(phase, address, accountValue.getValue(), + Collections.singletonList(row)); + DecodedAssetRow decoded = codec.decodePresentAssetRow(directKey, directValue.getValue()); + if (!Arrays.equals(address, decoded.getAccountAddress()) + || !tokenId.equals(decoded.getTokenId())) { + throw new ArchivePersistenceException("Historical AccountAsset identity mismatch"); + } + balance = decoded.getBalance(); + } + return Result.present(pinned.getTargetBlock(), address, tokenId, phase, + accountValue.getValue(), balance); + } + + static byte[] proposal66PhysicalKey() { + return Arrays.copyOf(PROPOSAL_66_PHYSICAL_KEY, PROPOSAL_66_PHYSICAL_KEY.length); + } + + static Phase decodeTargetPhase(OldValue propertyValue) { + if (!propertyValue.isPresent()) { + throw new ArchivePersistenceException("Historical proposal-66 property is absent"); + } + byte[] value = propertyValue.getValue(); + if (value.length != Long.BYTES) { + throw new ArchivePersistenceException( + "Historical proposal-66 property must be exactly eight bytes"); + } + long enabled = ByteArray.toLong(value); + if (enabled != 0L && enabled != 1L) { + throw new ArchivePersistenceException("Historical proposal-66 property must be 0 or 1"); + } + return enabled == 0L ? Phase.P66_OFF : Phase.P66_ON; + } + + static void requireScopedDatabases() { + if (!ArchiveStoreScope.isStateDatabase(PROPERTIES_DATABASE) + || !ArchiveStoreScope.isStateDatabase(ACCOUNT_DATABASE) + || !ArchiveStoreScope.isStateDatabase(ACCOUNT_ASSET_DATABASE)) { + throw new IllegalStateException("Historical AccountAsset resolver Store scope mismatch"); + } + } + + public static final class Result { + private final long blockNumber; + private final byte[] address; + private final String tokenId; + private final Phase phase; + private final boolean accountPresent; + private final byte[] accountValue; + private final long balance; + + private Result(long blockNumber, byte[] address, String tokenId, Phase phase, + boolean accountPresent, byte[] accountValue, long balance) { + this.blockNumber = blockNumber; + this.address = Arrays.copyOf(address, address.length); + this.tokenId = tokenId; + this.phase = phase; + this.accountPresent = accountPresent; + this.accountValue = accountValue == null ? null + : Arrays.copyOf(accountValue, accountValue.length); + this.balance = balance; + } + + private static Result absent(long blockNumber, byte[] address, String tokenId, Phase phase) { + return new Result(blockNumber, address, tokenId, phase, false, null, 0L); + } + + private static Result present(long blockNumber, byte[] address, String tokenId, Phase phase, + byte[] accountValue, long balance) { + return new Result(blockNumber, address, tokenId, phase, true, accountValue, balance); + } + + public long getBlockNumber() { + return blockNumber; + } + + public byte[] getAddress() { + return Arrays.copyOf(address, address.length); + } + + public String getTokenId() { + return tokenId; + } + + /** P66_ON also represents the activation target because both use the direct layout. */ + public Phase getPhase() { + return phase; + } + + public boolean isAccountPresent() { + return accountPresent; + } + + public byte[] getAccountValue() { + if (!accountPresent) { + throw new IllegalStateException("Historical account is absent"); + } + return Arrays.copyOf(accountValue, accountValue.length); + } + + public long getBalance() { + if (!accountPresent) { + throw new IllegalStateException("Historical account is absent"); + } + return balance; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetPrefixResolver.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetPrefixResolver.java new file mode 100644 index 00000000000..5b1bbe0b811 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetPrefixResolver.java @@ -0,0 +1,230 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.tron.core.db2.archive.HistoricalRangeOverlay.KeyRange; +import org.tron.core.db2.archive.P66AccountAssetCodec.DecodedAssetRow; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; +import org.tron.protos.Protocol.Account; + +/** Bounded historical AccountAsset address-prefix resolver over one pinned generation. */ +public final class HistoricalAccountAssetPrefixResolver { + + private final P66AccountAssetCodec codec = new P66AccountAssetCodec(); + + public Result resolve(ArchiveReadSnapshot snapshot, byte[] address, Limits limits) + throws IOException { + ArchiveReadSnapshot pinned = Objects.requireNonNull(snapshot, "snapshot"); + Limits budgets = Objects.requireNonNull(limits, "limits"); + byte[] accountAddress = requireAddress(address); + HistoricalAccountAssetBalanceResolver.requireScopedDatabases(); + pinned.requirePinnedIdentity(); + + OldValue propertyValue = pinned.get( + HistoricalAccountAssetBalanceResolver.PROPERTIES_DATABASE, + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey()); + OldValue accountValue = pinned.get( + HistoricalAccountAssetBalanceResolver.ACCOUNT_DATABASE, accountAddress); + List directRows = pinned.range( + HistoricalAccountAssetBalanceResolver.ACCOUNT_ASSET_DATABASE, + KeyRange.prefix(accountAddress), budgets.overlayLimits()); + + pinned.requirePinnedIdentity(); + Phase phase = HistoricalAccountAssetBalanceResolver.decodeTargetPhase(propertyValue); + if (!accountValue.isPresent()) { + if (!directRows.isEmpty()) { + throw new ArchivePersistenceException( + "Historical AccountAsset prefix has no owning Account"); + } + return Result.absent(pinned.getTargetBlock(), accountAddress, phase); + } + + byte[] exactAccount = accountValue.getValue(); + Account account = codec.decodeCanonicalAccount(phase, accountAddress, exactAccount); + List balances = phase == Phase.P66_OFF + ? resolveEmbedded(accountAddress, account.getAssetV2Map(), directRows, budgets) + : resolveDirect(accountAddress, directRows, budgets); + return Result.present(pinned.getTargetBlock(), accountAddress, phase, exactAccount, balances); + } + + private List resolveEmbedded(byte[] address, Map embedded, + List directRows, Limits limits) { + if (!directRows.isEmpty()) { + throw new ArchivePersistenceException("P66-off historical layout contains direct rows"); + } + List> sorted = new ArrayList<>(embedded.entrySet()); + sorted.sort(Comparator.comparing(Map.Entry::getKey)); + List result = new ArrayList<>(); + long totalBytes = 0L; + for (Map.Entry entry : sorted) { + String tokenId = Objects.requireNonNull(entry.getKey(), "embedded tokenId"); + Long balance = Objects.requireNonNull(entry.getValue(), "embedded balance"); + byte[] physicalKey = codec.assetPhysicalKey(address, tokenId); + totalBytes = limits.reserve(result.size(), physicalKey.length, Long.BYTES, totalBytes); + result.add(new Balance(tokenId, balance)); + } + return Collections.unmodifiableList(result); + } + + private List resolveDirect(byte[] address, + List directRows, Limits limits) { + List result = new ArrayList<>(); + byte[] previous = null; + long totalBytes = 0L; + for (HistoricalRangeOverlay.Entry row : directRows) { + byte[] key = row.getKey(); + byte[] value = row.getValue(); + if (previous != null && BlockReverseDiff.compareUnsigned(previous, key) >= 0) { + throw new ArchivePersistenceException( + "Historical AccountAsset prefix rows must be strictly sorted and unique"); + } + totalBytes = limits.reserve(result.size(), key.length, value.length, totalBytes); + DecodedAssetRow decoded = codec.decodePresentAssetRow(key, value); + if (!Arrays.equals(address, decoded.getAccountAddress())) { + throw new ArchivePersistenceException("Historical AccountAsset prefix escaped address"); + } + result.add(new Balance(decoded.getTokenId(), decoded.getBalance())); + previous = key; + } + return Collections.unmodifiableList(result); + } + + private static byte[] requireAddress(byte[] address) { + if (address == null || address.length != HistoricalAccountBalanceReader.ADDRESS_LENGTH) { + throw new ArchivePersistenceException("Account address must be exactly 21 bytes"); + } + return Arrays.copyOf(address, address.length); + } + + public static final class Limits { + private final int maxChangedKeys; + private final int maxCandidateKeys; + private final int maxEntries; + private final int maxKeyBytes; + private final int maxValueBytes; + private final long maxTotalBytes; + + public Limits(int maxChangedKeys, int maxCandidateKeys, int maxEntries, int maxKeyBytes, + int maxValueBytes, long maxTotalBytes) { + if (maxChangedKeys <= 0 || maxCandidateKeys <= 0 || maxEntries <= 0 || maxKeyBytes <= 0 + || maxValueBytes <= 0 || maxTotalBytes <= 0) { + throw new IllegalArgumentException("AccountAsset prefix limits must be positive"); + } + this.maxChangedKeys = maxChangedKeys; + this.maxCandidateKeys = maxCandidateKeys; + this.maxEntries = maxEntries; + this.maxKeyBytes = maxKeyBytes; + this.maxValueBytes = maxValueBytes; + this.maxTotalBytes = maxTotalBytes; + } + + private HistoricalRangeOverlay.Limits overlayLimits() { + return new HistoricalRangeOverlay.Limits(maxChangedKeys, maxCandidateKeys, maxEntries); + } + + private long reserve(int currentEntries, int keyBytes, int valueBytes, long currentTotal) { + if (currentEntries >= maxEntries) { + throw new ArchiveQueryLimitExceededException("AccountAsset entry budget exceeded"); + } + if (keyBytes > maxKeyBytes) { + throw new ArchiveQueryLimitExceededException("AccountAsset key-byte budget exceeded"); + } + if (valueBytes > maxValueBytes) { + throw new ArchiveQueryLimitExceededException("AccountAsset value-byte budget exceeded"); + } + final long entryBytes; + final long updated; + try { + entryBytes = Math.addExact((long) keyBytes, valueBytes); + updated = Math.addExact(currentTotal, entryBytes); + } catch (ArithmeticException overflow) { + throw new ArchiveQueryLimitExceededException( + "AccountAsset total-byte budget overflow"); + } + if (updated > maxTotalBytes) { + throw new ArchiveQueryLimitExceededException("AccountAsset total-byte budget exceeded"); + } + return updated; + } + } + + public static final class Balance { + private final String tokenId; + private final long balance; + + private Balance(String tokenId, long balance) { + this.tokenId = tokenId; + this.balance = balance; + } + + public String getTokenId() { + return tokenId; + } + + public long getBalance() { + return balance; + } + } + + public static final class Result { + private final long blockNumber; + private final byte[] address; + private final Phase phase; + private final boolean accountPresent; + private final byte[] accountValue; + private final List balances; + + private Result(long blockNumber, byte[] address, Phase phase, boolean accountPresent, + byte[] accountValue, List balances) { + this.blockNumber = blockNumber; + this.address = Arrays.copyOf(address, address.length); + this.phase = phase; + this.accountPresent = accountPresent; + this.accountValue = accountValue == null ? null + : Arrays.copyOf(accountValue, accountValue.length); + this.balances = balances; + } + + private static Result absent(long blockNumber, byte[] address, Phase phase) { + return new Result(blockNumber, address, phase, false, null, Collections.emptyList()); + } + + private static Result present(long blockNumber, byte[] address, Phase phase, + byte[] accountValue, List balances) { + return new Result(blockNumber, address, phase, true, accountValue, balances); + } + + public long getBlockNumber() { + return blockNumber; + } + + public byte[] getAddress() { + return Arrays.copyOf(address, address.length); + } + + public Phase getPhase() { + return phase; + } + + public boolean isAccountPresent() { + return accountPresent; + } + + public byte[] getAccountValue() { + if (!accountPresent) { + throw new IllegalStateException("Historical account is absent"); + } + return Arrays.copyOf(accountValue, accountValue.length); + } + + public List getBalances() { + return balances; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java index daf419edb07..c931ed36a84 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java @@ -17,7 +17,7 @@ * same encoded length. Contiguous epochs can be addressed directly without retaining one object * or creating one directory entry per block. */ -public final class HistoryCommitStore implements Closeable { +public final class HistoryCommitStore implements Closeable, CommittedHistoryAuthority { private static final String FILE_NAME = "commit.log"; @@ -156,6 +156,7 @@ public synchronized void truncateAfter(long lastEpoch) throws IOException { channel.position(channel.size()); } + @Override public synchronized HistoryCommitMarker head() { return head; } @@ -164,10 +165,18 @@ public synchronized long size() { return recordCount; } + @Override public synchronized long firstEpoch() { return firstEpoch; } + /** Returns one atomic height-coverage snapshot of the validated contiguous commit log. */ + @Override + public synchronized HistoryCoverage coverage() { + return head == null ? null : new HistoryCoverage(firstEpoch, recordCount, + head.getMeta().getEpoch(), head.getMeta().getBlockHash()); + } + /** Materializes the committed prefix. Do not use this method in the scale ingestion path. */ public synchronized List getMarkers() { if (recordCount > Integer.MAX_VALUE) { @@ -184,6 +193,7 @@ public synchronized List getMarkers() { } } + @Override public synchronized HistoryCommitMarker get(long epoch) { if (recordCount == 0 || epoch < firstEpoch || epoch - firstEpoch >= recordCount) { return null; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCoverage.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCoverage.java new file mode 100644 index 00000000000..659b6c0b636 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCoverage.java @@ -0,0 +1,41 @@ +package org.tron.core.db2.archive; + +import java.util.Arrays; + +/** Immutable height coverage of one validated, contiguous history commit log. */ +public final class HistoryCoverage { + + private final long firstEpoch; + private final long recordCount; + private final long headEpoch; + private final byte[] headHash; + + public HistoryCoverage(long firstEpoch, long recordCount, long headEpoch, byte[] headHash) { + if (firstEpoch < 0 || recordCount <= 0 || headEpoch < firstEpoch) { + throw new IllegalArgumentException("History coverage range is invalid"); + } + if (headHash == null || headHash.length != 32) { + throw new IllegalArgumentException("History coverage head hash must be exactly 32 bytes"); + } + this.firstEpoch = firstEpoch; + this.recordCount = recordCount; + this.headEpoch = headEpoch; + this.headHash = Arrays.copyOf(headHash, headHash.length); + } + + public long getFirstEpoch() { + return firstEpoch; + } + + public long getRecordCount() { + return recordCount; + } + + public long getHeadEpoch() { + return headEpoch; + } + + public byte[] getHeadHash() { + return Arrays.copyOf(headHash, headHash.length); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java index c424a1273e8..c1da86adc6e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java @@ -58,15 +58,38 @@ public AssetRow encodeAssetRow(Phase phase, byte[] accountAddress, String tokenI if (!phase.directAssetsEnabled()) { throw new ArchivePersistenceException("P66-off state must not contain direct asset rows"); } + byte[] key = assetPhysicalKey(accountAddress, tokenId); + PostValue value = balance == 0 ? PostValue.absent() + : PostValue.present(ByteBuffer.allocate(BALANCE_LENGTH).putLong(balance).array()); + return new AssetRow(key, value); + } + + /** Builds the exact direct-row physical key while rejecting non-canonical token identities. */ + public byte[] assetPhysicalKey(byte[] accountAddress, String tokenId) { byte[] address = requireAddress(accountAddress); byte[] token = requireTokenId(tokenId); - byte[] key = ByteBuffer.allocate(address.length + token.length) + return ByteBuffer.allocate(address.length + token.length) .put(address) .put(token) .array(); - PostValue value = balance == 0 ? PostValue.absent() - : PostValue.present(ByteBuffer.allocate(BALANCE_LENGTH).putLong(balance).array()); - return new AssetRow(key, value); + } + + /** Decodes an Account and verifies that it is canonical for the target P66 phase. */ + public Account decodeCanonicalAccount(Phase phase, byte[] physicalAccountKey, + byte[] canonicalAccountValue) { + Objects.requireNonNull(phase, "phase"); + byte[] accountKey = requireAddress(physicalAccountKey); + Account account = parseAccount(canonicalAccountValue); + requireAccountAddress(accountKey, account); + if (phase.directAssetsEnabled()) { + if (!account.getAssetOptimized() || !account.getAssetMap().isEmpty() + || !account.getAssetV2Map().isEmpty()) { + throw new ArchivePersistenceException("P66-on durable Account layout is mixed"); + } + } else if (account.getAssetOptimized()) { + throw new ArchivePersistenceException("P66-off durable Account layout is mixed"); + } + return account; } /** Decodes a PRESENT direct row and rejects the non-canonical stored zero representation. */ @@ -89,22 +112,17 @@ public void requireCanonicalLayout(Phase phase, byte[] physicalAccountKey, byte[] canonicalAccountValue, List directRows) { Objects.requireNonNull(phase, "phase"); byte[] accountKey = requireAddress(physicalAccountKey); - Account account = parseAccount(canonicalAccountValue); - requireAccountAddress(accountKey, account); + decodeCanonicalAccount(phase, accountKey, canonicalAccountValue); List rows = new ArrayList<>(Objects.requireNonNull(directRows, "directRows")); if (rows.contains(null)) { throw new ArchivePersistenceException("Canonical AccountAsset rows contain null"); } if (!phase.directAssetsEnabled()) { - if (account.getAssetOptimized() || !rows.isEmpty()) { + if (!rows.isEmpty()) { throw new ArchivePersistenceException("P66-off durable layout is mixed"); } return; } - if (!account.getAssetOptimized() || !account.getAssetMap().isEmpty() - || !account.getAssetV2Map().isEmpty()) { - throw new ArchivePersistenceException("P66-on durable Account layout is mixed"); - } byte[] previous = null; for (AssetRow row : rows) { byte[] key = row.getPhysicalRawKey(); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java new file mode 100644 index 00000000000..036d655f7b0 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java @@ -0,0 +1,262 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; +import org.tron.core.db2.core.SnapshotManager; + +/** Sole owner for exact-27 State Archive resources from recovered startup through shutdown. */ +public final class StateArchiveRuntimeOwner implements Closeable { + + public enum State { + RECOVERED, + RUNNING, + QUIESCING, + CLOSED, + FAILED_CLOSED + } + + private final SnapshotManager snapshotManager; + private final ArchiveRuntimeAttachment attachment; + private final ArchiveRuntimeQueryGate queryGate; + private final Closeable latestCoordinator; + private final Closeable servingCatalog; + private final List participants; + private final Closeable sink; + private final BlockSnapshotMeta recoveredHead; + private final int startupRecoveryActionCount; + private State state; + private boolean detached; + private IOException terminalFailure; + + public StateArchiveRuntimeOwner(SnapshotManager snapshotManager, + ArchiveRuntimeAttachment attachment, ArchiveRuntimeQueryGate queryGate, + Closeable latestCoordinator, Closeable servingCatalog, + List participants) { + this.snapshotManager = Objects.requireNonNull(snapshotManager, "snapshotManager"); + this.attachment = Objects.requireNonNull(attachment, "attachment"); + this.queryGate = Objects.requireNonNull(queryGate, "queryGate"); + this.latestCoordinator = Objects.requireNonNull(latestCoordinator, "latestCoordinator"); + this.servingCatalog = Objects.requireNonNull(servingCatalog, "servingCatalog"); + if (!(attachment.getSink() instanceof Closeable)) { + throw new IllegalArgumentException("Attached archive sink must be Closeable"); + } + this.sink = (Closeable) attachment.getSink(); + this.participants = immutableParticipants(participants); + this.recoveredHead = null; + this.startupRecoveryActionCount = 0; + this.state = State.RUNNING; + validateUniqueOwnership(); + } + + private StateArchiveRuntimeOwner(SnapshotManager snapshotManager, + List participants, BlockSnapshotMeta recoveredHead, + int startupRecoveryActionCount) { + this.snapshotManager = Objects.requireNonNull(snapshotManager, "snapshotManager"); + this.attachment = null; + this.queryGate = null; + this.latestCoordinator = null; + this.servingCatalog = null; + this.participants = immutableParticipants(participants); + this.sink = null; + this.recoveredHead = Objects.requireNonNull(recoveredHead, "recoveredHead"); + this.startupRecoveryActionCount = startupRecoveryActionCount; + this.state = State.RECOVERED; + } + + /** + * Opens the canonical exact-27 native participants and converges startup recovery before any + * normal archive producer is attached to {@link SnapshotManager}. + */ + public static StateArchiveRuntimeOwner recover(SnapshotManager snapshotManager, + Path archiveDirectory, long maxSegmentSize, String databaseEngine) throws IOException { + Objects.requireNonNull(snapshotManager, "snapshotManager"); + Path root = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + String engine = Objects.requireNonNull(databaseEngine, "databaseEngine") + .toUpperCase(Locale.ROOT); + if (!"LEVELDB".equals(engine) && !"ROCKSDB".equals(engine)) { + throw new IllegalArgumentException("Unsupported State Archive database engine: " + engine); + } + List names = ArchiveParticipantDescriptor.current().getParticipants(); + Map openedByName = new LinkedHashMap<>(); + List opened = new ArrayList<>(); + try { + for (String participant : names) { + Closeable nativeEngine = openParticipant(root.resolve("participants").resolve(participant), + participant, names, engine); + opened.add(nativeEngine); + openedByName.put(participant, (ArchiveParticipant) nativeEngine); + } + Path checkpoint = root.resolve("progress").resolve("checkpoint.progress"); + Path reader = root.resolve("progress").resolve("reader.progress"); + RecoveryPlan first; + HistoryCommitMarker head; + try (ArchiveParticipantRecoveryStorage recovery = + new ArchiveParticipantRecoveryStorage(root, maxSegmentSize, checkpoint, + openedByName, reader, names)) { + first = new ArchiveRecoveryExecutor(recovery).recover(); + RecoveryPlan fixed = new ArchiveRecoveryExecutor(recovery).recover(); + if (!fixed.getActions().isEmpty()) { + throw new ArchivePersistenceException( + "State Archive second startup recovery was not zero-action"); + } + head = recovery.committedHead(); + } + if (head == null) { + throw new ArchivePersistenceException("State Archive recovered H head is missing"); + } + return new StateArchiveRuntimeOwner(snapshotManager, opened, head.getMeta(), + first.getActions().size()); + } catch (IOException | RuntimeException failure) { + closeReverse(opened, failure); + throw failure; + } + } + + public synchronized State getState() { + return state; + } + + public BlockSnapshotMeta getRecoveredHead() { + if (recoveredHead == null) { + throw new IllegalStateException("State Archive runtime has no startup recovery head"); + } + return recoveredHead; + } + + public int getStartupRecoveryActionCount() { + return startupRecoveryActionCount; + } + + /** Quiesces, detaches and closes owned resources without waiting for active query leases. */ + @Override + public synchronized void close() throws IOException { + if (state == State.CLOSED) { + return; + } + if (state == State.FAILED_CLOSED) { + throw terminalFailure; + } + if (state == State.RECOVERED) { + IOException failure = closeParticipants(); + if (failure == null) { + state = State.CLOSED; + return; + } + terminalFailure = failure; + state = State.FAILED_CLOSED; + throw failure; + } + state = State.QUIESCING; + queryGate.quiesce(); + if (!detached) { + ArchiveRuntimeAttachment returned = snapshotManager.detachArchiveRuntime(attachment); + if (returned != attachment) { + throw new IllegalStateException("SnapshotManager returned a foreign archive attachment"); + } + detached = true; + } + if (!queryGate.isDrained()) { + throw new IllegalStateException( + "State Archive runtime still has active query leases: " + + queryGate.getActiveLeaseCount()); + } + queryGate.close(); + + IOException failure = null; + failure = closeOwned("latest coordinator", latestCoordinator, failure); + failure = closeOwned("serving catalog", servingCatalog, failure); + for (int i = participants.size() - 1; i >= 0; i--) { + failure = closeOwned("archive participant " + i, participants.get(i), failure); + } + failure = closeOwned("archive history sink", sink, failure); + if (failure == null) { + state = State.CLOSED; + return; + } + terminalFailure = failure; + state = State.FAILED_CLOSED; + throw failure; + } + + private IOException closeParticipants() { + IOException failure = null; + for (int i = participants.size() - 1; i >= 0; i--) { + failure = closeOwned("archive participant " + i, participants.get(i), failure); + } + return failure; + } + + private static Closeable openParticipant(Path directory, String participant, + List participants, String engine) throws IOException { + if ("ROCKSDB".equals(engine)) { + return new RocksDbArchiveParticipant(directory, participant, participants); + } + return new LevelDbArchiveParticipant(directory, participant, participants); + } + + private static void closeReverse(List resources, Exception failure) { + for (int i = resources.size() - 1; i >= 0; i--) { + try { + resources.get(i).close(); + } catch (IOException | RuntimeException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + } + + private static List immutableParticipants( + List participants) { + List source = Objects.requireNonNull(participants, "participants"); + List copy = new ArrayList<>(source.size()); + for (Closeable participant : source) { + copy.add(Objects.requireNonNull(participant, "participant")); + } + return Collections.unmodifiableList(copy); + } + + private void validateUniqueOwnership() { + Set unique = Collections.newSetFromMap(new IdentityHashMap()); + requireUnique(unique, latestCoordinator, "latestCoordinator"); + requireUnique(unique, servingCatalog, "servingCatalog"); + for (int i = 0; i < participants.size(); i++) { + requireUnique(unique, participants.get(i), "participant[" + i + "]"); + } + requireUnique(unique, sink, "sink"); + } + + private static void requireUnique(Set unique, Closeable resource, String name) { + if (!unique.add(resource)) { + throw new IllegalArgumentException("Archive runtime resource has multiple owners: " + name); + } + } + + private static IOException closeOwned(String name, Closeable resource, IOException current) { + try { + resource.close(); + return current; + } catch (IOException failure) { + return append(current, failure); + } catch (RuntimeException failure) { + return append(current, new IOException("Failed to close " + name, failure)); + } + } + + private static IOException append(IOException current, IOException failure) { + if (current == null) { + return failure; + } + current.addSuppressed(failure); + return current; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index 98a56825f2c..8c605405ffa 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -42,6 +42,7 @@ import org.tron.core.db2.archive.ArchiveBlockForwardPayload; import org.tron.core.db2.archive.ArchiveBlockProjectionPreparer; import org.tron.core.db2.archive.ArchivePersistenceException; +import org.tron.core.db2.archive.ArchiveRuntimeAttachment; import org.tron.core.db2.archive.ArchiveStateBarrier.ArchiveStateAction; import org.tron.core.db2.archive.ArchiveStoreScope; import org.tron.core.db2.archive.BlockChangeView; @@ -49,7 +50,7 @@ import org.tron.core.db2.archive.BlockReverseDiffSink; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.archive.DurableBlockReverseDiffSink; -import org.tron.core.db2.archive.DurableHistoryMarkerRangeReceipt; +import org.tron.core.db2.archive.DurableHistoryMarkerRangeEvidence; import org.tron.core.db2.archive.HistoryCommitMarker; import org.tron.core.db2.archive.OldValueCollector; import org.tron.core.db2.common.DB; @@ -104,6 +105,7 @@ public class SnapshotManager implements RevokingDatabase { private OldValueCollector oldValueCollector; private ArchiveBlockProjectionPreparer archiveBlockProjectionPreparer; + private ArchiveRuntimeAttachment archiveRuntimeAttachment; private final Map archiveForwardPayloadOwners = new HashMap<>(); private FrozenBatch pendingArchiveForwardFlush; @@ -350,6 +352,9 @@ private void validateBlockMeta(BlockSnapshotMeta meta) { public synchronized void installArchiveCollector(OldValueCollector collector, BlockReverseDiffSink sink) { ArchiveStoreScope.validate(dbs); + if (archiveRuntimeAttachment != null) { + throw new IllegalStateException("Borrowed archive runtime is already attached"); + } oldValueCollector = Objects.requireNonNull(collector, "collector"); blockReverseDiffSink = Objects.requireNonNull(sink, "sink"); } @@ -358,12 +363,51 @@ public synchronized void installArchiveCollector(OldValueCollector collector, public synchronized void installArchiveProjectionPreparer( ArchiveBlockProjectionPreparer preparer) { ArchiveStoreScope.validate(dbs); + if (archiveRuntimeAttachment != null) { + throw new IllegalStateException("Borrowed archive runtime is already attached"); + } if (oldValueCollector == null || blockReverseDiffSink == null) { throw new IllegalStateException("Archive collector must be installed before its preparer"); } archiveBlockProjectionPreparer = Objects.requireNonNull(preparer, "preparer"); } + /** Atomically installs one borrowed archive runtime bundle after store registration. */ + public synchronized void attachArchiveRuntime(ArchiveRuntimeAttachment attachment) { + ArchiveStoreScope.validate(dbs); + ArchiveRuntimeAttachment candidate = Objects.requireNonNull(attachment, "attachment"); + if (archiveRuntimeAttachment != null) { + throw new IllegalStateException("Archive runtime is already attached"); + } + if (oldValueCollector != null || archiveBlockProjectionPreparer != null + || blockReverseDiffSink != null) { + throw new IllegalStateException("Legacy archive collaborators are already installed"); + } + oldValueCollector = candidate.getCollector(); + archiveBlockProjectionPreparer = candidate.getProjectionPreparer(); + blockReverseDiffSink = candidate.getSink(); + archiveRuntimeAttachment = candidate; + } + + /** Detaches the exact borrowed bundle without closing resources owned by its runtime. */ + public synchronized ArchiveRuntimeAttachment detachArchiveRuntime( + ArchiveRuntimeAttachment expected) { + ArchiveRuntimeAttachment candidate = Objects.requireNonNull(expected, "expected"); + if (archiveRuntimeAttachment == null) { + throw new IllegalStateException("Archive runtime is not attached"); + } + if (archiveRuntimeAttachment != candidate) { + throw new IllegalStateException("Cannot detach a foreign archive runtime"); + } + abortArchiveForwardPayloads(); + archiveRuntimeAttachment = null; + oldValueCollector = null; + archiveBlockProjectionPreparer = null; + blockReverseDiffSink = null; + archiveReadableEpoch = -1; + return candidate; + } + /** Visible for lifecycle verification until the flush freeze coordinator consumes this registry. */ public synchronized int getArchiveForwardPayloadOwnerCount() { return archiveForwardPayloadOwners.size(); @@ -442,11 +486,11 @@ public synchronized void sealPendingArchiveForwardFlush(List sealed = Objects.requireNonNull(receipt, "receipt") + List sealed = Objects.requireNonNull(evidence, "evidence") .seal(pending); sealedArchiveForwardFlush = sealed; pendingArchiveForwardFlush = null; @@ -609,19 +653,32 @@ public synchronized void disable() { @Override public void shutdown() { - abortArchiveForwardPayloads(); + Closeable legacyArchiveSink = prepareArchiveShutdown(); ExecutorServiceManager.shutdownAndAwaitTermination(pruneCheckpointThread, pruneName); flushServices.forEach((key, value) -> ExecutorServiceManager.shutdownAndAwaitTermination(value, "flush-service-" + key)); - if (blockReverseDiffSink instanceof Closeable) { + if (legacyArchiveSink != null) { try { - ((Closeable) blockReverseDiffSink).close(); + legacyArchiveSink.close(); } catch (IOException e) { logger.error("Failed to close archive history sink.", e); } } } + private synchronized Closeable prepareArchiveShutdown() { + abortArchiveForwardPayloads(); + if (archiveRuntimeAttachment != null) { + archiveRuntimeAttachment = null; + oldValueCollector = null; + archiveBlockProjectionPreparer = null; + blockReverseDiffSink = null; + archiveReadableEpoch = -1; + return null; + } + return blockReverseDiffSink instanceof Closeable ? (Closeable) blockReverseDiffSink : null; + } + private synchronized void abortArchiveForwardPayloads() { if (pendingArchiveForwardFlush != null) { pendingArchiveForwardFlush.abortIfFrozen(); @@ -803,9 +860,9 @@ private Long publishArchiveHistoryForFlush() { } durableSink.awaitCommitted(last.getEpoch()); if (frozenForward != null) { - DurableHistoryMarkerRangeReceipt receipt = - durableSink.createMarkerRangeReceipt(prepared.size()); - sealPendingArchiveForwardFlush(receipt); + DurableHistoryMarkerRangeEvidence evidence = + durableSink.createMarkerRangeEvidence(prepared.size()); + sealPendingArchiveForwardFlush(evidence); submittedArchiveForwardHistoryEpoch = null; } } catch (RuntimeException e) { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 916ecfffa36..ff5339aa1f2 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -115,13 +115,10 @@ import org.tron.core.db.api.MigrateTurkishKeyHelper; import org.tron.core.db.api.MoveAbiHelper; import org.tron.core.db2.ISession; -import org.tron.core.db2.archive.AccountAssetArchiveProjector; import org.tron.core.db2.archive.ArchiveHistoryWriter; -import org.tron.core.db2.archive.ArchiveStoreScope; -import org.tron.core.db2.archive.AsyncArchiveHistorySink; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.archive.HistoricalAccountBalanceReader; -import org.tron.core.db2.archive.SnapshotOldValueCollector; +import org.tron.core.db2.archive.StateArchiveRuntimeOwner; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.exception.AccountResourceInsufficientException; @@ -198,6 +195,8 @@ public class Manager { private static final int SLEEP_FOR_WAIT_LOCK = 10; @Getter private ArchiveHistoryWriter archiveHistoryWriter; + @Getter + private StateArchiveRuntimeOwner stateArchiveRuntime; private static final int NO_BLOCK_WAITING_LOCK = 0; private final int shieldedTransInPendingMaxCounts = Args.getInstance().getShieldedTransInPendingMaxCounts(); @@ -621,43 +620,43 @@ public void init() { private void initStateArchive() { org.tron.core.config.args.Storage storage = Args.getInstance().getStorage(); + Path archiveDirectory = Paths.get(Args.getInstance().getOutputDirectory(), + storage.getStateArchiveDirectory()).normalize(); + StateArchiveBasePreflight.requireRecoverable(storage.isStateArchiveEnabled(), + archiveDirectory); if (!storage.isStateArchiveEnabled()) { return; } if (!(revokingStore instanceof SnapshotManager)) { throw new IllegalStateException("State archive requires SnapshotManager"); } - Path archiveDirectory = Paths.get(Args.getInstance().getOutputDirectory(), - storage.getStateArchiveDirectory()).normalize(); + StateArchiveRuntimeOwner recovered = null; try { - ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archiveDirectory, - storage.getStateArchiveMaxSegmentSize(), ArchiveStoreScope.getStateDatabases()); - BlockSnapshotMeta archiveHead = writer.committedHeadMeta(); + recovered = StateArchiveRuntimeOwner.recover((SnapshotManager) revokingStore, + archiveDirectory, storage.getStateArchiveMaxSegmentSize(), storage.getDbEngine()); + BlockSnapshotMeta archiveHead = recovered.getRecoveredHead(); if (archiveHead != null && (archiveHead.getBlockNumber() != getDynamicPropertiesStore().getLatestBlockHeaderNumber() || !Arrays.equals(archiveHead.getBlockHash(), getDynamicPropertiesStore().getLatestBlockHeaderHash().getBytes()))) { - writer.close(); throw new IllegalStateException( "State archive committed head differs from the persisted state root"); } - AsyncArchiveHistorySink sink = new AsyncArchiveHistorySink(writer, - storage.getStateArchiveQueueCapacity()); - AccountAssetArchiveProjector projector = new AccountAssetArchiveProjector(); - ((SnapshotManager) revokingStore).installArchiveCollector( - new SnapshotOldValueCollector(projector, - accountKey -> chainBaseManager.getAccountAssetStore().prefixQuery(accountKey), - () -> getDynamicPropertiesStore().supportAllowAccountAssetOptimization()), sink); - archiveHistoryWriter = writer; - if (archiveHead != null) { - ((SnapshotManager) revokingStore).markArchiveReadableThrough(archiveHead.getEpoch()); - } - logger.info("Experimental state archive enabled: directory={}, maxSegmentSize={}, queue={}", - archiveDirectory, storage.getStateArchiveMaxSegmentSize(), - storage.getStateArchiveQueueCapacity()); - } catch (java.io.IOException failure) { - throw new IllegalStateException("Failed to initialize experimental state archive", failure); + stateArchiveRuntime = recovered; + recovered = null; + logger.info("State archive startup recovered: directory={}, head={}, actions={}, engine={}", + archiveDirectory, archiveHead.getBlockNumber(), + stateArchiveRuntime.getStartupRecoveryActionCount(), storage.getDbEngine()); + } catch (java.io.IOException | RuntimeException failure) { + if (recovered != null) { + try { + recovered.close(); + } catch (java.io.IOException | RuntimeException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + throw new IllegalStateException("Failed to recover State Archive startup", failure); } } @@ -2746,11 +2745,25 @@ public void close() { stopFilterProcessThread(); stopValidateSignThread(); rewardViCalService.stop(); + closeStateArchive(); chainBaseManager.shutdown(); revokingStore.shutdown(); session.reset(); } + private void closeStateArchive() { + StateArchiveRuntimeOwner runtime = stateArchiveRuntime; + if (runtime == null) { + return; + } + try { + runtime.close(); + stateArchiveRuntime = null; + } catch (java.io.IOException failure) { + throw new IllegalStateException("Failed to close State Archive runtime", failure); + } + } + private static class ValidateSignTask implements Callable { private TransactionCapsule trx; diff --git a/framework/src/main/java/org/tron/core/db/StateArchiveBasePreflight.java b/framework/src/main/java/org/tron/core/db/StateArchiveBasePreflight.java new file mode 100644 index 00000000000..de2fce12b93 --- /dev/null +++ b/framework/src/main/java/org/tron/core/db/StateArchiveBasePreflight.java @@ -0,0 +1,45 @@ +package org.tron.core.db; + +import java.nio.file.Path; +import java.util.Objects; +import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator; +import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator.Result; +import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator.Status; + +/** Read-only base-format gate that must run before the archive writer opens. */ +final class StateArchiveBasePreflight { + + private StateArchiveBasePreflight() { + } + + static void requireAdmitted(boolean enabled, Path archiveDirectory) { + if (!enabled) { + return; + } + Result result = ArchiveFormatAdmissionValidator.inspect( + Objects.requireNonNull(archiveDirectory, "archiveDirectory")); + if (result.getStatus() == Status.EMPTY_NEW || result.getStatus() == Status.CURRENT_BASE) { + return; + } + throw new IllegalStateException("State archive base requires quarantine: " + + result.getReason() + ": " + result.getDetail()); + } + + /** S1 can recover an existing exact-27 base, but fresh-base bootstrap is not wired yet. */ + static void requireRecoverable(boolean enabled, Path archiveDirectory) { + if (!enabled) { + return; + } + Result result = ArchiveFormatAdmissionValidator.inspect( + Objects.requireNonNull(archiveDirectory, "archiveDirectory")); + if (result.getStatus() == Status.CURRENT_BASE) { + return; + } + if (result.getStatus() == Status.EMPTY_NEW) { + throw new IllegalStateException( + "State archive fresh-base bootstrap is not available in S1 startup recovery"); + } + throw new IllegalStateException("State archive base requires quarantine: " + + result.getReason() + ": " + result.getDetail()); + } +} diff --git a/framework/src/test/java/org/tron/core/db/StateArchiveBasePreflightTest.java b/framework/src/test/java/org/tron/core/db/StateArchiveBasePreflightTest.java new file mode 100644 index 00000000000..d631bb54ece --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/StateArchiveBasePreflightTest.java @@ -0,0 +1,150 @@ +package org.tron.core.db; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveHistoryWriter; +import org.tron.core.db2.archive.ArchiveStoreScope; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +public class StateArchiveBasePreflightTest { + + private static final int MANIFEST_MAGIC = 0x54414d46; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void disabledControlDoesNotInspectOrCreateArchivePath() throws Exception { + Path missingManifest = temporaryFolder.newFolder("disabled").toPath(); + byte[] evidence = new byte[]{1, 2, 3}; + Files.write(missingManifest.resolve("history.bin"), evidence); + + StateArchiveBasePreflight.requireAdmitted(false, missingManifest); + + assertArrayEquals(evidence, Files.readAllBytes(missingManifest.resolve("history.bin"))); + assertFalse(Files.exists(missingManifest.resolve("MANIFEST"))); + } + + @Test + public void absentAndEmptyArchivesPassWithoutPreflightWrites() throws Exception { + Path absent = temporaryFolder.getRoot().toPath().resolve("absent"); + StateArchiveBasePreflight.requireAdmitted(true, absent); + assertFalse(Files.exists(absent)); + + Path empty = temporaryFolder.newFolder("empty").toPath(); + StateArchiveBasePreflight.requireAdmitted(true, empty); + try (java.util.stream.Stream entries = Files.list(empty)) { + assertFalse(entries.findAny().isPresent()); + } + } + + @Test + public void recoverableGateRejectsFreshBaseWithoutCreatingBootstrapArtifacts() throws Exception { + Path absent = temporaryFolder.getRoot().toPath().resolve("fresh-recovery"); + + assertThrows(IllegalStateException.class, + () -> StateArchiveBasePreflight.requireRecoverable(true, absent)); + + assertFalse(Files.exists(absent)); + } + + @Test + public void currentManifestPassesWithoutRewrite() throws Exception { + Path archive = temporaryFolder.newFolder("current").toPath(); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, + 128L * 1024 * 1024, ArchiveStoreScope.getStateDatabases())) { + writer.accept(new BlockReverseDiff( + new BlockSnapshotMeta(1, 1, hash(1), hash(0), 1_000L), Collections.emptyList())); + } + byte[] before = Files.readAllBytes(archive.resolve("MANIFEST")); + + StateArchiveBasePreflight.requireAdmitted(true, archive); + + assertArrayEquals(before, Files.readAllBytes(archive.resolve("MANIFEST"))); + } + + @Test + public void missingManifestFailsWithoutCreatingOrChangingEvidence() throws Exception { + Path archive = temporaryFolder.newFolder("missing-manifest").toPath(); + byte[] evidence = new byte[]{4, 5, 6}; + Path history = archive.resolve("commit.log"); + Files.write(history, evidence); + + assertThrows(IllegalStateException.class, + () -> StateArchiveBasePreflight.requireAdmitted(true, archive)); + + assertArrayEquals(evidence, Files.readAllBytes(history)); + assertFalse(Files.exists(archive.resolve("MANIFEST"))); + } + + @Test + public void staleExact26ScopeFailsWithoutManifestRewrite() throws Exception { + Path archive = temporaryFolder.newFolder("stale-exact-26").toPath(); + List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + participants.remove("abi"); + byte[] stale = manifest("archive-state/exact-26-abi-tombstone/v1", participants); + Path path = archive.resolve("MANIFEST"); + Files.write(path, stale); + + assertThrows(IllegalStateException.class, + () -> StateArchiveBasePreflight.requireAdmitted(true, archive)); + + assertArrayEquals(stale, Files.readAllBytes(path)); + } + + private static byte[] manifest(String scopeIdentity, List participants) + throws Exception { + List sorted = new ArrayList<>(participants); + Collections.sort(sorted); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MANIFEST_MAGIC); + output.writeShort(2); + output.writeShort(0); + output.writeInt(0); + writeString(output, scopeIdentity); + output.writeLong(0); + output.write(new byte[32]); + output.writeInt(sorted.size()); + for (String participant : sorted) { + writeString(output, participant); + } + output.flush(); + byte[] payload = bytes.toByteArray(); + ByteBuffer.wrap(payload).putInt(8, payload.length + Integer.BYTES); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(payload); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } + + private static void writeString(DataOutputStream output, String value) throws Exception { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + output.writeInt(encoded.length); + output.write(encoded); + } + + private static byte[] hash(long suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java b/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java index 2a0dce1038c..3f220c17996 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java @@ -592,13 +592,13 @@ public void nonContiguousAndDuplicateFreezeFailureLeavesLayerOwnersAttached() { } @Test - public void durableMarkerReceiptFailureLeavesFrozenBatchRetryable() { + public void durableMarkerEvidenceFailureLeavesFrozenBatchRetryable() { BlockSnapshotMeta meta = meta(22); HistoryCommitMarker marker = marker(meta); AccountAssetBlockProjectionBridge bridge = emptyBridge(); boolean[] substitute = {true}; - DurableHistoryMarkerRangeReceipt.Source source = - new DurableHistoryMarkerRangeReceipt.Source() { + DurableHistoryMarkerRangeEvidence.Source source = + new DurableHistoryMarkerRangeEvidence.Source() { @Override public HistoryCommitMarker marker(long epoch) { return substitute[0] ? AccountAssetBlockProjectionBridgeTest.marker(meta(23)) : marker; @@ -619,21 +619,21 @@ public BlockReverseDiff readCommitted(long epoch) { owner.attach(prepared); FrozenBatch batch = AccountAssetPreparedBlockPayloadOwner.freezeContiguous( Collections.singletonList(owner)); - DurableHistoryMarkerRangeReceipt receipt = - new DurableHistoryMarkerRangeReceipt(source, 1); + DurableHistoryMarkerRangeEvidence evidence = + new DurableHistoryMarkerRangeEvidence(source, 1); - assertThrows(ArchivePersistenceException.class, () -> receipt.seal(batch)); + assertThrows(ArchivePersistenceException.class, () -> evidence.seal(batch)); assertEquals(meta, batch.getExpectedMetas().get(0)); assertTrue(prepared.retainsCapturedView()); substitute[0] = false; - List payloads = receipt.seal(batch); + List payloads = evidence.seal(batch); assertEquals(1, payloads.size()); assertSame(view, payloads.get(0).getView()); assertFalse(prepared.retainsCapturedView()); assertTrue(plan(payloads.get(0).getMarker(), payloads.get(0).getView(), payloads.get(0).getAccountAssetManifest()).getMutations().values().stream() .allMatch(List::isEmpty)); - assertThrows(ArchivePersistenceException.class, () -> receipt.seal(batch)); + assertThrows(ArchivePersistenceException.class, () -> evidence.seal(batch)); } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java new file mode 100644 index 00000000000..41bee711bb0 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java @@ -0,0 +1,226 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; + +public class ArchiveAuthorityHandleSourcesTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void persistentAndNativeHandlesProduceReadyAndReleaseEveryPin() throws Exception { + try (Fixture fixture = new Fixture(temporaryFolder.newFolder("ready").toPath(), false)) { + ArchiveAuthoritySnapshotCollector collector = new ArchiveAuthoritySnapshotCollector( + fixture.sources, fixture.sources, fixture.sources, fixture.sources); + + ArchiveFormatAdmissionValidator.Result result = ArchiveFormatAdmissionValidator.inspect( + fixture.archive, collector.collect()); + + assertEquals(ArchiveFormatAdmissionValidator.Status.CURRENT_READY, result.getStatus()); + assertEquals(2, fixture.latestOpened.get()); + assertEquals(2, fixture.latestClosed.get()); + assertEquals(0, fixture.catalog.getReferenceCount( + fixture.catalog.getCurrentGenerationId())); + } + } + + @Test + public void latestIdentityFailureReleasesLatestAndCatalogPins() throws Exception { + try (Fixture fixture = new Fixture(temporaryFolder.newFolder("bad-latest").toPath(), true)) { + ArchiveAuthoritySnapshotCollector collector = new ArchiveAuthoritySnapshotCollector( + fixture.sources, fixture.sources, fixture.sources, fixture.sources); + + assertThrows(ArchivePersistenceException.class, collector::collect); + + assertEquals(1, fixture.latestOpened.get()); + assertEquals(1, fixture.latestClosed.get()); + assertEquals(0, fixture.catalog.getReferenceCount( + fixture.catalog.getCurrentGenerationId())); + } + } + + private static final class Fixture implements AutoCloseable { + private final Path archive; + private final HistorySegmentStore bodies; + private final HistoryIndexStore index; + private final HistoryCommitStore history; + private final LevelDbArchiveParticipant level; + private final RocksDbArchiveParticipant rocks; + private final PersistentServingKeyIndexCatalog catalog; + private final AtomicInteger latestOpened = new AtomicInteger(); + private final AtomicInteger latestClosed = new AtomicInteger(); + private final ArchiveAuthorityHandleSources sources; + + private Fixture(Path root, boolean wrongLatestHead) throws Exception { + archive = root.resolve("archive"); + List participants = ArchiveParticipantDescriptor.current().getParticipants(); + ArchiveBaseManifest manifest = new ArchiveBaseManifest(archive, participants); + manifest.ensureBase(meta(1)); + bodies = new HistorySegmentStore(archive, new BlockHistoryCodec(), 4096); + index = new HistoryIndexStore(archive, new HistoryIndexCodec()); + history = new HistoryCommitStore(archive, new HistoryCommitMarkerCodec()); + BlockReverseDiff diff = new BlockReverseDiff(meta(1), Collections.emptyList()); + HistoryLocation body = bodies.append(diff); + HistoryIndexLocation indexLocation = index.append(HistoryIndexRecord.from(diff, body)); + HistoryCommitMarker marker = new HistoryCommitMarker(diff.getMeta(), 0, body, + indexLocation, digest16(41), participants); + bodies.sync(); + index.sync(); + history.commit(marker); + + ArchiveProgressEnvelope checkpoint = progress( + ArchiveProgressEnvelope.Kind.APPLY_CHECKPOINT, null, marker); + ArchiveProgressEnvelope reader = progress( + ArchiveProgressEnvelope.Kind.READER_VISIBLE, null, marker); + Path checkpointPath = archive.resolve("progress/checkpoint.progress"); + Path readerPath = archive.resolve("progress/reader.progress"); + ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); + new ArchiveProgressFile(checkpointPath, progressCodec).store(checkpoint); + new ArchiveProgressFile(readerPath, progressCodec).store(reader); + + level = new LevelDbArchiveParticipant(root.resolve("level-abi"), "abi", participants); + rocks = new RocksDbArchiveParticipant(root.resolve("rocks-account"), "account", + participants); + Map participantSources = new LinkedHashMap<>(); + for (String participant : participants) { + ArchiveProgressEnvelope participantProgress = progress( + ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, participant, marker); + if ("abi".equals(participant)) { + level.apply(Collections.emptyList(), participantProgress); + participantSources.put(participant, level); + } else if ("account".equals(participant)) { + rocks.apply(Collections.emptyList(), participantProgress); + participantSources.put(participant, rocks); + } else { + participantSources.put(participant, () -> participantProgress); + } + } + + Path shadow = root.resolve("serving-shadow"); + try (PersistentServingKeyIndexGeneration generation = + PersistentServingKeyIndexGeneration.build(shadow, "generation-1", 0, hash(0), + Collections.singletonList(marker), index::read, participants, digest(90))) { + assertEquals(1, generation.getIndexedThrough()); + } + catalog = PersistentServingKeyIndexCatalog.create(root.resolve("catalog"), shadow, reader); + ArchiveReadSnapshot.PinnedLatestStateFactory latestFactory = serving -> { + latestOpened.incrementAndGet(); + return new TestLatestPin(serving, wrongLatestHead, latestClosed); + }; + sources = new ArchiveAuthorityHandleSources(history, checkpointPath, participantSources, + readerPath, catalog, latestFactory); + } + + @Override + public void close() throws Exception { + IOException failure = null; + try { + catalog.close(); + } catch (IOException closeFailure) { + failure = closeFailure; + } + rocks.close(); + level.close(); + history.close(); + index.close(); + bodies.close(); + if (failure != null) { + throw failure; + } + } + } + + private static final class TestLatestPin implements PinnedLatestState { + private final long block; + private final byte[] hash; + private final byte[] sourceDigest; + private final AtomicInteger closed; + private boolean released; + + private TestLatestPin(PersistentServingKeyIndexGeneration serving, boolean wrongHead, + AtomicInteger closed) { + block = serving.getIndexedThrough(); + hash = wrongHead ? hash(2) : serving.getHeadHash(); + sourceDigest = serving.getLatestSourceIdentityDigest(); + this.closed = closed; + } + + @Override + public long getBlockNumber() { + return block; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(hash, hash.length); + } + + @Override + public byte[] getSourceIdentityDigest() { + return Arrays.copyOf(sourceDigest, sourceDigest.length); + } + + @Override + public OldValue get(String dbName, byte[] physicalRawKey) { + throw new AssertionError("Admission must not read latest business data"); + } + + @Override + public List range(String dbName, byte[] lowerInclusive, + byte[] upperExclusive) { + throw new AssertionError("Admission must not scan latest business data"); + } + + @Override + public void close() { + assertTrue(!released); + released = true; + closed.incrementAndGet(); + } + } + + private static ArchiveProgressEnvelope progress(ArchiveProgressEnvelope.Kind kind, + String participant, HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), + ArchiveParticipantDescriptor.current().getParticipants()); + } + + private static BlockSnapshotMeta meta(long epoch) { + return new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L); + } + + private static byte[] hash(long suffix) { + byte[] value = new byte[32]; + value[31] = (byte) suffix; + return value; + } + + private static byte[] digest(int value) { + byte[] digest = new byte[32]; + Arrays.fill(digest, (byte) value); + return digest; + } + + private static byte[] digest16(int value) { + byte[] digest = new byte[16]; + Arrays.fill(digest, (byte) value); + return digest; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthoritySnapshotCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthoritySnapshotCollectorTest.java new file mode 100644 index 00000000000..a36b17e25fb --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthoritySnapshotCollectorTest.java @@ -0,0 +1,210 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveAuthoritySnapshotCollector.HistorySource; +import org.tron.core.db2.archive.ArchiveAuthoritySnapshotCollector.LatestSource; +import org.tron.core.db2.archive.ArchiveAuthoritySnapshotCollector.ProgressSource; +import org.tron.core.db2.archive.ArchiveAuthoritySnapshotCollector.ServingSource; + +public class ArchiveAuthoritySnapshotCollectorTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void stableSourcesProduceOneReadyBundle() throws Exception { + Path archive = temporaryFolder.newFolder("stable").toPath(); + ArchiveBaseManifest manifest = new ArchiveBaseManifest(archive, + ArchiveParticipantDescriptor.current().getParticipants()); + manifest.ensureBase(meta(1)); + FakeSources sources = new FakeSources(Drift.NONE); + ArchiveAuthoritySnapshotCollector collector = collector(sources); + + ArchiveFormatAdmissionValidator.Result result = ArchiveFormatAdmissionValidator.inspect( + archive, collector.collect()); + + assertEquals(ArchiveFormatAdmissionValidator.Status.CURRENT_READY, result.getStatus()); + assertEquals(2, sources.headReads); + assertEquals(2, sources.planReads); + assertEquals(2, sources.readerReads); + assertEquals(2, sources.servingReads); + assertEquals(2, sources.coverageReads); + assertEquals(2, sources.latestReads); + } + + @Test + public void missingAuthorityAndSourceFailureFailClosed() throws Exception { + FakeSources missing = new FakeSources(Drift.NONE); + missing.checkpoint = null; + assertThrows(ArchivePersistenceException.class, () -> collector(missing).collect()); + + FakeSources failing = new FakeSources(Drift.NONE); + failing.failHead = true; + assertThrows(IOException.class, () -> collector(failing).collect()); + } + + @Test + public void everyMutableBoundaryReplacementRejectsTheWholeSnapshot() { + for (Drift drift : Arrays.asList(Drift.PLAN, Drift.HEAD, Drift.READER, Drift.SERVING, + Drift.COVERAGE, Drift.LATEST)) { + FakeSources sources = new FakeSources(drift); + assertThrows(ArchivePersistenceException.class, () -> collector(sources).collect()); + } + } + + private static ArchiveAuthoritySnapshotCollector collector(FakeSources sources) { + return new ArchiveAuthoritySnapshotCollector(sources, sources, sources, sources); + } + + private enum Drift { + NONE, + PLAN, + HEAD, + READER, + SERVING, + COVERAGE, + LATEST + } + + private static final class FakeSources + implements HistorySource, ProgressSource, ServingSource, LatestSource { + private final Drift drift; + private final HistoryCommitMarker first = marker(1); + private final HistoryCommitMarker head = marker(2); + private ArchiveProgressEnvelope checkpoint; + private final Map participants = new LinkedHashMap<>(); + private final ArchiveProgressEnvelope reader; + private boolean failHead; + private int headReads; + private int planReads; + private int readerReads; + private int servingReads; + private int coverageReads; + private int latestReads; + + private FakeSources(Drift drift) { + this.drift = drift; + checkpoint = progress(ArchiveProgressEnvelope.Kind.APPLY_CHECKPOINT, null, head); + for (String participant : ArchiveParticipantDescriptor.current().getParticipants()) { + participants.put(participant, progress( + ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, participant, head)); + } + reader = progress(ArchiveProgressEnvelope.Kind.READER_VISIBLE, null, head); + } + + @Override + public HistoryCoverage coverage() { + coverageReads++; + return drift == Drift.COVERAGE && coverageReads == 2 + ? new HistoryCoverage(1, 3, 3, hash(3)) + : new HistoryCoverage(1, 2, 2, hash(2)); + } + + @Override + public HistoryCommitMarker first() { + return first; + } + + @Override + public HistoryCommitMarker head() throws IOException { + headReads++; + if (failHead) { + throw new IOException("history source unavailable"); + } + return drift == Drift.HEAD && headReads == 2 ? marker(3) : head; + } + + @Override + public boolean mutationPlanPresent() { + planReads++; + return drift == Drift.PLAN && planReads == 2; + } + + @Override + public ArchiveProgressEnvelope applyCheckpoint() { + return checkpoint; + } + + @Override + public Map participantProgress() { + return participants; + } + + @Override + public ArchiveProgressEnvelope readerVisible() { + readerReads++; + return drift == Drift.READER && readerReads == 2 + ? progress(ArchiveProgressEnvelope.Kind.READER_VISIBLE, null, first) : reader; + } + + @Override + public ArchiveAuthoritySourceBundle.ServingGenerationSnapshot current() { + servingReads++; + return serving(drift == Drift.SERVING && servingReads == 2 ? digest(81) : digest(80), + digest(90)); + } + + @Override + public byte[] sourceIdentityDigest() { + latestReads++; + return digest(drift == Drift.LATEST && latestReads == 2 ? 91 : 90); + } + } + + private static ArchiveAuthoritySourceBundle.ServingGenerationSnapshot serving( + byte[] prefixDigest, byte[] latestDigest) { + return new ArchiveAuthoritySourceBundle.ServingGenerationSnapshot( + ArchiveParticipantDescriptor.FORMAT_ID, + ArchiveParticipantDescriptor.current().getParticipants(), 0, 2, hash(2), prefixDigest, + latestDigest); + } + + private static ArchiveProgressEnvelope progress(ArchiveProgressEnvelope.Kind kind, + String participant, HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), + ArchiveParticipantDescriptor.current().getParticipants()); + } + + private static HistoryCommitMarker marker(long epoch) { + return new HistoryCommitMarker( + new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L), + epoch - 1, new HistoryLocation(0, epoch * 100, 100, (int) epoch, + digest(20 + (int) epoch)), + new HistoryIndexLocation(epoch * 50, 50, digest(30 + (int) epoch)), + digest16(40 + (int) epoch), ArchiveParticipantDescriptor.current().getParticipants()); + } + + private static BlockSnapshotMeta meta(long epoch) { + return new BlockSnapshotMeta(epoch, epoch, hash(epoch), new byte[32], epoch * 1_000L); + } + + private static byte[] hash(long suffix) { + byte[] value = new byte[32]; + value[31] = (byte) suffix; + return value; + } + + private static byte[] digest(int value) { + byte[] digest = new byte[32]; + Arrays.fill(digest, (byte) value); + return digest; + } + + private static byte[] digest16(int value) { + byte[] digest = new byte[16]; + Arrays.fill(digest, (byte) value); + return digest; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java index 12bce03a60b..167ae5250bd 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java @@ -7,6 +7,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.google.protobuf.ByteString; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -23,6 +24,8 @@ import org.tron.common.BaseMethodTest; import org.tron.core.db2.ISession; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; import org.tron.core.db2.archive.ArchiveTargetApplyCoordinator.Stage; import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.common.DB; @@ -31,6 +34,7 @@ import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.db2.core.SnapshotRoot; +import org.tron.protos.Protocol.Account; /** End-to-end ownership and recovery test from block capture to durable mixed participants. */ public class ArchiveBlockForwardMutationRecoveryTest extends BaseMethodTest { @@ -170,6 +174,129 @@ archive, new HistoryCommitMarkerCodec())) { } } + @Test + public void p66ActivationCaptureRecoversExactParticipantSetAfterNativeReopen() + throws Exception { + Path archive = temporaryFolder.newFolder("p66-activation-exact-capture").toPath(); + List markers = initializeHistory(archive, 1); + HistoryCommitMarker initial = markers.get(0); + HistoryCommitMarker target = markers.get(1); + Path checkpointPath = archive.resolve("progress/checkpoint.progress"); + Path readerPath = archive.resolve("progress/reader.progress"); + ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); + new ArchiveProgressFile(checkpointPath, progressCodec).store(global(Kind.APPLY_CHECKPOINT, + initial)); + new ArchiveProgressFile(readerPath, progressCodec).store(global(Kind.READER_VISIBLE, initial)); + + byte[] address = accountAddress(7); + String tokenId = "1000007"; + Account raw = Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .putAsset("asset-name", 30L).putAssetV2(tokenId, 30L).build(); + P66AccountAssetCodec codec = new P66AccountAssetCodec(); + byte[] canonicalAccount = codec.canonicalizeAccount( + Phase.P66_ACTIVATION, address, raw.toByteArray()); + P66AccountAssetCodec.AssetRow direct = codec.encodeAssetRow( + Phase.P66_ACTIVATION, address, tokenId, 30L); + byte[] assetKey = direct.getPhysicalRawKey(); + byte[] assetValue = direct.getPostValue().getValue(); + byte[] proposalKey = bytes(2, 61); + byte[] proposalValue = bytes(3, 71); + + ArchiveParticipantMutationBatch batch; + try (ViewFixture viewFixture = new ViewFixture()) { + ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture( + target.getMeta(), Phase.P66_ACTIVATION, + new ArchiveBlockForwardMutationLimits(10, 10, 1024, 1024, 1024 * 1024)); + capture.recordAccount(target.getMeta(), address, + BlockChangeView.PostValue.present(raw.toByteArray()), + BlockChangeView.PostValue.present(canonicalAccount)); + capture.recordAssetPut(target.getMeta(), address, assetKey, assetValue); + BlockChangeView view = viewFixture.capture(target.getMeta(), databases -> { + databases.get("account").put(address, raw.toByteArray()); + databases.get("proposal").put(proposalKey, proposalValue); + }); + capture.attach(view); + batch = capture.seal(target); + } + assertEquals(Phase.P66_ACTIVATION, batch.getTargetPhase()); + assertEquals(P66AccountAssetCodec.FORMAT_ID, batch.getAccountAssetFormatId()); + assertEquals(PARTICIPANTS, batch.getParticipants()); + String firstParticipant = PARTICIPANTS.get(0); + + try (ParticipantFixture participants = new ParticipantFixture(archive, initial)) { + try (HistoryCommitStore history = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, + checkpointPath, participants.engines, readerPath, PARTICIPANTS, + action -> action.run(), + (stage, participant) -> failAfter(stage, participant, firstParticipant), + temporary -> { }); + assertThrows(IOException.class, () -> coordinator.apply(batch, () -> { })); + } + + ArchiveTargetMutationPlan durablePlan = + new ArchiveTargetMutationPlanFile(checkpointPath).loadRequired(); + byte[] planDigest = durablePlan.digest(); + assertEquals(Phase.P66_ACTIVATION, durablePlan.getTargetPhase()); + assertEquals(target.getMeta().getEpoch(), + participants.engines.get(firstParticipant).loadProgress().getEpoch()); + assertNull(participants.account.get(address)); + assertNull(participants.accountAsset.get(assetKey)); + assertNull(participants.memory.get("proposal").get(proposalKey)); + + participants.reopenNativeParticipants(); + AtomicInteger refreshes = new AtomicInteger(); + RecoveryPlan recoveryPlan; + try (ArchiveParticipantRecoveryStorage recovery = + new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, + participants.engines, readerPath, PARTICIPANTS, action -> action.run(), + refreshes::incrementAndGet)) { + recoveryPlan = new ArchiveRecoveryExecutor(recovery).recover(); + } + + List replayed = new ArrayList<>(); + recoveryPlan.getActions().stream() + .filter(action -> action.getType() == ActionType.REPLAY_PARTICIPANT) + .forEach(action -> replayed.add(action.getParticipant())); + assertEquals(PARTICIPANTS.subList(1, PARTICIPANTS.size()), replayed); + assertEquals(ActionType.PUBLISH_READER_HEAD, + recoveryPlan.getActions().get(recoveryPlan.getActions().size() - 1).getType()); + assertEquals(1, refreshes.get()); + assertArrayEquals(canonicalAccount, participants.account.get(address)); + assertArrayEquals(assetValue, participants.accountAsset.get(assetKey)); + assertArrayEquals(proposalValue, + participants.memory.get("proposal").get(proposalKey)); + + ArchiveProgressEnvelope checkpoint = + new ArchiveProgressFile(checkpointPath, progressCodec).load(); + ArchiveProgressEnvelope reader = + new ArchiveProgressFile(readerPath, progressCodec).load(); + assertArrayEquals(planDigest, checkpoint.getMutationPlanDigest()); + assertArrayEquals(target.getMeta().getBlockHash(), checkpoint.getBlockHash()); + assertEquals(target.getMeta().getEpoch(), reader.getEpoch()); + assertArrayEquals(planDigest, reader.getMutationPlanDigest()); + for (String participant : PARTICIPANTS) { + ArchiveProgressEnvelope progress = participants.engines.get(participant).loadProgress(); + assertEquals(PARTICIPANTS, progress.getParticipants()); + assertEquals(target.getMeta().getEpoch(), progress.getEpoch()); + assertArrayEquals(target.getMeta().getBlockHash(), progress.getBlockHash()); + assertArrayEquals(planDigest, progress.getMutationPlanDigest()); + } + assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); + + participants.reopenNativeParticipants(); + try (ArchiveParticipantRecoveryStorage fixed = + new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, + participants.engines, readerPath, PARTICIPANTS)) { + assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); + } + assertArrayEquals(canonicalAccount, participants.account.get(address)); + assertArrayEquals(assetValue, participants.accountAsset.get(assetKey)); + assertArrayEquals(proposalValue, + participants.memory.get("proposal").get(proposalKey)); + } + } + @Test public void consecutiveCaptureTargetsReplaceDigestAndRecoverPutDelete() throws Exception { Path archive = temporaryFolder.newFolder("consecutive-capture-recovery").toPath(); @@ -468,6 +595,13 @@ private static byte[] hash(int suffix) { return hash; } + private static byte[] accountAddress(int suffix) { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = (byte) suffix; + return address; + } + private static byte[] bytes(int length, int value) { byte[] bytes = new byte[length]; Arrays.fill(bytes, (byte) value); @@ -518,17 +652,16 @@ public void close() { } private static final class ParticipantFixture implements AutoCloseable { - private final LevelDbArchiveParticipant account; - private final RocksDbArchiveParticipant accountAsset; + private final Path archive; + private LevelDbArchiveParticipant account; + private RocksDbArchiveParticipant accountAsset; private final Map counted = new LinkedHashMap<>(); private final Map memory = new LinkedHashMap<>(); private final Map engines = new LinkedHashMap<>(); private ParticipantFixture(Path archive, HistoryCommitMarker initial) throws IOException { - account = new LevelDbArchiveParticipant( - archive.resolve("participants/account"), "account", PARTICIPANTS); - accountAsset = new RocksDbArchiveParticipant( - archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS); + this.archive = archive; + openNativeParticipants(); for (String participant : PARTICIPANTS) { ArchiveParticipant delegate; if ("account".equals(participant)) { @@ -547,11 +680,40 @@ private ParticipantFixture(Path archive, HistoryCommitMarker initial) throws IOE } } - @Override - public void close() throws IOException { + private void reopenNativeParticipants() throws IOException { + closeNativeParticipants(); + openNativeParticipants(); + replaceNativeParticipant("account", account); + replaceNativeParticipant("account-asset", accountAsset); + } + + private void openNativeParticipants() throws IOException { + account = new LevelDbArchiveParticipant( + archive.resolve("participants/account"), "account", PARTICIPANTS); + try { + accountAsset = new RocksDbArchiveParticipant( + archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS); + } catch (IOException | RuntimeException failure) { + account.close(); + throw failure; + } + } + + private void replaceNativeParticipant(String participant, ArchiveParticipant delegate) { + CountingParticipant engine = new CountingParticipant(delegate); + counted.put(participant, engine); + engines.put(participant, engine); + } + + private void closeNativeParticipants() throws IOException { accountAsset.close(); account.close(); } + + @Override + public void close() throws IOException { + closeNativeParticipants(); + } } private static final class CountingParticipant implements ArchiveParticipant { diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveFormatAdmissionValidatorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveFormatAdmissionValidatorTest.java new file mode 100644 index 00000000000..47d52c1e7b7 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveFormatAdmissionValidatorTest.java @@ -0,0 +1,312 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator.Reason; +import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator.Result; +import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator.Status; + +public class ArchiveFormatAdmissionValidatorTest { + + private static final int MANIFEST_MAGIC = 0x54414d46; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void absentAndEmptyDirectoriesRemainEmptyNewWithoutWrites() throws Exception { + Path absent = temporaryFolder.getRoot().toPath().resolve("absent"); + Result absentResult = ArchiveFormatAdmissionValidator.inspect(absent); + assertEquals(Status.EMPTY_NEW, absentResult.getStatus()); + assertEquals(Reason.NONE, absentResult.getReason()); + assertFalse(Files.exists(absent)); + + Path empty = temporaryFolder.newFolder("empty").toPath(); + Result emptyResult = ArchiveFormatAdmissionValidator.inspect(empty); + assertEquals(Status.EMPTY_NEW, emptyResult.getStatus()); + try (java.util.stream.Stream entries = Files.list(empty)) { + assertFalse(entries.findAny().isPresent()); + } + } + + @Test + public void nonemptyDirectoryWithoutManifestRequiresQuarantineWithoutMutation() + throws Exception { + Path archive = temporaryFolder.newFolder("missing-manifest").toPath(); + Path committed = archive.resolve("commits"); + byte[] evidence = new byte[]{1, 2, 3}; + Files.write(committed, evidence); + + Result result = ArchiveFormatAdmissionValidator.inspect(archive); + + assertEquals(Status.QUARANTINE_REQUIRED, result.getStatus()); + assertEquals(Reason.NONEMPTY_WITHOUT_MANIFEST, result.getReason()); + assertArrayEquals(evidence, Files.readAllBytes(committed)); + assertFalse(Files.exists(archive.resolve("MANIFEST"))); + } + + @Test + public void currentExact27ManifestIsCurrentBaseButNotStartupReady() throws Exception { + Path archive = temporaryFolder.newFolder("current-base").toPath(); + ArchiveBaseManifest manifest = new ArchiveBaseManifest(archive, + ArchiveParticipantDescriptor.current().getParticipants()); + manifest.ensureBase(meta(1)); + byte[] before = Files.readAllBytes(archive.resolve("MANIFEST")); + + Result result = ArchiveFormatAdmissionValidator.inspect(archive); + + assertEquals(Status.CURRENT_BASE, result.getStatus()); + assertEquals(Reason.NONE, result.getReason()); + assertArrayEquals(before, Files.readAllBytes(archive.resolve("MANIFEST"))); + } + + @Test + public void staleExact26ManifestRequiresQuarantineWithoutRewrite() throws Exception { + Path archive = temporaryFolder.newFolder("stale-exact-26").toPath(); + List exact26 = new ArrayList<>( + ArchiveParticipantDescriptor.current().getParticipants()); + assertTrue(exact26.remove("abi")); + byte[] stale = manifest("archive-state/exact-26-abi-tombstone/v1", exact26); + Path path = archive.resolve("MANIFEST"); + Files.write(path, stale); + + Result result = ArchiveFormatAdmissionValidator.inspect(archive); + + assertEquals(Status.QUARANTINE_REQUIRED, result.getStatus()); + assertEquals(Reason.UNSUPPORTED_OR_CORRUPT_MANIFEST, result.getReason()); + assertArrayEquals(stale, Files.readAllBytes(path)); + } + + @Test + public void completeExact27AuthorityBundleIsCurrentReadyWithoutWrites() throws Exception { + Path archive = currentArchive("current-ready"); + byte[] before = Files.readAllBytes(archive.resolve("MANIFEST")); + + Result result = ArchiveFormatAdmissionValidator.inspect(archive, readyBundle(false)); + + assertEquals(Status.CURRENT_READY, result.getStatus()); + assertEquals(Reason.NONE, result.getReason()); + assertArrayEquals(before, Files.readAllBytes(archive.resolve("MANIFEST"))); + } + + @Test + public void activePlanOrIncompleteProgressRequiresQuarantine() throws Exception { + Path archive = currentArchive("incomplete-authorities"); + ArchiveAuthoritySourceBundle complete = readyBundle(false); + Map incomplete = new LinkedHashMap<>( + complete.getParticipantProgress()); + incomplete.remove("abi"); + ArchiveAuthoritySourceBundle missingAbi = bundle(false, complete.getApplyCheckpoint(), + incomplete, complete.getReaderVisible(), complete.getServingGeneration(), + coverage(1, 2, 2), digest(90)); + + for (ArchiveAuthoritySourceBundle candidate : Arrays.asList(readyBundle(true), missingAbi, + bundle(false, null, complete.getParticipantProgress(), complete.getReaderVisible(), + complete.getServingGeneration(), coverage(1, 2, 2), digest(90)))) { + Result result = ArchiveFormatAdmissionValidator.inspect(archive, candidate); + assertEquals(Status.QUARANTINE_REQUIRED, result.getStatus()); + assertEquals(Reason.INCOMPLETE_OR_INCONSISTENT_AUTHORITIES, result.getReason()); + } + } + + @Test + public void staleReaderOrServingSourceRequiresQuarantine() throws Exception { + Path archive = currentArchive("mismatched-authorities"); + ArchiveAuthoritySourceBundle complete = readyBundle(false); + HistoryCommitMarker first = marker(1); + ArchiveProgressEnvelope staleReader = progress(ArchiveProgressEnvelope.Kind.READER_VISIBLE, + null, first, null); + ArchiveAuthoritySourceBundle staleReaderBundle = bundle(false, + complete.getApplyCheckpoint(), complete.getParticipantProgress(), staleReader, + complete.getServingGeneration(), coverage(1, 2, 2), digest(90)); + ArchiveAuthoritySourceBundle.ServingGenerationSnapshot wrongSource = serving(digest(91)); + ArchiveAuthoritySourceBundle wrongSourceBundle = bundle(false, + complete.getApplyCheckpoint(), complete.getParticipantProgress(), + complete.getReaderVisible(), wrongSource, coverage(1, 2, 2), digest(90)); + + for (ArchiveAuthoritySourceBundle candidate + : Arrays.asList(staleReaderBundle, wrongSourceBundle)) { + Result result = ArchiveFormatAdmissionValidator.inspect(archive, candidate); + assertEquals(Status.QUARANTINE_REQUIRED, result.getStatus()); + assertEquals(Reason.INCOMPLETE_OR_INCONSISTENT_AUTHORITIES, result.getReason()); + } + } + + @Test + public void nonContiguousOrMisalignedHistoryCoverageRequiresQuarantine() throws Exception { + Path archive = currentArchive("bad-history-coverage"); + ArchiveAuthoritySourceBundle complete = readyBundle(false); + + for (HistoryCoverage invalid : Arrays.asList( + coverage(1, 1, 2), coverage(1, 2, 3), coverage(1, 2, 2, hash(3)))) { + ArchiveAuthoritySourceBundle candidate = bundle(false, complete.getApplyCheckpoint(), + complete.getParticipantProgress(), complete.getReaderVisible(), + complete.getServingGeneration(), invalid, digest(90)); + Result result = ArchiveFormatAdmissionValidator.inspect(archive, candidate); + assertEquals(Status.QUARANTINE_REQUIRED, result.getStatus()); + assertEquals(Reason.INCOMPLETE_OR_INCONSISTENT_AUTHORITIES, result.getReason()); + } + } + + @Test + public void servingPrefixDigestIsNotAHistoryCoverageAuthority() throws Exception { + Path archive = currentArchive("serving-prefix-not-coverage"); + ArchiveAuthoritySourceBundle complete = readyBundle(false); + ArchiveAuthoritySourceBundle.ServingGenerationSnapshot differentInternalDigest = + new ArchiveAuthoritySourceBundle.ServingGenerationSnapshot( + ArchiveParticipantDescriptor.FORMAT_ID, + ArchiveParticipantDescriptor.current().getParticipants(), 0, 2, hash(2), + digest(81), digest(90)); + ArchiveAuthoritySourceBundle candidate = bundle(false, complete.getApplyCheckpoint(), + complete.getParticipantProgress(), complete.getReaderVisible(), + differentInternalDigest, coverage(1, 2, 2), digest(90)); + + Result result = ArchiveFormatAdmissionValidator.inspect(archive, candidate); + + assertEquals(Status.CURRENT_READY, result.getStatus()); + } + + private static byte[] manifest(String scopeIdentity, List participants) throws Exception { + List sorted = new ArrayList<>(participants); + Collections.sort(sorted); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MANIFEST_MAGIC); + output.writeShort(2); + output.writeShort(0); + output.writeInt(0); + writeString(output, scopeIdentity); + output.writeLong(0); + output.write(new byte[32]); + output.writeInt(sorted.size()); + for (String participant : sorted) { + writeString(output, participant); + } + output.flush(); + byte[] payload = bytes.toByteArray(); + ByteBuffer.wrap(payload).putInt(8, payload.length + Integer.BYTES); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(payload); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } + + private static void writeString(DataOutputStream output, String value) throws Exception { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + output.writeInt(encoded.length); + output.write(encoded); + } + + private static BlockSnapshotMeta meta(long epoch) { + byte[] hash = new byte[32]; + hash[31] = (byte) epoch; + return new BlockSnapshotMeta(epoch, epoch, hash, new byte[32], epoch * 1_000L); + } + + private Path currentArchive(String name) throws Exception { + Path archive = temporaryFolder.newFolder(name).toPath(); + ArchiveBaseManifest manifest = new ArchiveBaseManifest(archive, + ArchiveParticipantDescriptor.current().getParticipants()); + manifest.ensureBase(meta(1)); + return archive; + } + + private static ArchiveAuthoritySourceBundle readyBundle(boolean activePlan) { + HistoryCommitMarker head = marker(2); + byte[] planDigest = null; + ArchiveProgressEnvelope checkpoint = progress(ArchiveProgressEnvelope.Kind.APPLY_CHECKPOINT, + null, head, planDigest); + Map participantProgress = new LinkedHashMap<>(); + for (String participant : ArchiveParticipantDescriptor.current().getParticipants()) { + participantProgress.put(participant, progress( + ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, participant, head, planDigest)); + } + return bundle(activePlan, checkpoint, participantProgress, + progress(ArchiveProgressEnvelope.Kind.READER_VISIBLE, null, head, planDigest), + serving(digest(90)), coverage(1, 2, 2), digest(90)); + } + + private static ArchiveAuthoritySourceBundle bundle(boolean activePlan, + ArchiveProgressEnvelope checkpoint, + Map participantProgress, + ArchiveProgressEnvelope readerVisible, + ArchiveAuthoritySourceBundle.ServingGenerationSnapshot serving, + HistoryCoverage coverage, byte[] latestSourceDigest) { + return new ArchiveAuthoritySourceBundle(activePlan, coverage, marker(1), marker(2), + checkpoint, participantProgress, readerVisible, serving, latestSourceDigest); + } + + private static HistoryCoverage coverage(long firstEpoch, long recordCount, long headEpoch) { + return coverage(firstEpoch, recordCount, headEpoch, hash(headEpoch)); + } + + private static HistoryCoverage coverage(long firstEpoch, long recordCount, long headEpoch, + byte[] headHash) { + return new HistoryCoverage(firstEpoch, recordCount, headEpoch, headHash); + } + + private static ArchiveAuthoritySourceBundle.ServingGenerationSnapshot serving( + byte[] sourceDigest) { + return new ArchiveAuthoritySourceBundle.ServingGenerationSnapshot( + ArchiveParticipantDescriptor.FORMAT_ID, + ArchiveParticipantDescriptor.current().getParticipants(), 0, 2, hash(2), digest(80), + sourceDigest); + } + + private static ArchiveProgressEnvelope progress(ArchiveProgressEnvelope.Kind kind, + String participant, HistoryCommitMarker marker, byte[] planDigest) { + return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), planDigest, + ArchiveParticipantDescriptor.current().getParticipants()); + } + + private static HistoryCommitMarker marker(long epoch) { + return new HistoryCommitMarker( + new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L), + epoch - 1, new HistoryLocation(0, epoch * 100, 100, (int) epoch, + digest(20 + (int) epoch)), + new HistoryIndexLocation(epoch * 50, 50, digest(30 + (int) epoch)), + digest16(40 + (int) epoch), + ArchiveParticipantDescriptor.current().getParticipants()); + } + + private static byte[] hash(long suffix) { + byte[] value = new byte[32]; + value[31] = (byte) suffix; + return value; + } + + private static byte[] digest(int value) { + byte[] digest = new byte[32]; + Arrays.fill(digest, (byte) value); + return digest; + } + + private static byte[] digest16(int value) { + byte[] digest = new byte[16]; + Arrays.fill(digest, (byte) value); + return digest; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java index b7bcde794ad..86d2d7e61d6 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java @@ -1,5 +1,6 @@ package org.tron.core.db2.archive; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -57,6 +58,49 @@ public void makesHistoryVisibleOnlyAfterOrderedDurabilityStages() throws Excepti } } + @Test + public void rollsBackShortP66OnTailWithoutLosingActivationReverseDeletion() throws Exception { + Path archive = temporaryFolder.newFolder("p66-short-rollback").toPath(); + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = 7; + byte[] directKey = new P66AccountAssetCodec().assetPhysicalKey(address, "1000007"); + BlockReverseDiff off = new BlockReverseDiff(diff(1).getMeta(), Collections.emptyList()); + BlockReverseDiff activation = new BlockReverseDiff(diff(2).getMeta(), Arrays.asList( + new DbGroup("account", Collections.singletonList( + new Entry(address, OldValue.present(account(address, 20L))))), + new DbGroup("account-asset", Collections.singletonList( + new Entry(directKey, OldValue.absent()))))); + BlockReverseDiff on = new BlockReverseDiff(diff(3).getMeta(), Collections.singletonList( + new DbGroup("account-asset", Collections.singletonList( + new Entry(directKey, OldValue.present(java.nio.ByteBuffer.allocate(Long.BYTES) + .putLong(30L).array())))))); + Set p66Databases = new java.util.LinkedHashSet<>( + Arrays.asList("account", "account-asset", "properties")); + + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( + archive, 4096, p66Databases)) { + writer.acceptAll(Arrays.asList(off, activation, on)); + writer.revert(on.getMeta()); + assertEquals(activation.getMeta(), writer.committedHeadMeta()); + Entry reverseDeletion = writer.readCommitted(2).getGroups().stream() + .filter(group -> "account-asset".equals(group.getDbName())) + .findFirst().orElseThrow(AssertionError::new).getEntries().get(0); + assertArrayEquals(directKey, reverseDeletion.getKey()); + assertFalse(reverseDeletion.getOldValue().isPresent()); + } + + try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter( + archive, 4096, p66Databases)) { + assertEquals(activation.getMeta(), reopened.committedHeadMeta()); + assertThrows(IllegalArgumentException.class, () -> reopened.readCommitted(3)); + Entry reverseDeletion = reopened.readCommitted(2).getGroups().stream() + .filter(group -> "account-asset".equals(group.getDbName())) + .findFirst().orElseThrow(AssertionError::new).getEntries().get(0); + assertFalse(reverseDeletion.getOldValue().isPresent()); + } + } + @Test public void rollsBackPreparedSuffixAtEveryPreCommitFailure() throws Exception { for (Stage failedStage : Stage.values()) { @@ -315,6 +359,28 @@ public void buildsPersistentServingGenerationFromCommittedWriterPrefix() throws } } + @Test + public void exposesImmutableContiguousHistoryCoverageAcrossTailChanges() throws Exception { + Path archive = temporaryFolder.newFolder("history-coverage").toPath(); + initializeHistory(archive, 3); + + try (HistoryCommitStore commits = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + assertCoverage(commits.coverage(), 1, 3, 3, hash(3)); + byte[] exposedHash = commits.coverage().getHeadHash(); + exposedHash[31] = 99; + assertArrayEquals(hash(3), commits.coverage().getHeadHash()); + + commits.truncateAfter(2); + assertCoverage(commits.coverage(), 1, 2, 2, hash(2)); + HistoryCommitMarker second = commits.head(); + commits.removeHead(second.getMeta()); + assertCoverage(commits.coverage(), 1, 1, 1, hash(1)); + commits.removeHead(commits.head().getMeta()); + assertNull(commits.coverage()); + } + } + @Test public void commitLogForceBoundaryFailurePreservesRecordAsUncertain() throws Exception { Path archive = temporaryFolder.newFolder("uncertain-marker").toPath(); @@ -340,6 +406,14 @@ private static Set databases() { return new java.util.LinkedHashSet<>(Arrays.asList("account", "properties")); } + private static void assertCoverage(HistoryCoverage coverage, long firstEpoch, + long recordCount, long headEpoch, byte[] headHash) { + assertEquals(firstEpoch, coverage.getFirstEpoch()); + assertEquals(recordCount, coverage.getRecordCount()); + assertEquals(headEpoch, coverage.getHeadEpoch()); + assertArrayEquals(headHash, coverage.getHeadHash()); + } + private static void initializeHistory(Path archive, int lastEpoch) throws Exception { try (HistorySegmentStore bodies = new HistorySegmentStore( archive, new BlockHistoryCodec(), 4096); diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java index fc07e236495..82dc84ee9d9 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java @@ -1,7 +1,6 @@ package org.tron.core.db2.archive; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -11,7 +10,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import org.junit.Rule; import org.junit.Test; @@ -23,32 +21,33 @@ public class ArchiveParticipantDescriptorTest { public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void definesExact26WithStableAbiTombstoneAndLegacyAsset() { + public void definesExact27WithAbiAndBothAssetIssueStores() { ArchiveParticipantDescriptor descriptor = ArchiveParticipantDescriptor.current(); - assertEquals(26, descriptor.getParticipants().size()); + assertEquals(27, descriptor.getParticipants().size()); assertEquals(ArchiveParticipantDescriptor.ABI_STORE_ID, descriptor.getStoreId("abi")); assertEquals(6, descriptor.getStoreId("asset-issue")); assertEquals(7, descriptor.getStoreId("asset-issue-v2")); - assertEquals("abi", descriptor.getTombstonesByStoreId().get(1)); + assertTrue(descriptor.getTombstonesByStoreId().isEmpty()); assertTrue(descriptor.getParticipants().contains("asset-issue")); assertTrue(descriptor.getParticipants().contains("asset-issue-v2")); - assertFalse(descriptor.getParticipants().contains("abi")); - assertEquals("archive-state/exact-26-abi-tombstone/v1", + assertTrue(descriptor.getParticipants().contains("abi")); + assertEquals("archive-state/exact-27-abi-retained/v1", ArchiveParticipantDescriptor.FORMAT_ID); } @Test - public void rejectsOldExact27AndV2OnlyExact25ParticipantSets() { + public void rejectsAbiExcludedExact26AndV2OnlyExact25ParticipantSets() { ArchiveParticipantDescriptor descriptor = ArchiveParticipantDescriptor.current(); - List exact27 = new ArrayList<>(descriptor.getParticipants()); - exact27.add("abi"); + List exact26 = new ArrayList<>(descriptor.getParticipants()); + exact26.remove("abi"); List exact25 = new ArrayList<>(descriptor.getParticipants()); + exact25.remove("abi"); exact25.remove("asset-issue"); assertThrows(ArchivePersistenceException.class, - () -> descriptor.requireExactParticipants(exact27)); + () -> descriptor.requireExactParticipants(exact26)); assertThrows(ArchivePersistenceException.class, () -> descriptor.requireExactParticipants(exact25)); descriptor.requireExactParticipants(descriptor.getParticipants()); @@ -57,7 +56,7 @@ public void rejectsOldExact27AndV2OnlyExact25ParticipantSets() { @Test public void manifestBindsApprovedScopeAndRejectsLegacyVersion() throws Exception { List participants = ArchiveParticipantDescriptor.current().getParticipants(); - Path archive = temporaryFolder.newFolder("exact-26-manifest").toPath(); + Path archive = temporaryFolder.newFolder("exact-27-manifest").toPath(); ArchiveBaseManifest manifest = new ArchiveBaseManifest(archive, participants); manifest.ensureBase(meta(1)); @@ -65,11 +64,10 @@ public void manifestBindsApprovedScopeAndRejectsLegacyVersion() throws Exception assertEquals(2, ByteBuffer.wrap(encoded, Integer.BYTES, Short.BYTES).getShort()); new ArchiveBaseManifest(archive, participants); - List oldExact27 = new ArrayList<>(participants); - oldExact27.add("abi"); - Collections.sort(oldExact27); + List abiExcludedExact26 = new ArrayList<>(participants); + abiExcludedExact26.remove("abi"); assertThrows(ArchivePersistenceException.class, - () -> new ArchiveBaseManifest(archive, oldExact27)); + () -> new ArchiveBaseManifest(archive, abiExcludedExact26)); ByteBuffer.wrap(encoded).putShort(Integer.BYTES, (short) 1); byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java index b5eb6fb5599..7d202775dfb 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java @@ -22,6 +22,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.tron.common.utils.ByteArray; import org.tron.core.db2.archive.ArchiveReadContext.HistoricalStore; import org.tron.core.db2.archive.ArchiveReadContext.StoreAdapter; import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; @@ -30,6 +31,9 @@ import org.tron.core.db2.archive.HistoricalRangeOverlay.KeyRange; import org.tron.core.db2.archive.HistoricalRangeOverlay.Limits; import org.tron.core.db2.archive.HistoryIndexRecord.KeyGroup; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; +import org.tron.protos.Protocol.Account; +import org.tron.protos.Protocol.AccountType; import org.tron.protos.contract.SmartContractOuterClass.SmartContract; public class ArchiveReadSnapshotTest { @@ -159,6 +163,141 @@ public void bindsEveryVersionedPhysicalStoreToOneRequestSnapshot() throws Except } } + @Test + public void contextResolvesHistoricalAccountAssetBeforePinnedHead() throws Exception { + byte[] address = address(41); + String tokenId = "1000001"; + byte[] account = account(address, false, tokenId, 17L); + byte[] directKey = new P66AccountAssetCodec().assetPhysicalKey(address, tokenId); + try (Fixture fixture = new Fixture( + temporaryFolder.newFolder("historical-account-asset-context").toPath())) { + fixture.append(diff(1, + new DbGroup("properties", Collections.singletonList(new Entry( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), + OldValue.present(ByteArray.fromLong(0L))))), + new DbGroup("account", Collections.singletonList( + new Entry(address, OldValue.present(account)))), + new DbGroup("account-asset", Collections.singletonList( + new Entry(directKey, OldValue.absent()))))); + fixture.sync(); + InMemoryLatest latest = new InMemoryLatest(1, hash(1), Collections.emptyMap()); + + try (ArchiveReadContext context = ArchiveReadContext.open( + fixture.snapshot(0, latest), rawAdapters().adapters)) { + HistoricalAccountAssetBalanceResolver.Result result = + context.resolveAccountAsset(address, tokenId); + assertEquals(0L, result.getBlockNumber()); + assertEquals(Phase.P66_OFF, result.getPhase()); + assertEquals(17L, result.getBalance()); + assertArrayEquals(account, result.getAccountValue()); + byte[] callerCopy = result.getAccountValue(); + callerCopy[0] ^= 1; + assertArrayEquals(account, result.getAccountValue()); + + HistoricalAccountAssetPrefixResolver.Result all = context.resolveAccountAssets(address, + new HistoricalAccountAssetPrefixResolver.Limits(10, 10, 10, 64, 8, 1_000)); + assertEquals(1, all.getBalances().size()); + assertEquals(tokenId, all.getBalances().get(0).getTokenId()); + assertEquals(17L, all.getBalances().get(0).getBalance()); + } + assertTrue(latest.closed); + } + } + + @Test + public void resolvesFixedP66TransitionVectorsAcrossCommittedSnapshots() throws Exception { + byte[] address = address(43); + String tokenId = "1000007"; + byte[] directKey = new P66AccountAssetCodec().assetPhysicalKey(address, tokenId); + byte[] offAtZero = account(address, false, tokenId, 10L); + byte[] offAtOne = account(address, false, tokenId, 20L); + byte[] activationAtTwo = optimizedAccount(address, 2_000L); + byte[] onAtThree = optimizedAccount(address, 3_000L); + byte[] onAtFour = optimizedAccount(address, 4_000L); + HistoricalAccountAssetPrefixResolver.Limits limits = + new HistoricalAccountAssetPrefixResolver.Limits(10, 10, 10, 64, 8, 1_000); + + try (Fixture fixture = new Fixture( + temporaryFolder.newFolder("p66-transition-vectors").toPath())) { + fixture.append(diff(1, + new DbGroup("account", Collections.singletonList( + new Entry(address, OldValue.present(offAtZero)))))); + BlockReverseDiff activation = diff(2, + new DbGroup("properties", Collections.singletonList(new Entry( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), + OldValue.present(ByteArray.fromLong(0L))))), + new DbGroup("account", Collections.singletonList( + new Entry(address, OldValue.present(offAtOne)))), + new DbGroup("account-asset", Collections.singletonList( + new Entry(directKey, OldValue.absent())))); + fixture.append(activation); + fixture.append(diff(3, + new DbGroup("account", Collections.singletonList( + new Entry(address, OldValue.present(activationAtTwo)))), + new DbGroup("account-asset", Collections.singletonList( + new Entry(directKey, OldValue.present(ByteArray.fromLong(30L))))))); + fixture.append(diff(4, + new DbGroup("account", Collections.singletonList( + new Entry(address, OldValue.present(onAtThree)))), + new DbGroup("account-asset", Collections.singletonList( + new Entry(directKey, OldValue.present(ByteArray.fromLong(40L))))))); + fixture.sync(); + + Entry activationReverseAsset = activation.getGroups().stream() + .filter(group -> "account-asset".equals(group.getDbName())) + .findFirst().orElseThrow(AssertionError::new).getEntries().get(0); + assertArrayEquals(directKey, activationReverseAsset.getKey()); + assertFalse(activationReverseAsset.getOldValue().isPresent()); + + Map> latestValues = new HashMap<>(); + latestValues.put("properties", Collections.singletonMap( + text(HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey()), + ByteArray.fromLong(1L))); + latestValues.put("account", Collections.singletonMap(text(address), onAtFour)); + latestValues.put("account-asset", Collections.singletonMap( + text(directKey), ByteArray.fromLong(50L))); + + assertAccountAssetVector(fixture, latestValues, 1L, Phase.P66_OFF, offAtOne, + tokenId, 20L, limits); + // The request API reports the activation target as P66_ON because both use the same + // canonical direct-row layout; the durable mutation plan retains P66_ACTIVATION. + assertAccountAssetVector(fixture, latestValues, 2L, Phase.P66_ON, activationAtTwo, + tokenId, 30L, limits); + assertAccountAssetVector(fixture, latestValues, 3L, Phase.P66_ON, onAtThree, + tokenId, 40L, limits); + } + } + + @Test + public void accountAssetContextRejectsForeignAdaptersAndUseAfterClose() throws Exception { + byte[] address = address(42); + String tokenId = "1000001"; + Map latestValues = new HashMap<>(); + latestValues.put(text(HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey()), + ByteArray.fromLong(1L)); + latestValues.put(text(address), account(address, true, null, 0L)); + InMemoryLatest latest = new InMemoryLatest(0, hash(0), latestValues); + AdapterSet adapters = rawAdapters(); + try (Fixture fixture = new Fixture( + temporaryFolder.newFolder("closed-account-asset-context").toPath())) { + ArchiveReadContext context = ArchiveReadContext.open( + fixture.snapshot(0, latest), adapters.adapters); + StoreAdapter foreignAccount = StoreAdapter.define("account", value -> value); + assertThrows(IllegalArgumentException.class, () -> context.store(foreignAccount)); + StoreAdapter foreignAbi = StoreAdapter.define("abi", value -> value); + assertThrows(IllegalArgumentException.class, () -> context.store(foreignAbi)); + assertEquals(0L, context.resolveAccountAsset(address, tokenId).getBalance()); + + context.close(); + assertTrue(latest.closed); + assertThrows(IllegalStateException.class, + () -> context.resolveAccountAsset(address, tokenId)); + assertThrows(IllegalStateException.class, + () -> context.resolveAccountAssets(address, + new HistoricalAccountAssetPrefixResolver.Limits(10, 10, 10, 64, 8, 1_000))); + } + } + @Test public void rejectsIncompleteOrDerivedStoreAdaptersAndReleasesSnapshot() throws Exception { assertThrows(IllegalArgumentException.class, @@ -299,6 +438,59 @@ private static byte[] hash(int suffix) { return hash; } + private static byte[] address(int suffix) { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = (byte) suffix; + return address; + } + + private static byte[] account(byte[] address, boolean optimized, String tokenId, long balance) { + Account.Builder builder = Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .setType(AccountType.Normal).setAssetOptimized(optimized); + if (tokenId != null) { + builder.putAsset("asset-name", balance).putAssetV2(tokenId, balance); + } + return builder.build().toByteArray(); + } + + private static byte[] optimizedAccount(byte[] address, long balance) { + return Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .setType(AccountType.Normal).setAssetOptimized(true).setBalance(balance) + .build().toByteArray(); + } + + private static void assertAccountAssetVector(Fixture fixture, + Map> latestValues, long target, Phase expectedPhase, + byte[] expectedAccount, + String tokenId, long expectedBalance, + HistoricalAccountAssetPrefixResolver.Limits limits) throws Exception { + InMemoryLatest latest = InMemoryLatest.scoped(4, hash(4), latestValues); + ArchiveReadSnapshot snapshot = fixture.snapshot(target, latest); + assertArrayEquals(hash(4), snapshot.getPinnedHash()); + snapshot.requirePinnedIdentity(); + try (ArchiveReadContext context = ArchiveReadContext.open( + snapshot, rawAdapters().adapters)) { + assertEquals(target, context.getTargetBlock()); + assertEquals(4L, context.getPinnedBlock()); + HistoricalAccountAssetBalanceResolver.Result exact = + context.resolveAccountAsset(address(43), tokenId); + assertEquals(expectedPhase, exact.getPhase()); + assertArrayEquals(expectedAccount, exact.getAccountValue()); + assertEquals(expectedBalance, exact.getBalance()); + + HistoricalAccountAssetPrefixResolver.Result prefix = + context.resolveAccountAssets(address(43), limits); + assertEquals(expectedPhase, prefix.getPhase()); + assertArrayEquals(expectedAccount, prefix.getAccountValue()); + assertEquals(1, prefix.getBalances().size()); + assertEquals(tokenId, prefix.getBalances().get(0).getTokenId()); + assertEquals(expectedBalance, prefix.getBalances().get(0).getBalance()); + snapshot.requirePinnedIdentity(); + } + assertTrue(latest.closed); + } + private static byte[] bytes(String value) { return value.getBytes(StandardCharsets.UTF_8); } @@ -384,15 +576,35 @@ private static final class InMemoryLatest implements PinnedLatestState { private final long block; private final byte[] hash; private final Map values; + private final Map> scopedValues; private boolean closed; private InMemoryLatest(long block, byte[] hash, Map values) { this.block = block; this.hash = Arrays.copyOf(hash, hash.length); this.values = new HashMap<>(); + this.scopedValues = null; values.forEach((key, value) -> this.values.put(key, Arrays.copyOf(value, value.length))); } + private InMemoryLatest(long block, byte[] hash, + Map> scopedValues, boolean scoped) { + this.block = block; + this.hash = Arrays.copyOf(hash, hash.length); + this.values = Collections.emptyMap(); + this.scopedValues = new HashMap<>(); + scopedValues.forEach((dbName, rows) -> { + Map copy = new HashMap<>(); + rows.forEach((key, value) -> copy.put(key, Arrays.copyOf(value, value.length))); + this.scopedValues.put(dbName, copy); + }); + } + + private static InMemoryLatest scoped(long block, byte[] hash, + Map> values) { + return new InMemoryLatest(block, hash, values, true); + } + @Override public long getBlockNumber() { return block; @@ -405,14 +617,18 @@ public byte[] getBlockHash() { @Override public OldValue get(String dbName, byte[] physicalRawKey) { - return OldValue.fromNullable(values.get(text(physicalRawKey))); + Map rows = scopedValues == null ? values + : scopedValues.getOrDefault(dbName, Collections.emptyMap()); + return OldValue.fromNullable(rows.get(text(physicalRawKey))); } @Override public List range(String dbName, byte[] lowerInclusive, byte[] upperExclusive) { List result = new ArrayList<>(); - values.forEach((key, value) -> { + Map rows = scopedValues == null ? values + : scopedValues.getOrDefault(dbName, Collections.emptyMap()); + rows.forEach((key, value) -> { byte[] rawKey = bytes(key); if (BlockReverseDiff.compareUnsigned(rawKey, lowerInclusive) >= 0 && (upperExclusive == null diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGateTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGateTest.java new file mode 100644 index 00000000000..8a8429dbe43 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGateTest.java @@ -0,0 +1,162 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedHistory; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.archive.ArchiveRuntimeQueryGate.Lease; +import org.tron.core.db2.archive.ArchiveRuntimeQueryGate.State; + +public class ArchiveRuntimeQueryGateTest { + + @Test + public void quiesceRejectsNewPinsAndCloseRequiresEveryLease() throws Exception { + PinnedSnapshot pinned = snapshot(); + AtomicInteger pinCalls = new AtomicInteger(); + ArchiveRuntimeQueryGate gate = new ArchiveRuntimeQueryGate(target -> { + pinCalls.incrementAndGet(); + return pinned.snapshot; + }); + + Lease lease = gate.pin(0); + + assertSame(pinned.snapshot, lease.getSnapshot()); + assertEquals(1, gate.getActiveLeaseCount()); + assertFalse(gate.isDrained()); + gate.quiesce(); + assertEquals(State.QUIESCING, gate.getState()); + assertThrows(IllegalStateException.class, () -> gate.pin(0)); + assertEquals(1, pinCalls.get()); + assertThrows(IllegalStateException.class, gate::close); + assertSame(pinned.snapshot, lease.getSnapshot()); + + lease.close(); + lease.close(); + assertThrows(IllegalStateException.class, lease::getSnapshot); + + assertEquals(0, gate.getActiveLeaseCount()); + assertTrue(gate.isDrained()); + verify(pinned.history, times(1)).close(); + verify(pinned.latest, times(1)).close(); + verify(pinned.serving, times(1)).close(); + gate.close(); + gate.close(); + assertEquals(State.CLOSED, gate.getState()); + } + + @Test + public void failedPinAndFailedSnapshotCloseDoNotLeakLeaseAccounting() throws Exception { + AtomicInteger calls = new AtomicInteger(); + PinnedSnapshot pinned = snapshot(); + doThrow(new IOException("injected close failure")).when(pinned.history).close(); + ArchiveRuntimeQueryGate gate = new ArchiveRuntimeQueryGate(target -> { + if (calls.getAndIncrement() == 0) { + throw new IOException("injected pin failure"); + } + return pinned.snapshot; + }); + + assertThrows(IOException.class, () -> gate.pin(0)); + assertEquals(0, gate.getActiveLeaseCount()); + Lease lease = gate.pin(0); + assertEquals(1, gate.getActiveLeaseCount()); + + assertThrows(IOException.class, lease::close); + + assertTrue(gate.isDrained()); + verify(pinned.history).close(); + verify(pinned.latest).close(); + verify(pinned.serving).close(); + gate.close(); + } + + @Test + public void inFlightPinCompletesBeforeConcurrentQuiesce() throws Exception { + PinnedSnapshot pinned = snapshot(); + CountDownLatch pinEntered = new CountDownLatch(1); + CountDownLatch allowPin = new CountDownLatch(1); + ArchiveRuntimeQueryGate gate = new ArchiveRuntimeQueryGate(target -> { + pinEntered.countDown(); + try { + if (!allowPin.await(5, TimeUnit.SECONDS)) { + throw new IOException("pin release timed out"); + } + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IOException("pin interrupted", failure); + } + return pinned.snapshot; + }); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future pin = executor.submit(() -> gate.pin(0)); + assertTrue(pinEntered.await(5, TimeUnit.SECONDS)); + Future quiesce = executor.submit(gate::quiesce); + + assertFalse(quiesce.isDone()); + allowPin.countDown(); + Lease lease = pin.get(5, TimeUnit.SECONDS); + quiesce.get(5, TimeUnit.SECONDS); + + assertEquals(State.QUIESCING, gate.getState()); + assertEquals(1, gate.getActiveLeaseCount()); + assertThrows(IllegalStateException.class, () -> gate.pin(0)); + lease.close(); + assertTrue(gate.isDrained()); + gate.close(); + } finally { + executor.shutdownNow(); + } + } + + private static PinnedSnapshot snapshot() throws IOException { + byte[] hash = new byte[32]; + ServingKeyIndex serving = mock(ServingKeyIndex.class); + when(serving.getIndexedFrom()).thenReturn(0L); + when(serving.getIndexedThrough()).thenReturn(0L); + when(serving.getHeadHash()).thenReturn(hash); + when(serving.getAuthoritativePrefixDigest()).thenReturn(new byte[0]); + PinnedLatestState latest = mock(PinnedLatestState.class); + when(latest.getBlockNumber()).thenReturn(0L); + when(latest.getBlockHash()).thenReturn(hash); + PinnedHistory history = mock(PinnedHistory.class); + when(history.getIndexedFrom()).thenReturn(0L); + when(history.getIndexedThrough()).thenReturn(0L); + when(history.getHeadHash()).thenReturn(hash); + when(history.getAuthoritativePrefixDigest()).thenReturn(new byte[0]); + return new PinnedSnapshot( + ArchiveReadSnapshot.pin(0, 0, hash, serving, latest, history), serving, latest, history); + } + + private static final class PinnedSnapshot { + private final ArchiveReadSnapshot snapshot; + private final ServingKeyIndex serving; + private final PinnedLatestState latest; + private final PinnedHistory history; + + private PinnedSnapshot(ArchiveReadSnapshot snapshot, ServingKeyIndex serving, + PinnedLatestState latest, PinnedHistory history) { + this.snapshot = snapshot; + this.serving = serving; + this.latest = latest; + this.history = history; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java index 1b6559358af..5d90202d149 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java @@ -3,10 +3,13 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.google.protobuf.ByteString; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -22,8 +25,11 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; +import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; import org.tron.core.db2.archive.ArchiveTargetApplyCoordinator.Stage; import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; +import org.tron.protos.Protocol.Account; public class ArchiveTargetApplyCoordinatorTest { @@ -121,6 +127,192 @@ public void everyDurableStageFailureConvergesThroughFreshRecovery() throws Excep } } + @Test + public void p66PlansRecoverOnlyRemainingNativeParticipantAfterFreshReopen() throws Exception { + for (Phase phase : Arrays.asList(Phase.P66_ACTIVATION, Phase.P66_ON)) { + for (boolean failAfterAccount : Arrays.asList(false, true)) { + String boundary = failAfterAccount ? "after-account" : "after-checkpoint"; + try (Fixture fixture = fixture("p66-" + phase.name().toLowerCase() + "-" + boundary)) { + byte[] address = accountAddress(7); + byte[] accountValue = canonicalAccount(address, + phase == Phase.P66_ACTIVATION ? 2_000L : 3_000L); + byte[] assetKey = new P66AccountAssetCodec().assetPhysicalKey(address, "1000007"); + byte[] assetValue = ByteBuffer.allocate(Long.BYTES) + .putLong(phase == Phase.P66_ACTIVATION ? 30L : 40L).array(); + Map> mutations = + p66Mutations(address, accountValue, assetKey, assetValue); + ArchiveTargetApplyCoordinator.FaultHook failure = (stage, participant) -> { + if (!failAfterAccount && stage == Stage.AFTER_CHECKPOINT + || failAfterAccount && stage == Stage.AFTER_PARTICIPANT + && "account".equals(participant)) { + throw new IOException("injected " + boundary); + } + }; + + try (HistoryCommitStore history = fixture.openHistory()) { + ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, + action -> action.run(), failure, temporary -> { }); + assertThrows(IOException.class, + () -> coordinator.apply(1, phase, mutations, () -> { })); + } + + ArchiveTargetMutationPlan durablePlan = + new ArchiveTargetMutationPlanFile(fixture.checkpointPath).loadRequired(); + byte[] planDigest = durablePlan.digest(); + assertEquals(phase, durablePlan.getTargetPhase()); + assertArrayEquals(hash(1), durablePlan.getTarget().getBlockHash()); + assertArrayEquals(planDigest, fixture.checkpoint().getMutationPlanDigest()); + + fixture.reopenParticipants(); + AtomicInteger refreshes = new AtomicInteger(); + RecoveryPlan recoveryPlan; + try (ArchiveParticipantRecoveryStorage recovery = + new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, + action -> action.run(), refreshes::incrementAndGet)) { + recoveryPlan = new ArchiveRecoveryExecutor(recovery).recover(); + } + + List replayed = new ArrayList<>(); + recoveryPlan.getActions().stream() + .filter(action -> action.getType() == ActionType.REPLAY_PARTICIPANT) + .forEach(action -> replayed.add(action.getParticipant())); + assertEquals(failAfterAccount + ? Collections.singletonList("account-asset") : PARTICIPANTS, replayed); + assertEquals(ActionType.PUBLISH_READER_HEAD, + recoveryPlan.getActions().get(recoveryPlan.getActions().size() - 1).getType()); + assertEquals(1, refreshes.get()); + assertArrayEquals(accountValue, fixture.account.get(address)); + assertArrayEquals(assetValue, fixture.asset.get(assetKey)); + assertP66Authority(fixture, planDigest); + assertFalse(Files.exists( + new ArchiveTargetMutationPlanFile(fixture.checkpointPath).getPath())); + + fixture.reopenParticipants(); + try (ArchiveParticipantRecoveryStorage fixed = + new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { + assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); + } + assertArrayEquals(accountValue, fixture.account.get(address)); + assertArrayEquals(assetValue, fixture.asset.get(assetKey)); + assertP66Authority(fixture, planDigest); + } + } + } + } + + @Test + public void p66ReaderDurableCrashReopenRetiresPlanWithoutBusinessReplay() throws Exception { + for (Phase phase : Arrays.asList(Phase.P66_ACTIVATION, Phase.P66_ON)) { + try (Fixture fixture = fixture("p66-reader-durable-" + phase.name().toLowerCase())) { + P66Vector vector = p66Vector(phase); + try (HistoryCommitStore history = fixture.openHistory()) { + ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, + action -> action.run(), failAt(Stage.AFTER_READER, + "injected after durable reader publication"), temporary -> { }); + assertThrows(IOException.class, + () -> coordinator.apply(1, phase, vector.mutations, () -> { })); + } + + ArchiveTargetMutationPlan durablePlan = + new ArchiveTargetMutationPlanFile(fixture.checkpointPath).loadRequired(); + byte[] planDigest = durablePlan.digest(); + assertEquals(phase, durablePlan.getTargetPhase()); + assertP66BusinessAndAuthority(fixture, vector, planDigest); + + fixture.reopenParticipants(); + AtomicInteger refreshes = new AtomicInteger(); + try (ArchiveParticipantRecoveryStorage recovery = + new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, + action -> action.run(), refreshes::incrementAndGet)) { + assertEquals(0, new ArchiveRecoveryExecutor(recovery).recover().getActions().size()); + } + assertEquals(0, refreshes.get()); + assertFalse(Files.exists( + new ArchiveTargetMutationPlanFile(fixture.checkpointPath).getPath())); + assertP66BusinessAndAuthority(fixture, vector, planDigest); + + fixture.reopenParticipants(); + try (ArchiveParticipantRecoveryStorage fixed = + new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { + assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); + } + assertP66BusinessAndAuthority(fixture, vector, planDigest); + } + } + } + + @Test + public void p66RecoveryCrashReopenReplaysOnlySecondNativeParticipant() throws Exception { + for (Phase phase : Arrays.asList(Phase.P66_ACTIVATION, Phase.P66_ON)) { + try (Fixture fixture = fixture("p66-recovery-crash-" + phase.name().toLowerCase())) { + P66Vector vector = p66Vector(phase); + try (HistoryCommitStore history = fixture.openHistory()) { + ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, + action -> action.run(), failAt(Stage.AFTER_CHECKPOINT, + "injected after checkpoint"), temporary -> { }); + assertThrows(IOException.class, + () -> coordinator.apply(1, phase, vector.mutations, () -> { })); + } + ArchiveTargetMutationPlan durablePlan = + new ArchiveTargetMutationPlanFile(fixture.checkpointPath).loadRequired(); + byte[] planDigest = durablePlan.digest(); + assertEquals(phase, durablePlan.getTargetPhase()); + + fixture.reopenParticipants(); + try (ArchiveParticipantRecoveryStorage recovery = + new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { + assertThrows(ArchivePersistenceException.class, + () -> new ArchiveRecoveryExecutor(recovery, action -> { + if (action.getType() == ActionType.REPLAY_PARTICIPANT + && "account".equals(action.getParticipant())) { + throw new IOException("injected after recovered account"); + } + }).recover()); + } + assertArrayEquals(vector.accountValue, fixture.account.get(vector.address)); + assertNull(fixture.asset.get(vector.assetKey)); + assertEquals(1L, fixture.account.loadProgress().getEpoch()); + assertEquals(0L, fixture.asset.loadProgress().getEpoch()); + assertEquals(0L, fixture.reader().getEpoch()); + assertArrayEquals(planDigest, + fixture.account.loadProgress().getMutationPlanDigest()); + + fixture.reopenParticipants(); + RecoveryPlan recoveryPlan; + try (ArchiveParticipantRecoveryStorage recovery = + new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { + recoveryPlan = new ArchiveRecoveryExecutor(recovery).recover(); + } + assertEquals(2, recoveryPlan.getActions().size()); + assertEquals(ActionType.REPLAY_PARTICIPANT, + recoveryPlan.getActions().get(0).getType()); + assertEquals("account-asset", recoveryPlan.getActions().get(0).getParticipant()); + assertEquals(ActionType.PUBLISH_READER_HEAD, + recoveryPlan.getActions().get(1).getType()); + assertP66BusinessAndAuthority(fixture, vector, planDigest); + assertFalse(Files.exists( + new ArchiveTargetMutationPlanFile(fixture.checkpointPath).getPath())); + + fixture.reopenParticipants(); + try (ArchiveParticipantRecoveryStorage fixed = + new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, + fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { + assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); + } + assertP66BusinessAndAuthority(fixture, vector, planDigest); + } + } + } + private Fixture fixture(String name) throws Exception { return new Fixture(temporaryFolder.newFolder(name).toPath()); } @@ -134,6 +326,68 @@ private static Map> plans() { return plans; } + private static Map> p66Mutations(byte[] address, + byte[] accountValue, byte[] assetKey, byte[] assetValue) { + Map> mutations = new LinkedHashMap<>(); + mutations.put("account", Collections.singletonList( + ArchiveParticipantMutation.put(address, accountValue))); + mutations.put("account-asset", Collections.singletonList( + ArchiveParticipantMutation.put(assetKey, assetValue))); + return mutations; + } + + private static byte[] accountAddress(int suffix) { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = (byte) suffix; + return address; + } + + private static byte[] canonicalAccount(byte[] address, long balance) { + return Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .setAssetOptimized(true).setBalance(balance).build().toByteArray(); + } + + private static P66Vector p66Vector(Phase phase) { + byte[] address = accountAddress(7); + byte[] accountValue = canonicalAccount(address, + phase == Phase.P66_ACTIVATION ? 2_000L : 3_000L); + byte[] assetKey = new P66AccountAssetCodec().assetPhysicalKey(address, "1000007"); + byte[] assetValue = ByteBuffer.allocate(Long.BYTES) + .putLong(phase == Phase.P66_ACTIVATION ? 30L : 40L).array(); + return new P66Vector(address, accountValue, assetKey, assetValue, + p66Mutations(address, accountValue, assetKey, assetValue)); + } + + private static void assertP66BusinessAndAuthority(Fixture fixture, P66Vector vector, + byte[] planDigest) throws IOException { + assertArrayEquals(vector.accountValue, fixture.account.get(vector.address)); + assertArrayEquals(vector.assetValue, fixture.asset.get(vector.assetKey)); + assertP66Authority(fixture, planDigest); + } + + private static void assertP66Authority(Fixture fixture, byte[] planDigest) throws IOException { + ArchiveProgressEnvelope checkpoint = fixture.checkpoint(); + ArchiveProgressEnvelope reader = fixture.reader(); + assertEquals(1L, checkpoint.getEpoch()); + assertEquals(1L, reader.getEpoch()); + assertArrayEquals(hash(1), checkpoint.getBlockHash()); + assertArrayEquals(hash(1), reader.getBlockHash()); + assertArrayEquals(planDigest, checkpoint.getMutationPlanDigest()); + assertArrayEquals(planDigest, fixture.account.loadProgress().getMutationPlanDigest()); + assertArrayEquals(planDigest, fixture.asset.loadProgress().getMutationPlanDigest()); + assertArrayEquals(planDigest, reader.getMutationPlanDigest()); + } + + private static ArchiveTargetApplyCoordinator.FaultHook failAt(Stage expected, + String message) { + return (stage, participant) -> { + if (stage == expected) { + throw new IOException(message); + } + }; + } + private static void failAfterStage(FailurePoint point, Stage stage, String participant) throws IOException { if (point == FailurePoint.AFTER_CHECKPOINT && stage == Stage.AFTER_CHECKPOINT @@ -168,12 +422,29 @@ private boolean isPlanFailure() { } } + private static final class P66Vector { + private final byte[] address; + private final byte[] accountValue; + private final byte[] assetKey; + private final byte[] assetValue; + private final Map> mutations; + + private P66Vector(byte[] address, byte[] accountValue, byte[] assetKey, byte[] assetValue, + Map> mutations) { + this.address = address; + this.accountValue = accountValue; + this.assetKey = assetKey; + this.assetValue = assetValue; + this.mutations = mutations; + } + } + private static final class Fixture implements AutoCloseable { private final Path archive; private final Path checkpointPath; private final Path readerPath; - private final LevelDbArchiveParticipant account; - private final RocksDbArchiveParticipant asset; + private LevelDbArchiveParticipant account; + private RocksDbArchiveParticipant asset; private final ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); private Fixture(Path archive) throws Exception { @@ -185,14 +456,33 @@ private Fixture(Path archive) throws Exception { global(Kind.APPLY_CHECKPOINT, markers.get(0))); new ArchiveProgressFile(readerPath, codec).store( global(Kind.READER_VISIBLE, markers.get(0))); - account = new LevelDbArchiveParticipant( - archive.resolve("participants/account"), "account", PARTICIPANTS); - asset = new RocksDbArchiveParticipant( - archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS); + openParticipants(); account.apply(Collections.emptyList(), participant("account", markers.get(0))); asset.apply(Collections.emptyList(), participant("account-asset", markers.get(0))); } + private void reopenParticipants() throws IOException { + closeParticipants(); + openParticipants(); + } + + private void openParticipants() throws IOException { + account = new LevelDbArchiveParticipant( + archive.resolve("participants/account"), "account", PARTICIPANTS); + try { + asset = new RocksDbArchiveParticipant( + archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS); + } catch (IOException | RuntimeException failure) { + account.close(); + throw failure; + } + } + + private void closeParticipants() throws IOException { + asset.close(); + account.close(); + } + private HistoryCommitStore openHistory() throws IOException { return new HistoryCommitStore(archive, new HistoryCommitMarkerCodec()); } @@ -214,8 +504,7 @@ private ArchiveProgressEnvelope reader() throws IOException { @Override public void close() throws IOException { - asset.close(); - account.close(); + closeParticipants(); } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java index b1f656cc8b4..65e4c509dd3 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java @@ -83,16 +83,16 @@ public void rejectsTargetIdentityAndExactParticipantSetMismatch() { new ArchiveParticipantMutationBatch(incompleteTarget, Phase.P66_ON, Collections.emptyList()))); - List oldExact27 = new ArrayList<>(participants()); - oldExact27.add("abi"); - Collections.sort(oldExact27); - HistoryCommitMarker oldTarget = marker(1, oldExact27); + List abiExcludedExact26 = new ArrayList<>(participants()); + abiExcludedExact26.remove("abi"); + HistoryCommitMarker oldTarget = marker(1, abiExcludedExact26); assertThrows(ArchivePersistenceException.class, () -> new ArchiveTargetMutationPlanBuilder().build(oldTarget, new ArchiveParticipantMutationBatch(oldTarget, Phase.P66_ON, Collections.emptyList()))); List v2OnlyExact25 = new ArrayList<>(participants()); + v2OnlyExact25.remove("abi"); v2OnlyExact25.remove("asset-issue"); HistoryCommitMarker v2OnlyTarget = marker(1, v2OnlyExact25); assertThrows(ArchivePersistenceException.class, diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java index 2e42d0255e5..f6917319200 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanFileTest.java @@ -8,6 +8,7 @@ import com.google.common.hash.Hashing; import com.google.common.io.BaseEncoding; +import com.google.protobuf.ByteString; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Files; @@ -23,6 +24,8 @@ import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; import org.tron.core.db2.archive.ArchiveTargetMutationPlanFile.Stage; import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; +import org.tron.protos.Protocol.Account; +import org.tron.protos.Protocol.AccountType; public class ArchiveTargetMutationPlanFileTest { @@ -151,6 +154,47 @@ public void canonicalizesContainerOrderAndRejectsDuplicatePhysicalKeys() { Phase.P66_ON, second)); } + @Test + public void reopensFixedP66TransitionMutationPlanVectors() throws Exception { + byte[] address = accountAddress(7); + String tokenId = "1000007"; + P66AccountAssetCodec codec = new P66AccountAssetCodec(); + byte[] directKey = codec.assetPhysicalKey(address, tokenId); + ArchiveTargetMutationPlan[] vectors = { + transitionPlan(1, Phase.P66_OFF, address, + account(address, false, tokenId, 20L, 1_000L), null, null), + transitionPlan(2, Phase.P66_ACTIVATION, address, + account(address, true, null, 0L, 2_000L), directKey, + ByteBuffer.allocate(Long.BYTES).putLong(30L).array()), + transitionPlan(3, Phase.P66_ON, address, + account(address, true, null, 0L, 3_000L), directKey, + ByteBuffer.allocate(Long.BYTES).putLong(40L).array()) + }; + + for (ArchiveTargetMutationPlan expected : vectors) { + Path checkpoint = temporaryFolder.newFolder( + "p66-plan-" + expected.getTargetPhase().name().toLowerCase()) + .toPath().resolve("checkpoint.progress"); + new ArchiveTargetMutationPlanFile(checkpoint).store(expected); + + ArchiveTargetMutationPlan reopened = + new ArchiveTargetMutationPlanFile(checkpoint).loadRequired(); + assertEquals(expected.getTarget().getEpoch(), reopened.getTarget().getEpoch()); + assertArrayEquals(expected.getTarget().getBlockHash(), + reopened.getTarget().getBlockHash()); + assertArrayEquals(expected.getTarget().getBatchId(), reopened.getTarget().getBatchId()); + assertArrayEquals(expected.getTarget().getPayloadDigest(), + reopened.getTarget().getPayloadDigest()); + assertEquals(expected.getAccountAssetFormatId(), reopened.getAccountAssetFormatId()); + assertEquals(expected.getTargetPhase(), reopened.getTargetPhase()); + assertMutationsEqual(expected.getMutations("account"), + reopened.getMutations("account")); + assertMutationsEqual(expected.getMutations("account-asset"), + reopened.getMutations("account-asset")); + assertArrayEquals(expected.digest(), reopened.digest()); + } + } + private static ArchiveTargetMutationPlan plan(long epoch) { ArchiveProgressEnvelope target = new ArchiveProgressEnvelope(Kind.APPLY_CHECKPOINT, null, epoch, bytes(32, (int) epoch), bytes(16, (int) epoch + 10), @@ -164,6 +208,46 @@ epoch, bytes(32, (int) epoch), bytes(16, (int) epoch + 10), Phase.P66_ON, mutations); } + private static ArchiveTargetMutationPlan transitionPlan(long epoch, Phase phase, + byte[] address, byte[] accountValue, byte[] directKey, byte[] directValue) { + ArchiveProgressEnvelope target = new ArchiveProgressEnvelope(Kind.APPLY_CHECKPOINT, null, + epoch, bytes(32, (int) epoch), bytes(16, (int) epoch + 10), + bytes(32, (int) epoch + 20), PARTICIPANTS); + Map> mutations = new LinkedHashMap<>(); + mutations.put("account", Collections.singletonList( + ArchiveParticipantMutation.put(address, accountValue))); + mutations.put("account-asset", directKey == null ? Collections.emptyList() + : Collections.singletonList(ArchiveParticipantMutation.put(directKey, directValue))); + return new ArchiveTargetMutationPlan(target, P66AccountAssetCodec.FORMAT_ID, + phase, mutations); + } + + private static byte[] accountAddress(int suffix) { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = (byte) suffix; + return address; + } + + private static byte[] account(byte[] address, boolean optimized, String tokenId, + long assetBalance, long balance) { + Account.Builder builder = Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .setType(AccountType.Normal).setAssetOptimized(optimized).setBalance(balance); + if (tokenId != null) { + builder.putAsset("asset-name", assetBalance).putAssetV2(tokenId, assetBalance); + } + return builder.build().toByteArray(); + } + + private static void assertMutationsEqual(List expected, + List actual) { + assertEquals(expected.size(), actual.size()); + for (int i = 0; i < expected.size(); i++) { + assertArrayEquals(expected.get(i).getKey(), actual.get(i).getKey()); + assertArrayEquals(expected.get(i).getValue(), actual.get(i).getValue()); + } + } + private static byte[] bytes(int length, int value) { byte[] bytes = new byte[length]; Arrays.fill(bytes, (byte) value); diff --git a/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java b/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java index 807c77470a8..2ee5dab1c76 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/AsyncArchiveHistorySinkTest.java @@ -59,8 +59,8 @@ public void persistsOneFlushRangeAsOneDurabilityBatch() throws Exception { } @Test - public void createsReceiptFromTheSameDurableWriterAuthority() throws Exception { - Path archive = temporaryFolder.newFolder("async-receipt").toPath(); + public void createsEvidenceFromTheSameDurableWriterAuthority() throws Exception { + Path archive = temporaryFolder.newFolder("async-evidence").toPath(); ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, ArchiveStoreScope.getStateDatabases()); try (AsyncArchiveHistorySink sink = new AsyncArchiveHistorySink(writer, 1)) { @@ -68,11 +68,11 @@ public void createsReceiptFromTheSameDurableWriterAuthority() throws Exception { sink.accept(committed); sink.awaitCommitted(1); - List receipt = sink.createMarkerRangeReceipt(1) + List evidence = sink.createMarkerRangeEvidence(1) .read(Collections.singletonList(committed.getMeta())); - assertEquals(1, receipt.size()); - assertEquals(committed.getMeta(), receipt.get(0).getMeta()); + assertEquals(1, evidence.size()); + assertEquals(committed.getMeta(), evidence.get(0).getMeta()); } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/CommittedHistoryAuthorityTest.java b/framework/src/test/java/org/tron/core/db2/archive/CommittedHistoryAuthorityTest.java new file mode 100644 index 00000000000..db962f601d0 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/CommittedHistoryAuthorityTest.java @@ -0,0 +1,69 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class CommittedHistoryAuthorityTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void writerAndStoreExposeTheSameReadOnlyCommittedAuthority() throws Exception { + Path archive = temporaryFolder.newFolder("committed-authority").toPath(); + Path reader = archive.resolve("progress/reader.progress"); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1_000L); + + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( + archive, 4096, ArchiveStoreScope.getStateDatabases())) { + writer.accept(new BlockReverseDiff(meta, Collections.emptyList())); + assertAuthority(writer, meta); + + new ArchiveReaderHeadPublisher(writer, reader, participants()).publish(1); + ArchiveProgressEnvelope published = new ArchiveProgressFile(reader, + new ArchiveProgressEnvelopeCodec()).load(); + assertEquals(1L, published.getEpoch()); + assertArrayEquals(meta.getBlockHash(), published.getBlockHash()); + } + + try (HistoryCommitStore store = new HistoryCommitStore( + archive, new HistoryCommitMarkerCodec())) { + assertAuthority(store, meta); + } + } + + private static void assertAuthority(CommittedHistoryAuthority authority, + BlockSnapshotMeta expected) { + assertEquals(1L, authority.firstEpoch()); + assertNotNull(authority.head()); + assertEquals(expected, authority.head().getMeta()); + assertEquals(expected, authority.get(1).getMeta()); + HistoryCoverage coverage = authority.coverage(); + assertNotNull(coverage); + assertEquals(1L, coverage.getFirstEpoch()); + assertEquals(1L, coverage.getRecordCount()); + assertEquals(1L, coverage.getHeadEpoch()); + assertArrayEquals(expected.getBlockHash(), coverage.getHeadHash()); + } + + private static List participants() { + List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(participants); + return participants; + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceiptTest.java b/framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeEvidenceTest.java similarity index 72% rename from framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceiptTest.java rename to framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeEvidenceTest.java index ee376db75c5..68af89f8f86 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeReceiptTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeEvidenceTest.java @@ -15,33 +15,33 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -public class DurableHistoryMarkerRangeReceiptTest { +public class DurableHistoryMarkerRangeEvidenceTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void readsOnlyExactDurableRangeAndReopensWithIdenticalReceipt() throws Exception { - Path archive = temporaryFolder.newFolder("marker-receipt").toPath(); + public void readsOnlyExactDurableRangeAndReopensWithIdenticalEvidence() throws Exception { + Path archive = temporaryFolder.newFolder("marker-evidence").toPath(); List expected = Arrays.asList(meta(2), meta(3)); List encoded = new ArrayList<>(); try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( archive, 4096, new java.util.LinkedHashSet<>(participants()))) { writer.acceptAll(Arrays.asList(diff(1), diff(2), diff(3), diff(4))); - List receipt = - writer.createMarkerRangeReceipt(2).read(expected); - assertEquals(Arrays.asList(2L, 3L), epochs(receipt)); + List evidence = + writer.createMarkerRangeEvidence(2).read(expected); + assertEquals(Arrays.asList(2L, 3L), epochs(evidence)); HistoryCommitMarkerCodec codec = new HistoryCommitMarkerCodec(); - receipt.forEach(marker -> encoded.add(codec.encode(marker))); + evidence.forEach(marker -> encoded.add(codec.encode(marker))); } try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter( archive, 4096, new java.util.LinkedHashSet<>(participants()))) { - List receipt = - reopened.createMarkerRangeReceipt(2).read(expected); + List evidence = + reopened.createMarkerRangeEvidence(2).read(expected); HistoryCommitMarkerCodec codec = new HistoryCommitMarkerCodec(); - assertArrayEquals(encoded.get(0), codec.encode(receipt.get(0))); - assertArrayEquals(encoded.get(1), codec.encode(receipt.get(1))); + assertArrayEquals(encoded.get(0), codec.encode(evidence.get(0))); + assertArrayEquals(encoded.get(1), codec.encode(evidence.get(1))); assertEquals(4L, reopened.committedHead().getMeta().getEpoch()); } } @@ -50,62 +50,62 @@ public void readsOnlyExactDurableRangeAndReopensWithIdenticalReceipt() throws Ex public void markerPreflightRejectsMissingSubstitutedAndReorderedBeforeBodyRead() { FakeSource source = new FakeSource(); source.put(marker(meta(1)), diff(1)); - DurableHistoryMarkerRangeReceipt receipt = - new DurableHistoryMarkerRangeReceipt(source, 2); + DurableHistoryMarkerRangeEvidence evidence = + new DurableHistoryMarkerRangeEvidence(source, 2); List expected = Arrays.asList(meta(1), meta(2)); - assertThrows(ArchivePersistenceException.class, () -> receipt.read(expected)); + assertThrows(ArchivePersistenceException.class, () -> evidence.read(expected)); assertEquals(0, source.bodyReads); source.putAt(2, marker(meta(3)), diff(2)); - assertThrows(ArchivePersistenceException.class, () -> receipt.read(expected)); + assertThrows(ArchivePersistenceException.class, () -> evidence.read(expected)); assertEquals(0, source.bodyReads); source.putAt(1, marker(meta(2)), diff(1)); source.putAt(2, marker(meta(1)), diff(2)); - assertThrows(ArchivePersistenceException.class, () -> receipt.read(expected)); + assertThrows(ArchivePersistenceException.class, () -> evidence.read(expected)); assertEquals(0, source.bodyReads); source.clear(); source.put(marker(meta(1)), diff(1)); source.put(marker(meta(2)), diff(2)); - assertEquals(Arrays.asList(1L, 2L), epochs(receipt.read(expected))); + assertEquals(Arrays.asList(1L, 2L), epochs(evidence.read(expected))); assertEquals(2, source.bodyReads); } @Test - public void referenceFailureAndMarkerDriftLeaveReceiptRetryable() { + public void referenceFailureAndMarkerDriftLeaveEvidenceRetryable() { FakeSource source = new FakeSource(); source.put(marker(meta(1)), diff(1)); source.failBody = true; - DurableHistoryMarkerRangeReceipt receipt = - new DurableHistoryMarkerRangeReceipt(source, 1); + DurableHistoryMarkerRangeEvidence evidence = + new DurableHistoryMarkerRangeEvidence(source, 1); assertThrows(ArchivePersistenceException.class, - () -> receipt.read(Collections.singletonList(meta(1)))); + () -> evidence.read(Collections.singletonList(meta(1)))); source.failBody = false; - assertEquals(1, receipt.read(Collections.singletonList(meta(1))).size()); + assertEquals(1, evidence.read(Collections.singletonList(meta(1))).size()); source.bodyReads = 0; source.driftAfterBody = true; assertThrows(ArchivePersistenceException.class, - () -> receipt.read(Collections.singletonList(meta(1)))); + () -> evidence.read(Collections.singletonList(meta(1)))); source.driftAfterBody = false; - assertEquals(1, receipt.read(Collections.singletonList(meta(1))).size()); + assertEquals(1, evidence.read(Collections.singletonList(meta(1))).size()); } @Test public void invalidOrOversizedExpectedRangeFailsBeforeSourceAction() { FakeSource source = new FakeSource(); - DurableHistoryMarkerRangeReceipt receipt = - new DurableHistoryMarkerRangeReceipt(source, 1); + DurableHistoryMarkerRangeEvidence evidence = + new DurableHistoryMarkerRangeEvidence(source, 1); assertThrows(ArchivePersistenceException.class, - () -> receipt.read(Collections.emptyList())); + () -> evidence.read(Collections.emptyList())); assertThrows(ArchivePersistenceException.class, - () -> receipt.read(Arrays.asList(meta(1), meta(2)))); + () -> evidence.read(Arrays.asList(meta(1), meta(2)))); assertThrows(ArchivePersistenceException.class, - () -> new DurableHistoryMarkerRangeReceipt(source, 2) + () -> new DurableHistoryMarkerRangeEvidence(source, 2) .read(Arrays.asList(meta(1), meta(3)))); assertEquals(0, source.markerReads); assertEquals(0, source.bodyReads); @@ -151,7 +151,7 @@ private static byte[] bytes(int length, int value) { return result; } - private static final class FakeSource implements DurableHistoryMarkerRangeReceipt.Source { + private static final class FakeSource implements DurableHistoryMarkerRangeEvidence.Source { private final Map markers = new LinkedHashMap<>(); private final Map bodies = new LinkedHashMap<>(); private int markerReads; @@ -179,7 +179,7 @@ private void clear() { public HistoryCommitMarker marker(long epoch) { markerReads++; if (driftAfterBody && bodyReads > 0) { - return DurableHistoryMarkerRangeReceiptTest.marker(meta((int) epoch + 1)); + return DurableHistoryMarkerRangeEvidenceTest.marker(meta((int) epoch + 1)); } return markers.get(epoch); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolverTest.java b/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolverTest.java new file mode 100644 index 00000000000..1f5d8b14fb2 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolverTest.java @@ -0,0 +1,526 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.OptionalLong; +import org.junit.Test; +import org.tron.common.utils.ByteArray; +import org.tron.core.db2.archive.HistoricalAccountAssetBalanceResolver.Result; +import org.tron.core.db2.archive.HistoricalAccountAssetPrefixResolver.Balance; +import org.tron.core.db2.archive.HistoricalAccountAssetPrefixResolver.Limits; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; +import org.tron.protos.Protocol.Account; +import org.tron.protos.Protocol.AccountType; + +public class HistoricalAccountAssetBalanceResolverTest { + + private static final String TOKEN_ID = "1000001"; + + @Test + public void resolvesP66OffBalanceOnlyFromHistoricalAccountMap() throws Exception { + byte[] address = address(1); + Fixture fixture = new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(0L)) + .put("account", address, account(address, false, TOKEN_ID, 17L)); + + try (ArchiveReadSnapshot snapshot = fixture.snapshot()) { + Result result = new HistoricalAccountAssetBalanceResolver() + .resolve(snapshot, address, TOKEN_ID); + assertTrue(result.isAccountPresent()); + assertEquals(17L, result.getBalance()); + assertEquals(Phase.P66_OFF, result.getPhase()); + assertEquals(7L, result.getBlockNumber()); + assertEquals(Arrays.asList("properties", "account", "account-asset"), fixture.reads); + } + } + + @Test + public void resolvesP66OnBalanceOnlyFromExactDirectRow() throws Exception { + byte[] address = address(2); + byte[] directKey = new P66AccountAssetCodec().assetPhysicalKey(address, TOKEN_ID); + Fixture fixture = new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, account(address, true, null, 0L)) + .put("account-asset", directKey, ByteArray.fromLong(29L)); + + try (ArchiveReadSnapshot snapshot = fixture.snapshot()) { + Result result = new HistoricalAccountAssetBalanceResolver() + .resolve(snapshot, address, TOKEN_ID); + assertTrue(result.isAccountPresent()); + assertEquals(29L, result.getBalance()); + assertEquals(Phase.P66_ON, result.getPhase()); + } + } + + @Test + public void preservesSemanticAbsenceForMissingAccountAndZeroBalance() throws Exception { + byte[] existing = address(3); + byte[] missing = address(4); + Fixture fixture = new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", existing, account(existing, true, null, 0L)); + + try (ArchiveReadSnapshot snapshot = fixture.snapshot()) { + HistoricalAccountAssetBalanceResolver resolver = + new HistoricalAccountAssetBalanceResolver(); + Result zero = resolver.resolve(snapshot, existing, TOKEN_ID); + assertTrue(zero.isAccountPresent()); + assertEquals(0L, zero.getBalance()); + + Result absent = resolver.resolve(snapshot, missing, TOKEN_ID); + assertFalse(absent.isAccountPresent()); + assertThrows(IllegalStateException.class, absent::getBalance); + } + } + + @Test + public void rejectsMissingOrMalformedHistoricalProposal66Property() throws Exception { + byte[] address = address(5); + Fixture missing = new Fixture().put("account", address, + account(address, false, TOKEN_ID, 1L)); + try (ArchiveReadSnapshot snapshot = missing.snapshot()) { + assertThrows(ArchivePersistenceException.class, + () -> new HistoricalAccountAssetBalanceResolver() + .resolve(snapshot, address, TOKEN_ID)); + } + + for (byte[] invalid : Arrays.asList(new byte[]{1}, ByteArray.fromLong(2L))) { + Fixture malformed = new Fixture() + .put("properties", proposal66Key(), invalid) + .put("account", address, account(address, false, TOKEN_ID, 1L)); + try (ArchiveReadSnapshot snapshot = malformed.snapshot()) { + assertThrows(ArchivePersistenceException.class, + () -> new HistoricalAccountAssetBalanceResolver() + .resolve(snapshot, address, TOKEN_ID)); + } + } + } + + @Test + public void rejectsMixedAndOrphanPhysicalLayouts() throws Exception { + byte[] address = address(6); + byte[] directKey = new P66AccountAssetCodec().assetPhysicalKey(address, TOKEN_ID); + List invalid = Arrays.asList( + new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(0L)) + .put("account", address, account(address, false, TOKEN_ID, 3L)) + .put("account-asset", directKey, ByteArray.fromLong(3L)), + new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, account(address, false, TOKEN_ID, 3L)), + new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account-asset", directKey, ByteArray.fromLong(3L))); + + for (Fixture fixture : invalid) { + try (ArchiveReadSnapshot snapshot = fixture.snapshot()) { + assertThrows(ArchivePersistenceException.class, + () -> new HistoricalAccountAssetBalanceResolver() + .resolve(snapshot, address, TOKEN_ID)); + } + } + } + + @Test + public void rejectsCorruptWrongKeyAndNonCanonicalDirectValues() throws Exception { + byte[] address = address(7); + byte[] directKey = new P66AccountAssetCodec().assetPhysicalKey(address, TOKEN_ID); + List invalid = Arrays.asList( + new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, new byte[]{1, 2, 3}), + new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, account(address(8), true, null, 0L)), + new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, account(address, true, null, 0L)) + .put("account-asset", directKey, new byte[]{1}), + new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, account(address, true, null, 0L)) + .put("account-asset", directKey, ByteArray.fromLong(0L))); + + for (Fixture fixture : invalid) { + try (ArchiveReadSnapshot snapshot = fixture.snapshot()) { + assertThrows(ArchivePersistenceException.class, + () -> new HistoricalAccountAssetBalanceResolver() + .resolve(snapshot, address, TOKEN_ID)); + } + } + } + + @Test + public void rejectsUnsupportedTokenIdentitiesBeforeReadingStores() throws Exception { + byte[] address = address(9); + for (String tokenId : Arrays.asList("", "01", "asset-name", "12")) { + Fixture fixture = new Fixture(); + try (ArchiveReadSnapshot snapshot = fixture.snapshot()) { + assertThrows(ArchivePersistenceException.class, + () -> new HistoricalAccountAssetBalanceResolver() + .resolve(snapshot, address, tokenId)); + assertTrue(fixture.reads.isEmpty()); + } + } + } + + @Test + public void failsClosedWhenPinnedGenerationIdentityDriftsDuringResolution() throws Exception { + byte[] address = address(10); + Fixture fixture = new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, account(address, true, null, 0L)); + fixture.driftAfterThirdRead = true; + + try (ArchiveReadSnapshot snapshot = fixture.snapshot()) { + assertThrows(IllegalArgumentException.class, + () -> new HistoricalAccountAssetBalanceResolver() + .resolve(snapshot, address, TOKEN_ID)); + } + } + + @Test + public void prefixResolvesP66OffEmbeddedMapInCanonicalTokenOrder() throws Exception { + byte[] address = address(11); + Account account = Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .setType(AccountType.Normal).putAssetV2("1000002", 22L) + .putAssetV2("1000001", 11L).build(); + Fixture fixture = new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(0L)) + .put("account", address, account.toByteArray()); + + try (ArchiveReadSnapshot snapshot = fixture.snapshot()) { + HistoricalAccountAssetPrefixResolver.Result result = + new HistoricalAccountAssetPrefixResolver().resolve(snapshot, address, limits()); + assertEquals(Phase.P66_OFF, result.getPhase()); + assertTrue(result.isAccountPresent()); + assertBalances(result.getBalances(), "1000001", 11L, "1000002", 22L); + } + } + + @Test + public void prefixResolvesP66OnExactRowsAndRejectsMixedOrInvalidRows() throws Exception { + byte[] address = address(12); + P66AccountAssetCodec codec = new P66AccountAssetCodec(); + byte[] firstKey = codec.assetPhysicalKey(address, "1000001"); + byte[] secondKey = codec.assetPhysicalKey(address, "1000002"); + Fixture valid = new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, account(address, true, null, 0L)) + .put("account-asset", secondKey, ByteArray.fromLong(22L)) + .put("account-asset", firstKey, ByteArray.fromLong(11L)); + try (ArchiveReadSnapshot snapshot = valid.snapshot()) { + HistoricalAccountAssetPrefixResolver.Result result = + new HistoricalAccountAssetPrefixResolver().resolve(snapshot, address, limits()); + assertEquals(Phase.P66_ON, result.getPhase()); + assertBalances(result.getBalances(), "1000001", 11L, "1000002", 22L); + } + + List invalid = Arrays.asList( + new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(0L)) + .put("account", address, account(address, false, TOKEN_ID, 1L)) + .put("account-asset", firstKey, ByteArray.fromLong(1L)), + new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account-asset", firstKey, ByteArray.fromLong(1L)), + new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, account(address, true, null, 0L)) + .put("account-asset", concat(address, "01"), ByteArray.fromLong(1L)), + new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, account(address, true, null, 0L)) + .put("account-asset", firstKey, ByteArray.fromLong(0L))); + for (Fixture fixture : invalid) { + try (ArchiveReadSnapshot snapshot = fixture.snapshot()) { + assertThrows(ArchivePersistenceException.class, + () -> new HistoricalAccountAssetPrefixResolver() + .resolve(snapshot, address, limits())); + } + } + } + + @Test + public void prefixRejectsEveryOutputBudgetWithoutReturningPartialResults() throws Exception { + byte[] address = address(13); + P66AccountAssetCodec codec = new P66AccountAssetCodec(); + byte[] firstKey = codec.assetPhysicalKey(address, "1000001"); + byte[] secondKey = codec.assetPhysicalKey(address, "1000002"); + List rejected = Arrays.asList( + new Limits(10, 10, 1, 64, 8, 1_000), + new Limits(10, 10, 10, firstKey.length - 1, 8, 1_000), + new Limits(10, 10, 10, 64, 7, 1_000), + new Limits(10, 10, 10, 64, 8, + (long) firstKey.length + Long.BYTES + secondKey.length + Long.BYTES - 1)); + for (Limits limits : rejected) { + Fixture fixture = new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, account(address, true, null, 0L)) + .put("account-asset", firstKey, ByteArray.fromLong(11L)) + .put("account-asset", secondKey, ByteArray.fromLong(22L)); + try (ArchiveReadSnapshot snapshot = fixture.snapshot()) { + assertThrows(ArchiveQueryLimitExceededException.class, + () -> new HistoricalAccountAssetPrefixResolver() + .resolve(snapshot, address, limits)); + } + } + assertThrows(IllegalArgumentException.class, + () -> new Limits(0, 1, 1, 1, 1, 1)); + } + + @Test + public void prefixRejectsForeignUnsortedDuplicateAndGenerationDrift() throws Exception { + byte[] address = address(14); + byte[] firstKey = new P66AccountAssetCodec().assetPhysicalKey(address, "1000001"); + byte[] secondKey = new P66AccountAssetCodec().assetPhysicalKey(address, "1000002"); + Fixture foreign = prefixFixture(address, firstKey, secondKey); + foreign.put("account-asset", + new P66AccountAssetCodec().assetPhysicalKey(address(99), "1000003"), + ByteArray.fromLong(33L)); + foreign.foreignRange = true; + Fixture unsorted = prefixFixture(address, firstKey, secondKey); + unsorted.reverseRange = true; + Fixture duplicate = prefixFixture(address, firstKey, secondKey); + duplicate.duplicateRange = true; + for (Fixture fixture : Arrays.asList(foreign, unsorted, duplicate)) { + try (ArchiveReadSnapshot snapshot = fixture.snapshot()) { + assertThrows(IllegalArgumentException.class, + () -> new HistoricalAccountAssetPrefixResolver() + .resolve(snapshot, address, limits())); + } + } + + Fixture drift = prefixFixture(address, firstKey, secondKey); + drift.driftAfterRange = true; + try (ArchiveReadSnapshot snapshot = drift.snapshot()) { + assertThrows(IllegalArgumentException.class, + () -> new HistoricalAccountAssetPrefixResolver() + .resolve(snapshot, address, limits())); + } + } + + private static byte[] proposal66Key() { + return HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(); + } + + private static Fixture prefixFixture(byte[] address, byte[] firstKey, byte[] secondKey) { + return new Fixture() + .put("properties", proposal66Key(), ByteArray.fromLong(1L)) + .put("account", address, account(address, true, null, 0L)) + .put("account-asset", firstKey, ByteArray.fromLong(11L)) + .put("account-asset", secondKey, ByteArray.fromLong(22L)); + } + + private static Limits limits() { + return new Limits(10, 10, 10, 64, 8, 1_000); + } + + private static void assertBalances(List balances, String firstToken, + long firstBalance, String secondToken, long secondBalance) { + assertEquals(2, balances.size()); + assertEquals(firstToken, balances.get(0).getTokenId()); + assertEquals(firstBalance, balances.get(0).getBalance()); + assertEquals(secondToken, balances.get(1).getTokenId()); + assertEquals(secondBalance, balances.get(1).getBalance()); + } + + private static byte[] concat(byte[] address, String suffix) { + byte[] token = suffix.getBytes(java.nio.charset.StandardCharsets.US_ASCII); + byte[] key = Arrays.copyOf(address, address.length + token.length); + System.arraycopy(token, 0, key, address.length, token.length); + return key; + } + + private static byte[] account(byte[] address, boolean optimized, String tokenId, long balance) { + Account.Builder builder = Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .setType(AccountType.Normal).setAssetOptimized(optimized); + if (tokenId != null) { + builder.putAsset("asset-name", balance).putAssetV2(tokenId, balance); + } + return builder.build().toByteArray(); + } + + private static byte[] address(int suffix) { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = (byte) suffix; + return address; + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static final class Fixture { + private final List values = new ArrayList<>(); + private final List reads = new ArrayList<>(); + private final MutableServing serving = new MutableServing(); + private boolean driftAfterThirdRead; + private boolean driftAfterRange; + private boolean foreignRange; + private boolean reverseRange; + private boolean duplicateRange; + + private Fixture put(String dbName, byte[] key, byte[] value) { + values.add(new Value(dbName, key, value)); + return this; + } + + private ArchiveReadSnapshot snapshot() throws Exception { + ArchiveReadSnapshot.PinnedLatestState latest = new ArchiveReadSnapshot.PinnedLatestState() { + @Override + public long getBlockNumber() { + return 7L; + } + + @Override + public byte[] getBlockHash() { + return hash(7); + } + + @Override + public OldValue get(String dbName, byte[] rawKey) { + reads.add(dbName); + if (driftAfterThirdRead && reads.size() == 3) { + serving.drifted = true; + } + for (Value value : values) { + if (value.dbName.equals(dbName) && Arrays.equals(value.key, rawKey)) { + return OldValue.present(value.value); + } + } + return OldValue.absent(); + } + + @Override + public List range(String dbName, byte[] lower, + byte[] upper) { + List result = new ArrayList<>(); + for (Value value : values) { + if (value.dbName.equals(dbName) + && (foreignRange || inRange(value.key, lower, upper))) { + result.add(new HistoricalRangeOverlay.Entry(value.key, value.value)); + } + } + result.sort((left, right) -> BlockReverseDiff.compareUnsigned( + left.getKey(), right.getKey())); + if (reverseRange) { + Collections.reverse(result); + } + if (duplicateRange && !result.isEmpty()) { + result.add(result.get(result.size() - 1)); + } + if (driftAfterRange) { + serving.drifted = true; + } + return result; + } + + @Override + public void close() { + } + }; + ArchiveReadSnapshot.PinnedHistory history = new ArchiveReadSnapshot.PinnedHistory() { + @Override + public long getIndexedFrom() { + return 0L; + } + + @Override + public long getIndexedThrough() { + return 7L; + } + + @Override + public byte[] getHeadHash() { + return hash(7); + } + + @Override + public byte[] getAuthoritativePrefixDigest() { + return hash(11); + } + + @Override + public OldValue read(String dbName, byte[] rawKey, long firstChangeBlock) { + throw new AssertionError("Fixture must not read unconfigured history"); + } + + @Override + public void close() { + } + }; + return ArchiveReadSnapshot.pin(7L, 7L, hash(7), serving, latest, history); + } + } + + private static boolean inRange(byte[] key, byte[] lower, byte[] upper) { + return BlockReverseDiff.compareUnsigned(key, lower) >= 0 + && (upper == null || BlockReverseDiff.compareUnsigned(key, upper) < 0); + } + + private static final class MutableServing implements ServingKeyIndex { + private boolean drifted; + + @Override + public String getGenerationId() { + return "account-asset-resolver"; + } + + @Override + public long getIndexedFrom() { + return 0L; + } + + @Override + public long getIndexedThrough() { + return 7L; + } + + @Override + public byte[] getHeadHash() { + return drifted ? hash(8) : hash(7); + } + + @Override + public byte[] getAuthoritativePrefixDigest() { + return hash(11); + } + + @Override + public OptionalLong firstChangeAfter(String dbName, byte[] rawKey, long targetBlock, + long upperBound) { + return OptionalLong.empty(); + } + + @Override + public List changesInRange(String dbName, + byte[] lowerInclusive, byte[] upperExclusive, long targetBlock, long upperBound, + int maxChangedKeys) { + return Collections.emptyList(); + } + } + + private static final class Value { + private final String dbName; + private final byte[] key; + private final byte[] value; + + private Value(String dbName, byte[] key, byte[] value) { + this.dbName = dbName; + this.key = Arrays.copyOf(key, key.length); + this.value = Arrays.copyOf(value, value.length); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index 6ca7b9ea63d..9d0117e2f68 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -13,10 +13,12 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; import com.google.common.primitives.Bytes; import com.google.common.primitives.Longs; import com.google.protobuf.ByteString; +import java.io.Closeable; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; @@ -54,6 +56,104 @@ public class SnapshotOldValueCollectorTest extends BaseMethodTest { + @Test + public void borrowedRuntimeAttachmentIsAtomicAndIdentityBound() throws Exception { + SnapshotManager manager = new SnapshotManager(""); + manager.add(new Chainbase(new SnapshotRoot(new MemoryDb("code")))); + OldValueCollector collector = mock(OldValueCollector.class); + ArchiveBlockProjectionPreparer preparer = mock(ArchiveBlockProjectionPreparer.class); + DurableBlockReverseDiffSink sink = mock(DurableBlockReverseDiffSink.class, + withSettings().extraInterfaces(Closeable.class)); + ArchiveRuntimeAttachment attachment = + new ArchiveRuntimeAttachment(collector, preparer, sink); + ArchiveRuntimeAttachment foreign = + new ArchiveRuntimeAttachment(collector, preparer, sink); + + manager.attachArchiveRuntime(attachment); + + assertThrows(IllegalStateException.class, () -> manager.attachArchiveRuntime(foreign)); + assertThrows(IllegalStateException.class, () -> manager.detachArchiveRuntime(foreign)); + assertThrows(IllegalStateException.class, + () -> manager.installArchiveCollector(collector, sink)); + assertThrows(IllegalStateException.class, + () -> manager.installArchiveProjectionPreparer(preparer)); + assertSame(attachment, manager.detachArchiveRuntime(attachment)); + assertThrows(IllegalStateException.class, () -> manager.detachArchiveRuntime(attachment)); + verify((Closeable) sink, never()).close(); + + BlockReverseDiffSink legacySink = mock(BlockReverseDiffSink.class, + withSettings().extraInterfaces(Closeable.class)); + manager.installArchiveCollector(collector, legacySink); + manager.installArchiveProjectionPreparer(preparer); + manager.shutdown(); + + verify((Closeable) legacySink).close(); + verify((Closeable) sink, never()).close(); + } + + @Test + public void detachAbortsLayerAndFrozenForwardOwnership() throws Exception { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = + preparedProjection(meta, mock(BlockReverseDiff.class)); + DurableBlockReverseDiffSink sink = mock(DurableBlockReverseDiffSink.class, + withSettings().extraInterfaces(Closeable.class)); + ArchiveRuntimeAttachment attachment = new ArchiveRuntimeAttachment( + new SnapshotOldValueCollector(), captured -> projection, sink); + manager.attachArchiveRuntime(attachment); + commitBlock(manager, database, meta, "key-1"); + setFlushCount(manager, 1); + manager.freezeArchiveForwardFlushRange(); + + assertTrue(manager.hasPendingArchiveForwardFlush()); + assertSame(attachment, manager.detachArchiveRuntime(attachment)); + + assertFalse(manager.hasPendingArchiveForwardFlush()); + assertEquals(0, manager.getArchiveForwardPayloadOwnerCount()); + assertThrows(IllegalStateException.class, manager::claimArchiveForwardFlushPayloads); + verify(projection).abort(); + verify((Closeable) sink, never()).close(); + manager.shutdown(); + verify((Closeable) sink, never()).close(); + } + + @Test + public void detachClearsSealedForwardOwnershipWithoutAbortingCompletedProjection() + throws Exception { + MemoryDb memoryDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); + manager.add(database); + manager.enable(); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + BlockChangeView view = mock(BlockChangeView.class); + when(view.getMeta()).thenReturn(meta); + AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = + sealReadyProjection(meta, view); + DurableBlockReverseDiffSink sink = mock(DurableBlockReverseDiffSink.class); + ArchiveRuntimeAttachment attachment = new ArchiveRuntimeAttachment( + new SnapshotOldValueCollector(), captured -> projection, sink); + manager.attachArchiveRuntime(attachment); + commitBlock(manager, database, meta, "key-1"); + setFlushCount(manager, 1); + manager.freezeArchiveForwardFlushRange(); + manager.sealPendingArchiveForwardFlush(Collections.singletonList(marker(meta))); + + assertTrue(manager.hasPendingArchiveForwardFlush()); + manager.detachArchiveRuntime(attachment); + + assertFalse(manager.hasPendingArchiveForwardFlush()); + assertThrows(IllegalStateException.class, manager::claimArchiveForwardFlushPayloads); + verify(projection).completeSeal(); + verify(projection, never()).abort(); + manager.shutdown(); + } + @Test public void collectsBlockPreStateAfterNestedSessionsFinish() { MemoryDb memoryDb = new MemoryDb("code"); @@ -147,7 +247,7 @@ public void preservesStorageRowPhysicalKeyWithoutLogicalProjection() { } @Test - public void excludesAbiChangesFromCanonicalStateHistory() { + public void retainsAbiChangesInCanonicalStateHistory() { SnapshotManager manager = new SnapshotManager(""); Chainbase abi = new Chainbase(new SnapshotRoot(new MemoryDb("abi"))); Chainbase code = new Chainbase(new SnapshotRoot(new MemoryDb("code"))); @@ -163,10 +263,11 @@ public void excludesAbiChangesFromCanonicalStateHistory() { } BlockReverseDiff diff = prepared(code); - assertEquals(1, diff.getGroups().size()); - assertEquals("code", diff.getGroups().get(0).getDbName()); - assertFalse(diff.getGroups().stream().anyMatch(group -> "abi".equals(group.getDbName()))); - assertTrue(((SnapshotImpl) abi.getHead()).getPreparedArchiveBlock() == null); + assertEquals(2, diff.getGroups().size()); + assertEquals("abi", diff.getGroups().get(0).getDbName()); + assertEquals("code", diff.getGroups().get(1).getDbName()); + assertFalse(find(diff.getGroups().get(0), bytes("contract")).getOldValue().isPresent()); + assertEquals(diff, prepared(abi)); manager.shutdown(); } @@ -219,9 +320,9 @@ public void rejectsUnknownOrDuplicateRegisteredDatabaseNames() { public void classifiesEveryChainbaseRegisteredByTheApplication() { SnapshotManager applicationManager = context.getBean(SnapshotManager.class); ArchiveStoreScope.validate(applicationManager.getDbs()); - assertEquals(26, ArchiveStoreScope.getStateDatabases().size()); - assertFalse(ArchiveStoreScope.isStateDatabase("abi")); - assertTrue(ArchiveStoreScope.isExcludedDatabase("abi")); + assertEquals(27, ArchiveStoreScope.getStateDatabases().size()); + assertTrue(ArchiveStoreScope.isStateDatabase("abi")); + assertFalse(ArchiveStoreScope.isExcludedDatabase("abi")); assertTrue(ArchiveStoreScope.isClassified("abi")); } @@ -602,7 +703,7 @@ public void archiveDurabilityFailurePreventsCheckpointAndRefresh() throws Except } @Test - public void flushRetriesDurabilityAndReceiptWithoutResubmittingHistory() throws Exception { + public void flushRetriesDurabilityAndEvidenceWithoutResubmittingHistory() throws Exception { MemoryDb memoryDb = new MemoryDb("code"); SnapshotManager manager = new SnapshotManager(""); Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); @@ -615,9 +716,9 @@ public void flushRetriesDurabilityAndReceiptWithoutResubmittingHistory() throws when(checkpoint.getDbSource()).thenReturn(checkpointDb); manager.setCheckTmpStore(checkpoint); ArchiveHistoryWriter writer = new ArchiveHistoryWriter( - temporaryFolder.newFolder("flush-receipt-retry").toPath(), 4096, + temporaryFolder.newFolder("flush-evidence-retry").toPath(), 4096, ArchiveStoreScope.getStateDatabases()); - FailOnceReceiptSink sink = new FailOnceReceiptSink(writer); + FailOnceEvidenceSink sink = new FailOnceEvidenceSink(writer); manager.installArchiveCollector(new SnapshotOldValueCollector(), sink); AtomicReference prepared = new AtomicReference<>(); @@ -644,7 +745,7 @@ public void flushRetriesDurabilityAndReceiptWithoutResubmittingHistory() throws assertEquals(1, sink.acceptAllCalls); assertEquals(3, sink.awaitCalls); - assertEquals(2, sink.receiptCalls); + assertEquals(2, sink.evidenceCalls); verify(prepared.get()).completeSeal(); assertEquals(1, manager.claimArchiveForwardFlushPayloads().size()); assertFalse(manager.hasPendingArchiveForwardFlush()); @@ -1027,7 +1128,7 @@ public void pendingForwardFlushSealRetriesAndClaimsOrderedPayloadsOnce() throws } @Test - public void durableReceiptFailureKeepsFrozenSlotAndShutdownReleasesSealedSlot() + public void durableEvidenceFailureKeepsFrozenSlotAndShutdownReleasesSealedSlot() throws Exception { MemoryDb memoryDb = new MemoryDb("code"); SnapshotManager manager = new SnapshotManager(""); @@ -1046,8 +1147,8 @@ public void durableReceiptFailureKeepsFrozenSlotAndShutdownReleasesSealedSlot() setFlushCount(manager, 1); FrozenBatch frozen = manager.freezeArchiveForwardFlushRange(); boolean[] substitute = {true}; - DurableHistoryMarkerRangeReceipt receipt = new DurableHistoryMarkerRangeReceipt( - new DurableHistoryMarkerRangeReceipt.Source() { + DurableHistoryMarkerRangeEvidence evidence = new DurableHistoryMarkerRangeEvidence( + new DurableHistoryMarkerRangeEvidence.Source() { @Override public HistoryCommitMarker marker(long epoch) { return substitute[0] @@ -1062,10 +1163,10 @@ public BlockReverseDiff readCommitted(long epoch) { }, 1); assertThrows(ArchivePersistenceException.class, - () -> manager.sealPendingArchiveForwardFlush(receipt)); + () -> manager.sealPendingArchiveForwardFlush(evidence)); assertSame(frozen, manager.freezeArchiveForwardFlushRange()); substitute[0] = false; - manager.sealPendingArchiveForwardFlush(receipt); + manager.sealPendingArchiveForwardFlush(evidence); assertTrue(manager.hasPendingArchiveForwardFlush()); manager.shutdown(); @@ -1244,13 +1345,13 @@ private static Map copy(Map source) { return copy; } - private static final class FailOnceReceiptSink implements DurableBlockReverseDiffSink { + private static final class FailOnceEvidenceSink implements DurableBlockReverseDiffSink { private final ArchiveHistoryWriter writer; private int acceptAllCalls; private int awaitCalls; - private int receiptCalls; + private int evidenceCalls; - private FailOnceReceiptSink(ArchiveHistoryWriter writer) { + private FailOnceEvidenceSink(ArchiveHistoryWriter writer) { this.writer = writer; } @@ -1280,11 +1381,11 @@ public void awaitCommitted(long epoch) { } @Override - public DurableHistoryMarkerRangeReceipt createMarkerRangeReceipt(int maxMarkers) { - receiptCalls++; - if (receiptCalls == 1) { - return new DurableHistoryMarkerRangeReceipt( - new DurableHistoryMarkerRangeReceipt.Source() { + public DurableHistoryMarkerRangeEvidence createMarkerRangeEvidence(int maxMarkers) { + evidenceCalls++; + if (evidenceCalls == 1) { + return new DurableHistoryMarkerRangeEvidence( + new DurableHistoryMarkerRangeEvidence.Source() { @Override public HistoryCommitMarker marker(long epoch) { return null; @@ -1296,7 +1397,7 @@ public BlockReverseDiff readCommitted(long epoch) { } }, maxMarkers); } - return writer.createMarkerRangeReceipt(maxMarkers); + return writer.createMarkerRangeEvidence(maxMarkers); } @Override diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java new file mode 100644 index 00000000000..f87b6b4c432 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -0,0 +1,289 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.Closeable; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.ChainBaseManager; +import org.tron.core.config.args.Storage; +import org.tron.core.db.Manager; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.StateArchiveRuntimeOwner.State; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.store.DynamicPropertiesStore; + +public class StateArchiveManagerStartupIntegrationTest { + + private static final List PARTICIPANTS = participants(); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void managerRecoversExact27NativeFilesBeforeOpeningProducers() throws Exception { + for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { + Path output = temporaryFolder.newFolder("manager-" + engine.toLowerCase()).toPath(); + Path archive = output.resolve("state-archive"); + HistoryCommitMarker head = initializeRecoverableTail(archive, engine); + SnapshotManager snapshots = new SnapshotManager(""); + Manager manager = manager(snapshots, head); + + withArchiveConfig(output, engine, true, () -> invoke(manager, "initStateArchive")); + + assertEquals(State.RECOVERED, manager.getStateArchiveRuntime().getState()); + assertEquals(1, manager.getStateArchiveRuntime().getStartupRecoveryActionCount()); + assertEquals(head.getMeta(), manager.getStateArchiveRuntime().getRecoveredHead()); + assertNull(manager.getArchiveHistoryWriter()); + assertEquals(-1, snapshots.getArchiveReadableEpoch()); + + invoke(manager, "closeStateArchive"); + assertNull(manager.getStateArchiveRuntime()); + assertNativeParticipantsReopen(archive, engine, PARTICIPANTS); + } + } + + @Test + public void partialParticipantOpenRollsBackAndPreservesFailureEvidence() throws Exception { + for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { + Path output = temporaryFolder.newFolder("partial-" + engine.toLowerCase()).toPath(); + Path archive = output.resolve("state-archive"); + HistoryCommitMarker head = initializeHistoryAndGlobalProgress(archive); + int failureIndex = 3; + List openedNames = PARTICIPANTS.subList(0, failureIndex); + initializeParticipants(archive, engine, openedNames, head); + Path failurePath = archive.resolve("participants").resolve(PARTICIPANTS.get(failureIndex)); + byte[] evidence = new byte[]{4, 5, 6, 7}; + Files.write(failurePath, evidence); + Manager manager = manager(new SnapshotManager(""), head); + + assertThrows(IllegalStateException.class, + () -> withArchiveConfig(output, engine, true, + () -> invoke(manager, "initStateArchive"))); + + assertNull(manager.getStateArchiveRuntime()); + assertArrayEquals(evidence, Files.readAllBytes(failurePath)); + assertNativeParticipantsReopen(archive, engine, openedNames); + } + } + + @Test + public void disabledManagerControlDoesNotInspectOrCreateArchiveRuntime() throws Exception { + Path output = temporaryFolder.newFolder("disabled-manager").toPath(); + Path archive = output.resolve("state-archive"); + Files.createDirectories(archive); + byte[] evidence = new byte[]{8, 9, 10}; + Files.write(archive.resolve("unexpected-evidence"), evidence); + Manager manager = new Manager(); + + withArchiveConfig(output, "LEVELDB", false, () -> invoke(manager, "initStateArchive")); + + assertNull(manager.getStateArchiveRuntime()); + assertArrayEquals(evidence, Files.readAllBytes(archive.resolve("unexpected-evidence"))); + assertFalse(Files.exists(archive.resolve("participants"))); + } + + private static Manager manager(SnapshotManager snapshots, HistoryCommitMarker head) + throws Exception { + DynamicPropertiesStore properties = mock(DynamicPropertiesStore.class); + when(properties.getLatestBlockHeaderNumber()).thenReturn(head.getMeta().getBlockNumber()); + when(properties.getLatestBlockHeaderHash()) + .thenReturn(Sha256Hash.wrap(head.getMeta().getBlockHash())); + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getDynamicPropertiesStore()).thenReturn(properties); + Manager manager = new Manager(); + setField(manager, "revokingStore", snapshots); + setField(manager, "chainBaseManager", chainBase); + return manager; + } + + private static HistoryCommitMarker initializeRecoverableTail(Path archive, String engine) + throws Exception { + HistoryCommitMarker checkpoint; + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( + archive, 4096, ArchiveStoreScope.getStateDatabases())) { + writer.accept(new BlockReverseDiff( + new BlockSnapshotMeta(6, 6, hash(6), hash(5), 6_000L), + Collections.emptyList())); + writer.accept(new BlockReverseDiff( + new BlockSnapshotMeta(7, 7, hash(7), hash(6), 7_000L), + Collections.emptyList())); + checkpoint = writer.get(6); + } + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + new ArchiveProgressFile(archive.resolve("progress/checkpoint.progress"), codec) + .store(global(Kind.APPLY_CHECKPOINT, checkpoint)); + new ArchiveProgressFile(archive.resolve("progress/reader.progress"), codec) + .store(global(Kind.READER_VISIBLE, checkpoint)); + initializeParticipants(archive, engine, PARTICIPANTS, checkpoint); + return checkpoint; + } + + private static HistoryCommitMarker initializeHistoryAndGlobalProgress(Path archive) + throws Exception { + HistoryCommitMarker head; + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( + archive, 4096, ArchiveStoreScope.getStateDatabases())) { + writer.accept(new BlockReverseDiff( + new BlockSnapshotMeta(7, 7, hash(7), hash(6), 7_000L), + Collections.emptyList())); + head = writer.committedHead(); + } + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + new ArchiveProgressFile(archive.resolve("progress/checkpoint.progress"), codec) + .store(global(Kind.APPLY_CHECKPOINT, head)); + new ArchiveProgressFile(archive.resolve("progress/reader.progress"), codec) + .store(global(Kind.READER_VISIBLE, head)); + return head; + } + + private static void initializeParticipants(Path archive, String engine, List names, + HistoryCommitMarker head) throws Exception { + List opened = new ArrayList<>(); + try { + for (String name : names) { + ArchiveParticipant participant = openParticipant(archive, engine, name); + opened.add((Closeable) participant); + participant.apply(Collections.emptyList(), participant(name, head)); + } + } finally { + closeReverse(opened); + } + } + + private static void assertNativeParticipantsReopen(Path archive, String engine, + List names) throws Exception { + List reopened = new ArrayList<>(); + try { + for (String name : names) { + reopened.add((Closeable) openParticipant(archive, engine, name)); + } + } finally { + closeReverse(reopened); + } + } + + private static ArchiveParticipant openParticipant(Path archive, String engine, String name) + throws Exception { + Path directory = archive.resolve("participants").resolve(name); + return "ROCKSDB".equals(engine) + ? new RocksDbArchiveParticipant(directory, name, PARTICIPANTS) + : new LevelDbArchiveParticipant(directory, name, PARTICIPANTS); + } + + private static ArchiveProgressEnvelope participant(String name, HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, name, + marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { + return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); + } + + private static void withArchiveConfig(Path output, String engine, boolean enabled, + ThrowingRunnable action) throws Exception { + CommonParameter args = CommonParameter.getInstance(); + Storage oldStorage = args.getStorage(); + Storage storage = oldStorage == null ? new Storage() : oldStorage; + args.storage = storage; + String oldOutput = args.outputDirectory; + String oldDirectory = storage.getStateArchiveDirectory(); + String oldEngine = storage.getDbEngine(); + long oldSegmentSize = storage.getStateArchiveMaxSegmentSize(); + int oldQueueCapacity = storage.getStateArchiveQueueCapacity(); + boolean oldEnabled = storage.isStateArchiveEnabled(); + try { + args.outputDirectory = output.toString(); + storage.setStateArchiveDirectory("state-archive"); + storage.setDbEngine(engine); + storage.setStateArchiveMaxSegmentSize(4096); + storage.setStateArchiveQueueCapacity(4); + storage.setStateArchiveEnabled(enabled); + action.run(); + } finally { + args.outputDirectory = oldOutput; + storage.setStateArchiveDirectory(oldDirectory); + storage.setDbEngine(oldEngine); + storage.setStateArchiveMaxSegmentSize(oldSegmentSize); + storage.setStateArchiveQueueCapacity(oldQueueCapacity); + storage.setStateArchiveEnabled(oldEnabled); + args.storage = oldStorage; + } + } + + private static void closeReverse(List resources) throws Exception { + Exception failure = null; + for (int i = resources.size() - 1; i >= 0; i--) { + try { + resources.get(i).close(); + } catch (Exception closeFailure) { + if (failure == null) { + failure = closeFailure; + } else { + failure.addSuppressed(closeFailure); + } + } + } + if (failure != null) { + throw failure; + } + } + + private static void setField(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } + + private static void invoke(Manager manager, String name) throws Exception { + Method method = Manager.class.getDeclaredMethod(name); + method.setAccessible(true); + try { + method.invoke(manager); + } catch (InvocationTargetException failure) { + Throwable cause = failure.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw failure; + } + } + + private static List participants() { + List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(participants); + return Collections.unmodifiableList(participants); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + @FunctionalInterface + private interface ThrowingRunnable { + void run() throws Exception; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java new file mode 100644 index 00000000000..3e479f90e83 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java @@ -0,0 +1,217 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.OptionalLong; +import org.junit.Test; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedHistory; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.archive.ArchiveRuntimeQueryGate.Lease; +import org.tron.core.db2.archive.StateArchiveRuntimeOwner.State; +import org.tron.core.db2.core.SnapshotManager; + +public class StateArchiveRuntimeOwnerTest { + + @Test + public void activeQueryStopsCloseAfterDetachAndDrainedRetryClosesInOrder() + throws Exception { + List order = new ArrayList<>(); + Fixture fixture = fixture(order, null, null, null); + Lease lease = fixture.queryGate.pin(0); + + assertThrows(IllegalStateException.class, fixture.owner::close); + + assertEquals(State.QUIESCING, fixture.owner.getState()); + assertTrue(order.isEmpty()); + assertThrows(IllegalStateException.class, () -> fixture.queryGate.pin(0)); + verify(fixture.manager).detachArchiveRuntime(fixture.attachment); + + lease.close(); + fixture.owner.close(); + fixture.owner.close(); + + assertEquals(State.CLOSED, fixture.owner.getState()); + verify(fixture.manager, times(1)).detachArchiveRuntime(fixture.attachment); + assertEquals(Arrays.asList("latest", "catalog", "participant-2", "participant-1", + "participant-0", "sink"), order); + } + + @Test + public void independentCloseFailuresAreSuppressedAndNotRetried() throws Exception { + List order = new ArrayList<>(); + IOException latestFailure = new IOException("latest failure"); + Fixture fixture = fixture(order, latestFailure, + new IllegalStateException("participant failure"), new IOException("sink failure")); + + IOException failure = assertThrows(IOException.class, fixture.owner::close); + + assertSame(latestFailure, failure); + assertEquals(2, failure.getSuppressed().length); + assertEquals("Failed to close archive participant 1", + failure.getSuppressed()[0].getMessage()); + assertEquals("sink failure", failure.getSuppressed()[1].getMessage()); + assertEquals(Arrays.asList("latest", "catalog", "participant-2", "participant-1", + "participant-0", "sink"), order); + assertEquals(State.FAILED_CLOSED, fixture.owner.getState()); + + assertSame(failure, assertThrows(IOException.class, fixture.owner::close)); + assertEquals(6, order.size()); + } + + @Test + public void rejectsNonCloseableSinkAndDuplicateOwnedResource() { + SnapshotManager manager = manager(); + ArchiveRuntimeQueryGate queryGate = new ArchiveRuntimeQueryGate(target -> snapshot()); + Closeable latest = () -> { }; + Closeable catalog = () -> { }; + DurableBlockReverseDiffSink nonCloseable = mock(DurableBlockReverseDiffSink.class); + ArchiveRuntimeAttachment invalid = attachment(nonCloseable); + + assertThrows(IllegalArgumentException.class, () -> new StateArchiveRuntimeOwner(manager, + invalid, queryGate, latest, catalog, Collections.emptyList())); + + TrackingSink sink = new TrackingSink(new ArrayList<>(), null); + ArchiveRuntimeAttachment valid = attachment(sink); + assertThrows(IllegalArgumentException.class, () -> new StateArchiveRuntimeOwner(manager, + valid, queryGate, latest, catalog, Collections.singletonList(latest))); + } + + private static Fixture fixture(List order, IOException latestFailure, + RuntimeException participantFailure, IOException sinkFailure) { + SnapshotManager manager = manager(); + TrackingSink sink = new TrackingSink(order, sinkFailure); + ArchiveRuntimeAttachment attachment = attachment(sink); + when(manager.detachArchiveRuntime(attachment)).thenReturn(attachment); + manager.attachArchiveRuntime(attachment); + ArchiveRuntimeQueryGate queryGate = new ArchiveRuntimeQueryGate(target -> snapshot()); + TrackingCloseable latest = new TrackingCloseable("latest", order, latestFailure, null); + TrackingCloseable catalog = new TrackingCloseable("catalog", order, null, null); + List participants = Arrays.asList( + new TrackingCloseable("participant-0", order, null, null), + new TrackingCloseable("participant-1", order, null, participantFailure), + new TrackingCloseable("participant-2", order, null, null)); + StateArchiveRuntimeOwner owner = new StateArchiveRuntimeOwner(manager, attachment, queryGate, + latest, catalog, participants); + return new Fixture(manager, attachment, queryGate, owner); + } + + private static SnapshotManager manager() { + return mock(SnapshotManager.class); + } + + private static ArchiveRuntimeAttachment attachment(DurableBlockReverseDiffSink sink) { + return new ArchiveRuntimeAttachment(mock(OldValueCollector.class), + mock(ArchiveBlockProjectionPreparer.class), sink); + } + + private static ArchiveReadSnapshot snapshot() throws IOException { + byte[] hash = new byte[32]; + ServingKeyIndex serving = mock(ServingKeyIndex.class); + when(serving.getIndexedFrom()).thenReturn(0L); + when(serving.getIndexedThrough()).thenReturn(0L); + when(serving.getHeadHash()).thenReturn(hash); + when(serving.getAuthoritativePrefixDigest()).thenReturn(new byte[0]); + when(serving.firstChangeAfter(org.mockito.ArgumentMatchers.anyString(), + org.mockito.ArgumentMatchers.any(byte[].class), org.mockito.ArgumentMatchers.anyLong(), + org.mockito.ArgumentMatchers.anyLong())).thenReturn(OptionalLong.empty()); + PinnedLatestState latest = mock(PinnedLatestState.class); + when(latest.getBlockNumber()).thenReturn(0L); + when(latest.getBlockHash()).thenReturn(hash); + PinnedHistory history = mock(PinnedHistory.class); + when(history.getIndexedFrom()).thenReturn(0L); + when(history.getIndexedThrough()).thenReturn(0L); + when(history.getHeadHash()).thenReturn(hash); + when(history.getAuthoritativePrefixDigest()).thenReturn(new byte[0]); + return ArchiveReadSnapshot.pin(0, 0, hash, serving, latest, history); + } + + private static final class Fixture { + private final SnapshotManager manager; + private final ArchiveRuntimeAttachment attachment; + private final ArchiveRuntimeQueryGate queryGate; + private final StateArchiveRuntimeOwner owner; + + private Fixture(SnapshotManager manager, ArchiveRuntimeAttachment attachment, + ArchiveRuntimeQueryGate queryGate, StateArchiveRuntimeOwner owner) { + this.manager = manager; + this.attachment = attachment; + this.queryGate = queryGate; + this.owner = owner; + } + } + + private static final class TrackingCloseable implements Closeable { + private final String name; + private final List order; + private final IOException ioFailure; + private final RuntimeException runtimeFailure; + + private TrackingCloseable(String name, List order, IOException ioFailure, + RuntimeException runtimeFailure) { + this.name = name; + this.order = order; + this.ioFailure = ioFailure; + this.runtimeFailure = runtimeFailure; + } + + @Override + public void close() throws IOException { + order.add(name); + if (ioFailure != null) { + throw ioFailure; + } + if (runtimeFailure != null) { + throw runtimeFailure; + } + } + } + + private static final class TrackingSink implements DurableBlockReverseDiffSink, Closeable { + private final List order; + private final IOException failure; + + private TrackingSink(List order, IOException failure) { + this.order = order; + this.failure = failure; + } + + @Override + public void accept(BlockReverseDiff diff) { + } + + @Override + public void awaitCommitted(long epoch) { + } + + @Override + public DurableHistoryMarkerRangeEvidence createMarkerRangeEvidence(int maxMarkers) { + throw new UnsupportedOperationException(); + } + + @Override + public void releaseThrough(long epoch) { + } + + @Override + public void close() throws IOException { + order.add("sink"); + if (failure != null) { + throw failure; + } + } + } + +} From cc9ad4870752fca2ec0820fd025874afe672de94 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 15:52:05 +0800 Subject: [PATCH 032/161] feat(chainbase): integrate archive normal flush --- .../core/db2/archive/AccountChangeIndex.java | 27 +++ .../db2/archive/ArchiveHistoryWriter.java | 10 +- .../db2/archive/ArchiveRuntimeAttachment.java | 30 +++ .../db2/archive/StateArchiveRuntimeOwner.java | 175 ++++++++++++++++-- .../tron/core/db2/core/SnapshotManager.java | 23 ++- .../main/java/org/tron/core/db/Manager.java | 14 +- .../db2/archive/ArchiveHistoryWriterTest.java | 13 +- ...eArchiveManagerStartupIntegrationTest.java | 155 +++++++++++++++- 8 files changed, 421 insertions(+), 26 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java index 2b6b70e761c..6659bc2a3e3 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java @@ -101,6 +101,33 @@ synchronized void revert(BlockReverseDiff diff, BlockSnapshotMeta newHead) throw } } + /** Truncates this derived index to the authoritative committed-history head. */ + synchronized void truncateAfter(BlockSnapshotMeta newHead) throws IOException { + long target = newHead == null ? -1 : newHead.getEpoch(); + try (WriteBatch batch = new WriteBatch(); RocksIterator iterator = database.newIterator()) { + iterator.seek(new byte[]{DATA_PREFIX}); + while (iterator.isValid()) { + byte[] key = iterator.key(); + if (key.length != DATA_KEY_LENGTH || key[0] != DATA_PREFIX) { + break; + } + long epoch = ByteBuffer.wrap(key, 1 + ADDRESS_LENGTH, Long.BYTES).getLong(); + if (epoch > target) { + batch.delete(key); + } + iterator.next(); + } + if (newHead == null) { + batch.delete(HEAD_KEY); + } else { + batch.put(HEAD_KEY, encodeHead(newHead)); + } + database.write(syncWrites, batch); + } catch (RocksDBException failure) { + throw new IOException("Failed to truncate account change index", failure); + } + } + synchronized OptionalLong firstChangeAfter(byte[] address, long target, long upperBound) throws IOException { if (address == null || address.length != ADDRESS_LENGTH) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java index 1742d8659e7..fdd081cff98 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java @@ -372,10 +372,17 @@ private void recoverPreparedSuffix() throws IOException { private void catchUpAccountIndex() throws IOException { HistoryCommitMarker head = commits.head(); if (head == null) { + if (accountIndex.getIndexedThrough() >= 0) { + accountIndex.truncateAfter(null); + } return; } long indexed = accountIndex.getIndexedThrough(); long first = commits.firstEpoch(); + if (indexed > head.getMeta().getEpoch()) { + accountIndex.truncateAfter(head.getMeta()); + indexed = head.getMeta().getEpoch(); + } if (indexed >= 0) { HistoryCommitMarker indexedMarker = commits.get(indexed); if (indexedMarker == null || !accountIndex.headMatches(indexedMarker.getMeta())) { @@ -384,9 +391,6 @@ private void catchUpAccountIndex() throws IOException { } } if (indexed >= head.getMeta().getEpoch()) { - if (indexed > head.getMeta().getEpoch()) { - throw new ArchivePersistenceException("Account index is ahead of committed history"); - } return; } long next = indexed < 0 ? first : indexed + 1; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java index d94d0ce1e79..3008fe4fd60 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java @@ -1,5 +1,7 @@ package org.tron.core.db2.archive; +import java.io.IOException; +import java.util.List; import java.util.Objects; /** Borrowed archive runtime collaborators installed into SnapshotManager as one unit. */ @@ -8,12 +10,20 @@ public final class ArchiveRuntimeAttachment { private final OldValueCollector collector; private final ArchiveBlockProjectionPreparer projectionPreparer; private final DurableBlockReverseDiffSink sink; + private final ForwardFlushPublisher forwardFlushPublisher; public ArchiveRuntimeAttachment(OldValueCollector collector, ArchiveBlockProjectionPreparer projectionPreparer, DurableBlockReverseDiffSink sink) { + this(collector, projectionPreparer, sink, null); + } + + public ArchiveRuntimeAttachment(OldValueCollector collector, + ArchiveBlockProjectionPreparer projectionPreparer, DurableBlockReverseDiffSink sink, + ForwardFlushPublisher forwardFlushPublisher) { this.collector = Objects.requireNonNull(collector, "collector"); this.projectionPreparer = Objects.requireNonNull(projectionPreparer, "projectionPreparer"); this.sink = Objects.requireNonNull(sink, "sink"); + this.forwardFlushPublisher = forwardFlushPublisher; } public OldValueCollector getCollector() { @@ -27,4 +37,24 @@ public ArchiveBlockProjectionPreparer getProjectionPreparer() { public DurableBlockReverseDiffSink getSink() { return sink; } + + public boolean hasForwardFlushPublisher() { + return forwardFlushPublisher != null; + } + + public void publishForwardFlush(List payloads, + ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { + if (forwardFlushPublisher == null) { + throw new IllegalStateException("Archive runtime has no forward flush publisher"); + } + forwardFlushPublisher.publish(payloads, refresh); + } + + /** Publishes one frozen normal-flush target through C/D, refresh and R. */ + @FunctionalInterface + public interface ForwardFlushPublisher { + + void publish(List payloads, + ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException; + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java index 036d655f7b0..5f6fe64a546 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; import org.tron.core.db2.core.SnapshotManager; @@ -27,14 +28,18 @@ public enum State { } private final SnapshotManager snapshotManager; - private final ArchiveRuntimeAttachment attachment; - private final ArchiveRuntimeQueryGate queryGate; - private final Closeable latestCoordinator; - private final Closeable servingCatalog; + private final Path archiveDirectory; + private final long maxSegmentSize; private final List participants; - private final Closeable sink; + private final Map participantEngines; private final BlockSnapshotMeta recoveredHead; private final int startupRecoveryActionCount; + private ArchiveRuntimeAttachment attachment; + private ArchiveRuntimeQueryGate queryGate; + private Closeable latestCoordinator; + private Closeable servingCatalog; + private Closeable sink; + private ArchiveHistoryWriter historyWriter; private State state; private boolean detached; private IOException terminalFailure; @@ -44,6 +49,8 @@ public StateArchiveRuntimeOwner(SnapshotManager snapshotManager, Closeable latestCoordinator, Closeable servingCatalog, List participants) { this.snapshotManager = Objects.requireNonNull(snapshotManager, "snapshotManager"); + this.archiveDirectory = null; + this.maxSegmentSize = 0; this.attachment = Objects.requireNonNull(attachment, "attachment"); this.queryGate = Objects.requireNonNull(queryGate, "queryGate"); this.latestCoordinator = Objects.requireNonNull(latestCoordinator, "latestCoordinator"); @@ -53,6 +60,7 @@ public StateArchiveRuntimeOwner(SnapshotManager snapshotManager, } this.sink = (Closeable) attachment.getSink(); this.participants = immutableParticipants(participants); + this.participantEngines = Collections.emptyMap(); this.recoveredHead = null; this.startupRecoveryActionCount = 0; this.state = State.RUNNING; @@ -60,14 +68,18 @@ public StateArchiveRuntimeOwner(SnapshotManager snapshotManager, } private StateArchiveRuntimeOwner(SnapshotManager snapshotManager, - List participants, BlockSnapshotMeta recoveredHead, - int startupRecoveryActionCount) { + Path archiveDirectory, long maxSegmentSize, List participants, + Map participantEngines, + BlockSnapshotMeta recoveredHead, int startupRecoveryActionCount) { this.snapshotManager = Objects.requireNonNull(snapshotManager, "snapshotManager"); + this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + this.maxSegmentSize = maxSegmentSize; this.attachment = null; this.queryGate = null; this.latestCoordinator = null; this.servingCatalog = null; this.participants = immutableParticipants(participants); + this.participantEngines = immutableParticipantEngines(participantEngines); this.sink = null; this.recoveredHead = Objects.requireNonNull(recoveredHead, "recoveredHead"); this.startupRecoveryActionCount = startupRecoveryActionCount; @@ -115,8 +127,8 @@ public static StateArchiveRuntimeOwner recover(SnapshotManager snapshotManager, if (head == null) { throw new ArchivePersistenceException("State Archive recovered H head is missing"); } - return new StateArchiveRuntimeOwner(snapshotManager, opened, head.getMeta(), - first.getActions().size()); + return new StateArchiveRuntimeOwner(snapshotManager, root, maxSegmentSize, opened, + openedByName, head.getMeta(), first.getActions().size()); } catch (IOException | RuntimeException failure) { closeReverse(opened, failure); throw failure; @@ -138,6 +150,101 @@ public int getStartupRecoveryActionCount() { return startupRecoveryActionCount; } + /** Continues this recovered owner into one atomically attached normal-write runtime. */ + public synchronized ArchiveHistoryWriter attachNormalWriter(OldValueCollector collector, + ArchiveBlockProjectionPreparer projectionPreparer, int queueCapacity) throws IOException { + if (state != State.RECOVERED) { + throw new IllegalStateException("State Archive owner is not recovered"); + } + ArchiveHistoryWriter writer = null; + AsyncArchiveHistorySink asyncSink = null; + ArchiveRuntimeAttachment candidate = null; + boolean attached = false; + try { + writer = new ArchiveHistoryWriter(archiveDirectory, maxSegmentSize, + new java.util.LinkedHashSet<>(participantEngines.keySet())); + if (!recoveredHead.equals(writer.committedHeadMeta())) { + throw new ArchivePersistenceException( + "Recovered archive head changed before normal writer attachment"); + } + asyncSink = new AsyncArchiveHistorySink(writer, queueCapacity); + Path checkpoint = archiveDirectory.resolve("progress").resolve("checkpoint.progress"); + Path reader = archiveDirectory.resolve("progress").resolve("reader.progress"); + ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(writer, + checkpoint, participantEngines, reader, new ArrayList<>(participantEngines.keySet()), + snapshotManager::withArchiveStateBarrier); + candidate = new ArchiveRuntimeAttachment(collector, projectionPreparer, asyncSink, + (payloads, refresh) -> publishOneTarget(coordinator, payloads, refresh)); + snapshotManager.attachArchiveRuntime(candidate); + attached = true; + attachment = candidate; + sink = asyncSink; + historyWriter = writer; + state = State.RUNNING; + return writer; + } catch (IOException | RuntimeException failure) { + if (attached) { + snapshotManager.detachArchiveRuntime(candidate); + } + if (asyncSink != null) { + try { + asyncSink.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } else if (writer != null) { + try { + writer.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + throw failure; + } + } + + public synchronized ArchiveHistoryWriter getHistoryWriter() { + if (state != State.RUNNING || historyWriter == null) { + throw new IllegalStateException("State Archive normal writer is not attached"); + } + return historyWriter; + } + + /** Machine-checks the current H=C=D[0..26]=R identity and retired mutation plan. */ + public synchronized BlockSnapshotMeta verifyNormalWriteFixedPoint() throws IOException { + ArchiveHistoryWriter writer = getHistoryWriter(); + HistoryCommitMarker head = Objects.requireNonNull(writer.committedHead(), + "archive history head"); + List names = new ArrayList<>(participantEngines.keySet()); + Path checkpointPath = archiveDirectory.resolve("progress").resolve("checkpoint.progress"); + if (new ArchiveTargetMutationPlanFile(checkpointPath).loadIfPresent() != null) { + throw new ArchivePersistenceException("Archive mutation plan is not retired"); + } + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + ArchiveProgressEnvelope checkpoint = new ArchiveProgressFile(checkpointPath, codec).load(); + ArchiveProgressEnvelope reader = new ArchiveProgressFile( + archiveDirectory.resolve("progress").resolve("reader.progress"), codec).load(); + requireAuthority(checkpoint, Kind.APPLY_CHECKPOINT, null, head, names); + requireAuthority(reader, Kind.READER_VISIBLE, null, head, names); + if (!java.util.Arrays.equals(checkpoint.getMutationPlanDigest(), + reader.getMutationPlanDigest())) { + throw new ArchivePersistenceException("Archive C/R mutation-plan identity differs"); + } + for (Map.Entry entry : participantEngines.entrySet()) { + ArchiveProgressEnvelope progress = entry.getValue().loadProgress(); + requireAuthority(progress, Kind.PARTICIPANT_PROGRESS, entry.getKey(), head, names); + if (!java.util.Arrays.equals(checkpoint.getMutationPlanDigest(), + progress.getMutationPlanDigest())) { + throw new ArchivePersistenceException( + "Archive participant mutation-plan identity differs: " + entry.getKey()); + } + } + if (snapshotManager.getArchiveReadableEpoch() != head.getMeta().getEpoch()) { + throw new ArchivePersistenceException("SnapshotManager readable epoch differs from R"); + } + return head.getMeta(); + } + /** Quiesces, detaches and closes owned resources without waiting for active query leases. */ @Override public synchronized void close() throws IOException { @@ -158,7 +265,9 @@ public synchronized void close() throws IOException { throw failure; } state = State.QUIESCING; - queryGate.quiesce(); + if (queryGate != null) { + queryGate.quiesce(); + } if (!detached) { ArchiveRuntimeAttachment returned = snapshotManager.detachArchiveRuntime(attachment); if (returned != attachment) { @@ -166,16 +275,22 @@ public synchronized void close() throws IOException { } detached = true; } - if (!queryGate.isDrained()) { + if (queryGate != null && !queryGate.isDrained()) { throw new IllegalStateException( "State Archive runtime still has active query leases: " + queryGate.getActiveLeaseCount()); } - queryGate.close(); + if (queryGate != null) { + queryGate.close(); + } IOException failure = null; - failure = closeOwned("latest coordinator", latestCoordinator, failure); - failure = closeOwned("serving catalog", servingCatalog, failure); + if (latestCoordinator != null) { + failure = closeOwned("latest coordinator", latestCoordinator, failure); + } + if (servingCatalog != null) { + failure = closeOwned("serving catalog", servingCatalog, failure); + } for (int i = participants.size() - 1; i >= 0; i--) { failure = closeOwned("archive participant " + i, participants.get(i), failure); } @@ -225,6 +340,15 @@ private static List immutableParticipants( return Collections.unmodifiableList(copy); } + private static Map immutableParticipantEngines( + Map engines) { + Map source = Objects.requireNonNull(engines, "engines"); + Map copy = new LinkedHashMap<>(); + source.forEach((name, engine) -> copy.put(Objects.requireNonNull(name, "participant name"), + Objects.requireNonNull(engine, "participant engine"))); + return Collections.unmodifiableMap(copy); + } + private void validateUniqueOwnership() { Set unique = Collections.newSetFromMap(new IdentityHashMap()); requireUnique(unique, latestCoordinator, "latestCoordinator"); @@ -235,6 +359,29 @@ private void validateUniqueOwnership() { requireUnique(unique, sink, "sink"); } + private static void publishOneTarget(ArchiveTargetApplyCoordinator coordinator, + List payloads, + ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { + if (payloads.size() != 1) { + throw new ArchivePersistenceException( + "S1 archive runtime requires one forward payload per normal flush"); + } + ArchiveBlockForwardPayload payload = payloads.get(0); + ArchiveParticipantMutationBatch batch = new ArchiveParticipantMutationBatchCollector( + payload.getAccountAssetManifest()).collect(payload.getMarker(), payload.getView()); + coordinator.apply(batch, refresh); + } + + private static void requireAuthority(ArchiveProgressEnvelope envelope, Kind kind, + String participant, HistoryCommitMarker marker, List participants) { + if (envelope == null) { + throw new ArchivePersistenceException("Missing archive authority: " + kind); + } + envelope.requireIdentity(kind, participant, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), participants); + } + private static void requireUnique(Set unique, Closeable resource, String name) { if (!unique.add(resource)) { throw new IllegalArgumentException("Archive runtime resource has multiple owners: " + name); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index 8c605405ffa..f773f6d86c7 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -788,7 +788,9 @@ private synchronized void flush(boolean force) { createCheckpoint(); long checkPointEnd = System.currentTimeMillis(); - refresh(); + if (!publishArchiveForwardStateForFlush()) { + refresh(); + } if (archiveEpoch != null) { archiveReadableEpoch = archiveEpoch; ((DurableBlockReverseDiffSink) blockReverseDiffSink).releaseThrough(archiveEpoch); @@ -814,6 +816,11 @@ private Long publishArchiveHistoryForFlush() { if (!(blockReverseDiffSink instanceof DurableBlockReverseDiffSink)) { throw new TronDBException("Archive sink cannot prove durable history before checkpoint"); } + if (archiveRuntimeAttachment != null + && archiveRuntimeAttachment.hasForwardFlushPublisher() && flushCount != 1) { + throw new TronDBException( + "S1 archive runtime requires exactly one target per normal flush"); + } FrozenBatch frozenForward = archiveBlockProjectionPreparer == null ? null : freezeArchiveForwardFlushRange(); Chainbase stateDatabase = dbs.stream() @@ -871,6 +878,20 @@ private Long publishArchiveHistoryForFlush() { return last.getEpoch(); } + private boolean publishArchiveForwardStateForFlush() { + ArchiveRuntimeAttachment runtime = archiveRuntimeAttachment; + if (runtime == null || !runtime.hasForwardFlushPublisher()) { + return false; + } + List payloads = claimArchiveForwardFlushPayloads(); + try { + runtime.publishForwardFlush(payloads, this::refresh); + return true; + } catch (IOException | RuntimeException failure) { + throw new TronDBException("Archive forward publication failed", failure); + } + } + public void createCheckpoint() { TronDatabase checkPointStore = null; try { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index ff5339aa1f2..d32f70e8678 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -115,9 +115,13 @@ import org.tron.core.db.api.MigrateTurkishKeyHelper; import org.tron.core.db.api.MoveAbiHelper; import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.AccountAssetArchiveProjector; +import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge; +import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.TargetAssetOptimization; import org.tron.core.db2.archive.ArchiveHistoryWriter; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.archive.HistoricalAccountBalanceReader; +import org.tron.core.db2.archive.SnapshotOldValueCollector; import org.tron.core.db2.archive.StateArchiveRuntimeOwner; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; @@ -643,9 +647,16 @@ private void initStateArchive() { throw new IllegalStateException( "State archive committed head differs from the persisted state root"); } + AccountAssetBlockProjectionBridge bridge = new AccountAssetBlockProjectionBridge( + new AccountAssetArchiveProjector(), + accountKey -> getAccountAssetStore().prefixQuery(accountKey)); + archiveHistoryWriter = recovered.attachNormalWriter(new SnapshotOldValueCollector(), + view -> bridge.prepare(view, TargetAssetOptimization.forTarget(view.getMeta(), + getDynamicPropertiesStore().supportAllowAssetOptimization())), + storage.getStateArchiveQueueCapacity()); stateArchiveRuntime = recovered; recovered = null; - logger.info("State archive startup recovered: directory={}, head={}, actions={}, engine={}", + logger.info("State archive runtime attached: directory={}, head={}, actions={}, engine={}", archiveDirectory, archiveHead.getBlockNumber(), stateArchiveRuntime.getStartupRecoveryActionCount(), storage.getDbEngine()); } catch (java.io.IOException | RuntimeException failure) { @@ -2759,6 +2770,7 @@ private void closeStateArchive() { try { runtime.close(); stateArchiveRuntime = null; + archiveHistoryWriter = null; } catch (java.io.IOException failure) { throw new IllegalStateException("Failed to close State Archive runtime", failure); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java index 86d2d7e61d6..cd7789358dc 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java @@ -329,18 +329,25 @@ public void completesPreparedTruncationBeforeLoadingRestartCheckpoint() throws E } @Test - public void failsClosedWhenDerivedAccountIndexIsAheadAfterRecovery() throws Exception { + public void truncatesDerivedAccountIndexToRecoveredHistoryAuthority() throws Exception { Path archive = temporaryFolder.newFolder("writer-index-ahead").toPath(); try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { writer.acceptAll(Arrays.asList(diff(1), diff(2), diff(3))); } prepareTruncation(archive, 2); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveHistoryWriter(archive, 4096, databases())); + try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter( + archive, 4096, databases())) { + assertEquals(2, reopened.committedHead().getMeta().getEpoch()); + } ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, new HistoryCommitMarkerCodec()); assertEquals(2, checkpoint.getMarker().getMeta().getEpoch()); + try (AccountChangeIndex index = new AccountChangeIndex( + archive.resolve("account-change-index"))) { + assertEquals(2, index.getIndexedThrough()); + assertTrue(index.headMatches(checkpoint.getMarker().getMeta())); + } assertFalse(Files.exists(archive.resolve("truncation.intent"))); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index f87b6b4c432..c2dad449f27 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; @@ -14,10 +15,14 @@ import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -26,9 +31,17 @@ import org.tron.core.ChainBaseManager; import org.tron.core.config.args.Storage; import org.tron.core.db.Manager; +import org.tron.core.db.common.DbSourceInter; +import org.tron.core.db2.ISession; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; import org.tron.core.db2.archive.StateArchiveRuntimeOwner.State; +import org.tron.core.db2.common.DB; +import org.tron.core.db2.common.Flusher; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.core.SnapshotRoot; +import org.tron.core.store.CheckTmpStore; import org.tron.core.store.DynamicPropertiesStore; public class StateArchiveManagerStartupIntegrationTest { @@ -39,25 +52,48 @@ public class StateArchiveManagerStartupIntegrationTest { public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void managerRecoversExact27NativeFilesBeforeOpeningProducers() throws Exception { + public void managerRunsTwoNormalFlushTargetsThroughExact27FixedPoint() throws Exception { for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { Path output = temporaryFolder.newFolder("manager-" + engine.toLowerCase()).toPath(); Path archive = output.resolve("state-archive"); HistoryCommitMarker head = initializeRecoverableTail(archive, engine); - SnapshotManager snapshots = new SnapshotManager(""); + SnapshotFixture fixture = snapshotFixture(); + SnapshotManager snapshots = fixture.snapshots; Manager manager = manager(snapshots, head); withArchiveConfig(output, engine, true, () -> invoke(manager, "initStateArchive")); - assertEquals(State.RECOVERED, manager.getStateArchiveRuntime().getState()); + assertEquals(State.RUNNING, manager.getStateArchiveRuntime().getState()); assertEquals(1, manager.getStateArchiveRuntime().getStartupRecoveryActionCount()); assertEquals(head.getMeta(), manager.getStateArchiveRuntime().getRecoveredHead()); - assertNull(manager.getArchiveHistoryWriter()); + assertNotNull(manager.getArchiveHistoryWriter()); assertEquals(-1, snapshots.getArchiveReadableEpoch()); + byte[] key = new byte[]{3, 1, 4}; + for (int epoch = 7; epoch <= 8; epoch++) { + BlockSnapshotMeta target = new BlockSnapshotMeta(epoch, epoch, hash(epoch), + hash(epoch - 1), epoch * 1_000L); + try (ISession block = snapshots.buildSession()) { + fixture.databases.get("proposal").put(key, new byte[]{(byte) epoch}); + block.commit(target); + } + setField(snapshots, "flushCount", 1); + snapshots.flushPending(); + assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + setField(snapshots, "size", 0); + } + invoke(manager, "closeStateArchive"); assertNull(manager.getStateArchiveRuntime()); + assertNull(manager.getArchiveHistoryWriter()); assertNativeParticipantsReopen(archive, engine, PARTICIPANTS); + try (Closeable participant = (Closeable) openParticipant(archive, engine, "proposal")) { + byte[] value = engine.equals("ROCKSDB") + ? ((RocksDbArchiveParticipant) participant).get(key) + : ((LevelDbArchiveParticipant) participant).get(key); + assertArrayEquals(new byte[]{8}, value); + } + snapshots.shutdown(); } } @@ -109,12 +145,35 @@ private static Manager manager(SnapshotManager snapshots, HistoryCommitMarker he .thenReturn(Sha256Hash.wrap(head.getMeta().getBlockHash())); ChainBaseManager chainBase = mock(ChainBaseManager.class); when(chainBase.getDynamicPropertiesStore()).thenReturn(properties); + ChainBaseManager.init(chainBase); Manager manager = new Manager(); setField(manager, "revokingStore", snapshots); setField(manager, "chainBaseManager", chainBase); return manager; } + private static SnapshotFixture snapshotFixture() { + if (CommonParameter.getInstance().getStorage() == null) { + CommonParameter.getInstance().storage = new Storage(); + } + SnapshotManager snapshots = new SnapshotManager(""); + Map databases = new LinkedHashMap<>(); + for (String participant : PARTICIPANTS) { + Chainbase database = new Chainbase(new SnapshotRoot(new MemoryDb(participant))); + snapshots.add(database); + databases.put(participant, database); + } + snapshots.enable(); + snapshots.setUnChecked(false); + CheckTmpStore checkpoint = mock(CheckTmpStore.class); + @SuppressWarnings("unchecked") + DbSourceInter checkpointDb = mock(DbSourceInter.class); + when(checkpointDb.iterator()).thenReturn(Collections.emptyIterator()); + when(checkpoint.getDbSource()).thenReturn(checkpointDb); + snapshots.setCheckTmpStore(checkpoint); + return new SnapshotFixture(snapshots, databases); + } + private static HistoryCommitMarker initializeRecoverableTail(Path archive, String engine) throws Exception { HistoryCommitMarker checkpoint; @@ -282,6 +341,94 @@ private static byte[] hash(int suffix) { return hash; } + private static final class SnapshotFixture { + private final SnapshotManager snapshots; + private final Map databases; + + private SnapshotFixture(SnapshotManager snapshots, Map databases) { + this.snapshots = snapshots; + this.databases = databases; + } + } + + private static final class MemoryDb implements DB, Flusher { + private final String name; + private final Map values = new LinkedHashMap<>(); + + private MemoryDb(String name) { + this.name = name; + } + + @Override + public byte[] get(byte[] key) { + byte[] value = values.get(WrappedByteArray.of(key)); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + @Override + public void put(byte[] key, byte[] value) { + values.put(WrappedByteArray.copyOf(key), Arrays.copyOf(value, value.length)); + } + + @Override + public long size() { + return values.size(); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + @Override + public void remove(byte[] key) { + values.remove(WrappedByteArray.of(key)); + } + + @Override + public Iterator> iterator() { + List> entries = new ArrayList<>(); + values.forEach((key, value) -> entries.add(new AbstractMap.SimpleImmutableEntry<>( + key.getBytes(), Arrays.copyOf(value, value.length)))); + return entries.iterator(); + } + + @Override + public void close() { + values.clear(); + } + + @Override + public void flush(Map batch) { + batch.forEach((key, value) -> { + if (value == null || value.getBytes() == null) { + values.remove(key); + } else { + values.put(WrappedByteArray.copyOf(key.getBytes()), value.getBytes()); + } + }); + } + + @Override + public void reset() { + values.clear(); + } + + @Override + public String getDbName() { + return name; + } + + @Override + public void stat() { + } + + @Override + public DB newInstance() { + return new MemoryDb(name); + } + } + @FunctionalInterface private interface ThrowingRunnable { void run() throws Exception; From 5f7e9d97ec8306d6076aef9ef25eaf2676ebf22a Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 16:19:05 +0800 Subject: [PATCH 033/161] feat(chainbase): bootstrap archive base --- .../db2/archive/ArchiveBootstrapAnchor.java | 55 ++++++++ .../db2/archive/ArchiveHistoryWriter.java | 17 ++- .../db2/archive/StateArchiveRuntimeOwner.java | 123 ++++++++++++++++++ .../main/java/org/tron/core/db/Manager.java | 27 +++- .../core/db/StateArchiveBasePreflight.java | 6 +- ...eArchiveManagerStartupIntegrationTest.java | 53 +++++++- .../archive/StateArchiveRuntimeOwnerTest.java | 55 ++++++++ 7 files changed, 324 insertions(+), 12 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBootstrapAnchor.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBootstrapAnchor.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBootstrapAnchor.java new file mode 100644 index 00000000000..7ff51cdda3b --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBootstrapAnchor.java @@ -0,0 +1,55 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; + +/** Explicit identity which makes a synthetic empty first H a non-queryable bootstrap anchor. */ +final class ArchiveBootstrapAnchor { + + private static final String PATH = "progress/bootstrap.progress"; + + private ArchiveBootstrapAnchor() { + } + + static void store(Path archiveDirectory, HistoryCommitMarker marker, byte[] planDigest, + List participants) throws IOException { + ArchiveProgressEnvelope anchor = new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, + marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), planDigest, participants); + new ArchiveProgressFile(archiveDirectory.resolve(PATH), new ArchiveProgressEnvelopeCodec()) + .store(anchor); + } + + static HistoryCommitMarker loadAndValidateIfPresent(Path archiveDirectory, + CommittedHistoryAuthority history, List participants) throws IOException { + Path path = archiveDirectory.resolve(PATH); + if (!Files.exists(path)) { + return null; + } + HistoryCommitMarker first = history.get(history.firstEpoch()); + if (first == null) { + throw new ArchivePersistenceException("Archive bootstrap anchor has no history marker"); + } + ArchiveProgressEnvelope anchor = new ArchiveProgressFile(path, + new ArchiveProgressEnvelopeCodec()).load(); + if (anchor.getMutationPlanDigest() == null) { + throw new ArchivePersistenceException("Archive bootstrap anchor plan digest is missing"); + } + anchor.requireIdentity(Kind.READER_VISIBLE, null, first.getMeta().getEpoch(), + first.getMeta().getBlockHash(), first.getBatchId(), + first.getHistoryLocation().getBodyDigest(), anchor.getMutationPlanDigest(), participants); + BlockReverseDiff diff; + if (history instanceof ArchiveHistoryWriter) { + diff = ((ArchiveHistoryWriter) history).readCommitted(first.getMeta().getEpoch()); + } else { + return first; + } + if (!diff.getGroups().isEmpty()) { + throw new ArchivePersistenceException("Archive bootstrap anchor history is not empty"); + } + return first; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java index fdd081cff98..8e9302ef2d6 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java @@ -31,6 +31,7 @@ public final class ArchiveHistoryWriter private final HistoryCommitMarkerCodec commitCodec; private final List participatingDatabases; private final DurabilityHook hook; + private final Long bootstrapFloor; public ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, Set participatingDatabases) throws IOException { @@ -64,6 +65,9 @@ public ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, closeAfterFailedConstruction(failure); throw failure; } + HistoryCommitMarker bootstrap = ArchiveBootstrapAnchor.loadAndValidateIfPresent( + archiveDirectory, this, this.participatingDatabases); + this.bootstrapFloor = bootstrap == null ? null : bootstrap.getMeta().getEpoch(); } @Override @@ -207,7 +211,7 @@ public synchronized OldValue readAccountAt(long targetBlock, byte[] address, if (head == null) { throw new IllegalStateException("State archive has no committed history"); } - long base = commits.firstEpoch() - 1; + long base = bootstrapFloor == null ? commits.firstEpoch() - 1 : bootstrapFloor; if (targetBlock < base || targetBlock > head.getMeta().getEpoch()) { throw new IllegalArgumentException("Account query is outside archive coverage"); } @@ -242,8 +246,12 @@ public synchronized PersistentServingKeyIndexGeneration buildServingGeneration( if (first == null) { throw new IllegalStateException("Cannot build a serving generation from empty history"); } - long firstEpoch = commits.firstEpoch(); + long firstEpoch = bootstrapFloor == null ? commits.firstEpoch() : bootstrapFloor + 1; long lastEpoch = commits.head().getMeta().getEpoch(); + if (firstEpoch > lastEpoch) { + throw new IllegalStateException( + "Cannot build a serving generation before post-bootstrap history exists"); + } Iterable committed = () -> new Iterator() { private long nextEpoch = firstEpoch; @@ -260,8 +268,11 @@ public HistoryCommitMarker next() { return commits.get(nextEpoch++); } }; + long baseEpoch = firstEpoch - 1; + byte[] baseHash = bootstrapFloor == null + ? first.getMeta().getParentHash() : first.getMeta().getBlockHash(); return PersistentServingKeyIndexGeneration.build(shadowDirectory, generationId, - firstEpoch - 1, first.getMeta().getParentHash(), committed, index::read, + baseEpoch, baseHash, committed, index::read, participatingDatabases, latestSourceIdentityDigest); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java index 5f6fe64a546..19800daf5f0 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java @@ -2,7 +2,11 @@ import java.io.Closeable; import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; @@ -12,8 +16,10 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.UUID; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.core.SnapshotManager; /** Sole owner for exact-27 State Archive resources from recovered startup through shutdown. */ @@ -135,6 +141,90 @@ public static StateArchiveRuntimeOwner recover(SnapshotManager snapshotManager, } } + /** + * Atomically establishes one empty-diff H/C/27D/R baseline at the persisted Chainbase head, + * then reopens it through the ordinary startup recovery path. The staging directory is never + * published until every durable authority is complete and independently recoverable. + */ + public static StateArchiveRuntimeOwner bootstrapAndRecover(SnapshotManager snapshotManager, + Path archiveDirectory, long maxSegmentSize, String databaseEngine, + BlockSnapshotMeta baseHead, Phase targetPhase) throws IOException { + Objects.requireNonNull(snapshotManager, "snapshotManager"); + Path root = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + BlockSnapshotMeta head = Objects.requireNonNull(baseHead, "baseHead"); + Phase phase = Objects.requireNonNull(targetPhase, "targetPhase"); + Path parent = Objects.requireNonNull(root.getParent(), "archive parent directory"); + requireEmptyBootstrapTarget(root); + Files.createDirectories(parent); + Path staging = parent.resolve("." + root.getFileName() + ".bootstrap-" + UUID.randomUUID()); + String engine = Objects.requireNonNull(databaseEngine, "databaseEngine") + .toUpperCase(Locale.ROOT); + if (!"LEVELDB".equals(engine) && !"ROCKSDB".equals(engine)) { + throw new IllegalArgumentException("Unsupported State Archive database engine: " + engine); + } + + List names = ArchiveParticipantDescriptor.current().getParticipants(); + List opened = new ArrayList<>(); + try { + HistoryCommitMarker marker; + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(staging, maxSegmentSize, + new java.util.LinkedHashSet<>(names))) { + writer.accept(new BlockReverseDiff(head, Collections.emptyList())); + marker = Objects.requireNonNull(writer.committedHead(), "bootstrap history head"); + } + + Map engines = new LinkedHashMap<>(); + for (String participant : names) { + Closeable nativeEngine = openParticipant( + staging.resolve("participants").resolve(participant), participant, names, engine); + opened.add(nativeEngine); + engines.put(participant, (ArchiveParticipant) nativeEngine); + } + Map> emptyMutations = new LinkedHashMap<>(); + for (String participant : names) { + emptyMutations.put(participant, Collections.emptyList()); + } + ArchiveProgressEnvelope target = progress(Kind.APPLY_CHECKPOINT, null, marker, null, names); + byte[] planDigest = new ArchiveTargetMutationPlan(target, + P66AccountAssetCodec.FORMAT_ID, phase, emptyMutations).digest(); + ArchiveBootstrapAnchor.store(staging, marker, planDigest, names); + ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); + new ArchiveProgressFile(staging.resolve("progress/checkpoint.progress"), codec) + .store(progress(Kind.APPLY_CHECKPOINT, null, marker, planDigest, names)); + for (String participant : names) { + engines.get(participant).apply(Collections.emptyList(), + progress(Kind.PARTICIPANT_PROGRESS, participant, marker, planDigest, names)); + } + new ArchiveProgressFile(staging.resolve("progress/reader.progress"), codec) + .store(progress(Kind.READER_VISIBLE, null, marker, planDigest, names)); + closeReverseOrThrow(opened); + opened.clear(); + + try (StateArchiveRuntimeOwner verified = recover(snapshotManager, staging, + maxSegmentSize, engine)) { + if (!head.equals(verified.getRecoveredHead()) + || verified.getStartupRecoveryActionCount() != 0) { + throw new ArchivePersistenceException( + "Fresh State Archive baseline did not recover at a zero-action fixed point"); + } + } + if (Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + Files.delete(root); + } + try { + Files.move(staging, root, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException failure) { + throw new ArchivePersistenceException( + "State Archive bootstrap requires atomic directory publication", failure); + } + HistorySegmentStore.syncDirectory(parent); + return recover(snapshotManager, root, maxSegmentSize, engine); + } catch (IOException | RuntimeException failure) { + closeReverse(opened, failure); + throw failure; + } + } + public synchronized State getState() { return state; } @@ -320,6 +410,39 @@ private static Closeable openParticipant(Path directory, String participant, return new LevelDbArchiveParticipant(directory, participant, participants); } + private static void requireEmptyBootstrapTarget(Path root) throws IOException { + if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + return; + } + if (Files.isSymbolicLink(root) + || !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) { + throw new ArchivePersistenceException("State Archive bootstrap target is not a directory"); + } + try (java.util.stream.Stream entries = Files.list(root)) { + if (entries.findAny().isPresent()) { + throw new ArchivePersistenceException("State Archive bootstrap target is not empty"); + } + } + } + + private static ArchiveProgressEnvelope progress(Kind kind, String participant, + HistoryCommitMarker marker, byte[] planDigest, List participants) { + return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), + marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), planDigest, participants); + } + + private static void closeReverseOrThrow(List resources) throws IOException { + IOException failure = null; + while (!resources.isEmpty()) { + int index = resources.size() - 1; + failure = closeOwned("bootstrap participant " + index, resources.remove(index), failure); + } + if (failure != null) { + throw failure; + } + } + private static void closeReverse(List resources, Exception failure) { for (int i = resources.size() - 1; i >= 0; i--) { try { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index d32f70e8678..f02c3a7dfca 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -118,9 +118,12 @@ import org.tron.core.db2.archive.AccountAssetArchiveProjector; import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge; import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.TargetAssetOptimization; +import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator.Result; +import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator.Status; import org.tron.core.db2.archive.ArchiveHistoryWriter; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.archive.HistoricalAccountBalanceReader; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.archive.SnapshotOldValueCollector; import org.tron.core.db2.archive.StateArchiveRuntimeOwner; import org.tron.core.db2.core.Chainbase; @@ -626,7 +629,7 @@ private void initStateArchive() { org.tron.core.config.args.Storage storage = Args.getInstance().getStorage(); Path archiveDirectory = Paths.get(Args.getInstance().getOutputDirectory(), storage.getStateArchiveDirectory()).normalize(); - StateArchiveBasePreflight.requireRecoverable(storage.isStateArchiveEnabled(), + Result admission = StateArchiveBasePreflight.requireAdmitted(storage.isStateArchiveEnabled(), archiveDirectory); if (!storage.isStateArchiveEnabled()) { return; @@ -636,8 +639,23 @@ private void initStateArchive() { } StateArchiveRuntimeOwner recovered = null; try { - recovered = StateArchiveRuntimeOwner.recover((SnapshotManager) revokingStore, - archiveDirectory, storage.getStateArchiveMaxSegmentSize(), storage.getDbEngine()); + if (admission.getStatus() == Status.EMPTY_NEW) { + long headNumber = getDynamicPropertiesStore().getLatestBlockHeaderNumber(); + BlockCapsule headBlock = chainBaseManager.getBlockByNum(headNumber); + BlockSnapshotMeta baseHead = BlockSnapshotMeta.forBlock(headNumber, + getDynamicPropertiesStore().getLatestBlockHeaderHash().getBytes(), + headBlock.getParentHash().getBytes(), headBlock.getTimeStamp()); + Phase phase = getDynamicPropertiesStore().supportAllowAssetOptimization() + ? Phase.P66_ON : Phase.P66_OFF; + recovered = StateArchiveRuntimeOwner.bootstrapAndRecover( + (SnapshotManager) revokingStore, archiveDirectory, + storage.getStateArchiveMaxSegmentSize(), storage.getDbEngine(), baseHead, phase); + logger.info("State archive fresh baseline published: directory={}, head={}", + archiveDirectory, headNumber); + } else { + recovered = StateArchiveRuntimeOwner.recover((SnapshotManager) revokingStore, + archiveDirectory, storage.getStateArchiveMaxSegmentSize(), storage.getDbEngine()); + } BlockSnapshotMeta archiveHead = recovered.getRecoveredHead(); if (archiveHead != null && (archiveHead.getBlockNumber() @@ -659,7 +677,8 @@ private void initStateArchive() { logger.info("State archive runtime attached: directory={}, head={}, actions={}, engine={}", archiveDirectory, archiveHead.getBlockNumber(), stateArchiveRuntime.getStartupRecoveryActionCount(), storage.getDbEngine()); - } catch (java.io.IOException | RuntimeException failure) { + } catch (java.io.IOException | BadItemException | ItemNotFoundException + | RuntimeException failure) { if (recovered != null) { try { recovered.close(); diff --git a/framework/src/main/java/org/tron/core/db/StateArchiveBasePreflight.java b/framework/src/main/java/org/tron/core/db/StateArchiveBasePreflight.java index de2fce12b93..1d0c4604cdc 100644 --- a/framework/src/main/java/org/tron/core/db/StateArchiveBasePreflight.java +++ b/framework/src/main/java/org/tron/core/db/StateArchiveBasePreflight.java @@ -12,14 +12,14 @@ final class StateArchiveBasePreflight { private StateArchiveBasePreflight() { } - static void requireAdmitted(boolean enabled, Path archiveDirectory) { + static Result requireAdmitted(boolean enabled, Path archiveDirectory) { if (!enabled) { - return; + return null; } Result result = ArchiveFormatAdmissionValidator.inspect( Objects.requireNonNull(archiveDirectory, "archiveDirectory")); if (result.getStatus() == Status.EMPTY_NEW || result.getStatus() == Status.CURRENT_BASE) { - return; + return result; } throw new IllegalStateException("State archive base requires quarantine: " + result.getReason() + ": " + result.getDetail()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index c2dad449f27..232b02041f2 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -29,6 +30,7 @@ import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.Sha256Hash; import org.tron.core.ChainBaseManager; +import org.tron.core.capsule.BlockCapsule; import org.tron.core.config.args.Storage; import org.tron.core.db.Manager; import org.tron.core.db.common.DbSourceInter; @@ -51,6 +53,44 @@ public class StateArchiveManagerStartupIntegrationTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + public void managerBootstrapsFreshBaseAndContinuesNormalFlush() throws Exception { + for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { + Path output = temporaryFolder.newFolder("fresh-manager-" + engine.toLowerCase()).toPath(); + Path archive = output.resolve("state-archive"); + BlockSnapshotMeta head = new BlockSnapshotMeta(6, 6, hash(6), hash(5), 6_000L); + SnapshotFixture fixture = snapshotFixture(); + SnapshotManager snapshots = fixture.snapshots; + Manager manager = manager(snapshots, head); + + withArchiveConfig(output, engine, true, () -> invoke(manager, "initStateArchive")); + + assertEquals(State.RUNNING, manager.getStateArchiveRuntime().getState()); + assertEquals(head, manager.getStateArchiveRuntime().getRecoveredHead()); + assertEquals(0, manager.getStateArchiveRuntime().getStartupRecoveryActionCount()); + assertTrue(Files.isRegularFile(archive.resolve("MANIFEST"))); + + byte[] key = new byte[]{2, 7, 1, 8}; + BlockSnapshotMeta target = new BlockSnapshotMeta(7, 7, hash(7), hash(6), 7_000L); + try (ISession block = snapshots.buildSession()) { + fixture.databases.get("proposal").put(key, new byte[]{7}); + block.commit(target); + } + setField(snapshots, "flushCount", 1); + snapshots.flushPending(); + assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + try (PersistentServingKeyIndexGeneration serving = manager.getArchiveHistoryWriter() + .buildServingGeneration(output.resolve("serving-" + engine.toLowerCase()), "fresh")) { + assertEquals(6, serving.getIndexedFrom()); + assertEquals(7, serving.getIndexedThrough()); + } + + invoke(manager, "closeStateArchive"); + assertNativeParticipantsReopen(archive, engine, PARTICIPANTS); + snapshots.shutdown(); + } + } + @Test public void managerRunsTwoNormalFlushTargetsThroughExact27FixedPoint() throws Exception { for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { @@ -139,12 +179,21 @@ public void disabledManagerControlDoesNotInspectOrCreateArchiveRuntime() throws private static Manager manager(SnapshotManager snapshots, HistoryCommitMarker head) throws Exception { + return manager(snapshots, head.getMeta()); + } + + private static Manager manager(SnapshotManager snapshots, BlockSnapshotMeta head) + throws Exception { DynamicPropertiesStore properties = mock(DynamicPropertiesStore.class); - when(properties.getLatestBlockHeaderNumber()).thenReturn(head.getMeta().getBlockNumber()); + when(properties.getLatestBlockHeaderNumber()).thenReturn(head.getBlockNumber()); when(properties.getLatestBlockHeaderHash()) - .thenReturn(Sha256Hash.wrap(head.getMeta().getBlockHash())); + .thenReturn(Sha256Hash.wrap(head.getBlockHash())); ChainBaseManager chainBase = mock(ChainBaseManager.class); when(chainBase.getDynamicPropertiesStore()).thenReturn(properties); + BlockCapsule headBlock = mock(BlockCapsule.class); + when(headBlock.getParentHash()).thenReturn(Sha256Hash.wrap(head.getParentHash())); + when(headBlock.getTimeStamp()).thenReturn(head.getTimestamp()); + when(chainBase.getBlockByNum(head.getBlockNumber())).thenReturn(headBlock); ChainBaseManager.init(chainBase); Manager manager = new Manager(); setField(manager, "revokingStore", snapshots); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java index 3e479f90e83..46fd54e3d52 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java @@ -1,6 +1,7 @@ package org.tron.core.db2.archive; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -11,20 +12,68 @@ import java.io.Closeable; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.OptionalLong; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedHistory; import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; import org.tron.core.db2.archive.ArchiveRuntimeQueryGate.Lease; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.archive.StateArchiveRuntimeOwner.State; import org.tron.core.db2.core.SnapshotManager; public class StateArchiveRuntimeOwnerTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void freshBootstrapPublishesRecoverableExact27FixedPoint() throws Exception { + for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { + Path parent = temporaryFolder.newFolder("bootstrap-" + engine.toLowerCase()).toPath(); + Path archive = parent.resolve("state-archive"); + BlockSnapshotMeta head = BlockSnapshotMeta.forBlock(123, hash(123), hash(122), 456_000L); + SnapshotManager snapshots = new SnapshotManager(""); + + try (StateArchiveRuntimeOwner owner = StateArchiveRuntimeOwner.bootstrapAndRecover( + snapshots, archive, 4096, engine, head, Phase.P66_ON)) { + assertEquals(head, owner.getRecoveredHead()); + assertEquals(0, owner.getStartupRecoveryActionCount()); + assertEquals(State.RECOVERED, owner.getState()); + } + + assertTrue(Files.isRegularFile(archive.resolve("MANIFEST"))); + assertTrue(Files.isRegularFile(archive.resolve("progress/checkpoint.progress"))); + assertTrue(Files.isRegularFile(archive.resolve("progress/reader.progress"))); + try (java.util.stream.Stream entries = Files.list(parent)) { + assertFalse(entries.anyMatch(path -> path.getFileName().toString() + .startsWith(".state-archive.bootstrap-"))); + } + try (StateArchiveRuntimeOwner reopened = StateArchiveRuntimeOwner.recover( + snapshots, archive, 4096, engine)) { + assertEquals(head, reopened.getRecoveredHead()); + assertEquals(0, reopened.getStartupRecoveryActionCount()); + } + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( + archive, 4096, ArchiveStoreScope.getStateDatabases())) { + byte[] address = new byte[21]; + assertThrows(IllegalArgumentException.class, + () -> writer.readAccountAt(122, address, null)); + assertFalse(writer.readAccountAt(123, address, null).isPresent()); + } + assertThrows(ArchivePersistenceException.class, + () -> StateArchiveRuntimeOwner.bootstrapAndRecover(snapshots, archive, 4096, + engine, head, Phase.P66_ON)); + } + } + @Test public void activeQueryStopsCloseAfterDetachAndDrainedRetryClosesInOrder() throws Exception { @@ -138,6 +187,12 @@ private static ArchiveReadSnapshot snapshot() throws IOException { return ArchiveReadSnapshot.pin(0, 0, hash, serving, latest, history); } + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + private static final class Fixture { private final SnapshotManager manager; private final ArchiveRuntimeAttachment attachment; From d03b20790a3e4b8299151e12aff14ac17d0eeb09 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 16:35:51 +0800 Subject: [PATCH 034/161] fix(chainbase): align archive source set --- .../archive/AccountAssetBlockProjectionBridge.java | 6 ++++-- .../ArchiveParticipantMutationBatchCollector.java | 12 ++++++++++-- .../AccountAssetBlockProjectionBridgeTest.java | 10 ++++++++-- .../StateArchiveManagerStartupIntegrationTest.java | 3 +++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java index d10c914c945..04ca5941dd3 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java @@ -95,9 +95,11 @@ private void validateBeforeProjection(BlockChangeView view, } } Collections.sort(actual); - if (!actual.equals(participants)) { + List captured = new ArrayList<>(participants); + captured.remove(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB); + if (!actual.equals(captured)) { throw new ArchivePersistenceException( - "Block projection does not cover the exact VERSIONED_STATE set"); + "Block projection source set mismatch: expected=" + captured + ", actual=" + actual); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java index 595e5df6eae..03638dd43da 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java @@ -102,9 +102,17 @@ private void requireExactCoverage(HistoryCommitMarker target, BlockChangeView vi actual.add(database.getDbName()); } Collections.sort(actual); - if (!actual.equals(participants) || !target.getDatabases().equals(participants)) { + List captured = new ArrayList<>(participants); + captured.remove(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB); + boolean legacyEmptyDerivedGroup = actual.equals(participants) + && view.getDatabases().stream() + .filter(database -> AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals( + database.getDbName())) + .allMatch(database -> database.getChanges().isEmpty()); + if ((!actual.equals(captured) && !legacyEmptyDerivedGroup) + || !target.getDatabases().equals(participants)) { throw new ArchivePersistenceException( - "Block mutation view does not cover the exact VERSIONED_STATE set"); + "Block mutation source set mismatch: expected=" + captured + ", actual=" + actual); } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java b/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java index 3f220c17996..53ab4356fb8 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java @@ -125,7 +125,7 @@ public void rejectsActivationIdentityAndCoverageBeforeAnyPhysicalRead() { () -> bridge.prepare(view, TargetAssetOptimization.forTarget(meta, true))); } - try (Fixture duplicateSource = new Fixture(participants())) { + try (Fixture duplicateSource = new Fixture(archiveParticipants())) { duplicateSource.rootPut("account", accountKey, old.toByteArray()); BlockChangeView view = duplicateSource.capture(meta, databases -> { databases.get("account").delete(accountKey); @@ -692,6 +692,12 @@ private static byte[] proposal53Key() { } private static List participants() { + List participants = archiveParticipants(); + participants.remove(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB); + return participants; + } + + private static List archiveParticipants() { List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); Collections.sort(participants); return participants; @@ -702,7 +708,7 @@ private static BlockSnapshotMeta meta(int epoch) { } private static HistoryCommitMarker marker(BlockSnapshotMeta meta) { - return marker(meta, participants()); + return marker(meta, archiveParticipants()); } private static HistoryCommitMarker marker(BlockSnapshotMeta meta, diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index 232b02041f2..c4e20e0ce09 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -208,6 +208,9 @@ private static SnapshotFixture snapshotFixture() { SnapshotManager snapshots = new SnapshotManager(""); Map databases = new LinkedHashMap<>(); for (String participant : PARTICIPANTS) { + if (AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(participant)) { + continue; + } Chainbase database = new Chainbase(new SnapshotRoot(new MemoryDb(participant))); snapshots.add(database); databases.put(participant, database); From f5f5496114a2435cc2fe181abba311742eafb273 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 16:42:48 +0800 Subject: [PATCH 035/161] fix(chainbase): bind archive flush topology --- .../tron/core/db2/core/SnapshotManager.java | 17 +++++++---- ...eArchiveManagerStartupIntegrationTest.java | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index f773f6d86c7..14482e53ea5 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -111,6 +111,7 @@ public class SnapshotManager implements RevokingDatabase { private FrozenBatch pendingArchiveForwardFlush; private List sealedArchiveForwardFlush; private Long submittedArchiveForwardHistoryEpoch; + private Integer archiveForwardTopologySize; private BlockReverseDiffSink blockReverseDiffSink; @Getter private volatile long archiveReadableEpoch = -1; @@ -158,6 +159,7 @@ public synchronized ISession buildSession(boolean forceEnable) { } if (size > maxSize.get() && !hitDown) { + archiveForwardTopologySize = size; flushCount = flushCount + (size - maxSize.get()); updateSolidity(size - maxSize.get()); size = maxSize.get(); @@ -429,12 +431,13 @@ public synchronized FrozenBatch freezeArchiveForwardFlushRange() { if (archiveBlockProjectionPreparer == null) { throw new IllegalStateException("Archive projection preparer is not installed"); } - if (flushCount <= 0 || flushCount > size) { + int topologySize = archiveForwardTopologySize == null ? size : archiveForwardTopologySize; + if (flushCount <= 0 || flushCount > topologySize) { throw new IllegalStateException("Archive forward flush range is empty or exceeds topology"); } - List topology = stateLayerMetas(); - if (topology.size() != size) { + List topology = stateLayerMetas(topologySize); + if (topology.size() != topologySize) { throw new IllegalStateException("Archive state topology size mismatch"); } java.util.Set unique = new java.util.LinkedHashSet<>(topology); @@ -516,15 +519,15 @@ private FrozenBatch requirePendingArchiveForwardFlush() { return pendingArchiveForwardFlush; } - private List stateLayerMetas() { + private List stateLayerMetas(int layerCount) { List reference = null; for (Chainbase db : dbs) { if (!ArchiveStoreScope.isStateDatabase(db.getDbName())) { continue; } - List candidate = new ArrayList<>(size); + List candidate = new ArrayList<>(layerCount); Snapshot next = db.getHead().getRoot(); - for (int i = 0; i < size; i++) { + for (int i = 0; i < layerCount; i++) { next = next.getNext(); if (!Snapshot.isImpl(next)) { throw new IllegalStateException("Archive state topology is missing a snapshot layer"); @@ -686,6 +689,7 @@ private synchronized void abortArchiveForwardPayloads() { } sealedArchiveForwardFlush = null; submittedArchiveForwardHistoryEpoch = null; + archiveForwardTopologySize = null; for (Map.Entry entry : archiveForwardPayloadOwners.entrySet()) { AccountAssetPreparedBlockPayloadOwner owner = entry.getValue(); @@ -796,6 +800,7 @@ private synchronized void flush(boolean force) { ((DurableBlockReverseDiffSink) blockReverseDiffSink).releaseThrough(archiveEpoch); } flushCount = 0; + archiveForwardTopologySize = null; logger.info("Flush cost: {} ms, create checkpoint cost: {} ms, refresh cost: {} ms.", System.currentTimeMillis() - start, checkPointEnd - start, diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index c4e20e0ce09..1feb16ea31d 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -53,6 +53,36 @@ public class StateArchiveManagerStartupIntegrationTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + public void automaticOverflowFlushKeepsForwardRegistryBoundToFullTopology() throws Exception { + Path output = temporaryFolder.newFolder("overflow-manager").toPath(); + Path archive = output.resolve("state-archive"); + HistoryCommitMarker head = initializeRecoverableTail(archive, "ROCKSDB"); + SnapshotFixture fixture = snapshotFixture(); + SnapshotManager snapshots = fixture.snapshots; + snapshots.setMaxSize(1); + Manager manager = manager(snapshots, head); + + withArchiveConfig(output, "ROCKSDB", true, () -> invoke(manager, "initStateArchive")); + + byte[] key = new byte[]{1, 6, 1, 8}; + for (int epoch = 7; epoch <= 9; epoch++) { + BlockSnapshotMeta target = new BlockSnapshotMeta(epoch, epoch, hash(epoch), + hash(epoch - 1), epoch * 1_000L); + try (ISession block = snapshots.buildSession()) { + fixture.databases.get("proposal").put(key, new byte[]{(byte) epoch}); + block.commit(target); + } + if (epoch == 9) { + assertEquals(new BlockSnapshotMeta(7, 7, hash(7), hash(6), 7_000L), + manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + } + } + + invoke(manager, "closeStateArchive"); + snapshots.shutdown(); + } + @Test public void managerBootstrapsFreshBaseAndContinuesNormalFlush() throws Exception { for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { From 776f7cb87a5c72960bd03b61e125c1d84027bb57 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 17:54:18 +0800 Subject: [PATCH 036/161] feat(chainbase): support multi-target archive flush --- .../db2/archive/ArchiveRuntimeAttachment.java | 2 +- .../db2/archive/StateArchiveRuntimeOwner.java | 31 +++++++++----- .../tron/core/db2/core/SnapshotManager.java | 23 ++++++----- ...eArchiveManagerStartupIntegrationTest.java | 41 +++++++++++++++++++ 4 files changed, 76 insertions(+), 21 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java index 3008fe4fd60..9fffc66b061 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java @@ -50,7 +50,7 @@ public void publishForwardFlush(List payloads, forwardFlushPublisher.publish(payloads, refresh); } - /** Publishes one frozen normal-flush target through C/D, refresh and R. */ + /** Publishes one frozen normal-flush range target-by-target through C/D, refresh and R. */ @FunctionalInterface public interface ForwardFlushPublisher { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java index 19800daf5f0..0691e9183ef 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java @@ -264,7 +264,7 @@ public synchronized ArchiveHistoryWriter attachNormalWriter(OldValueCollector co checkpoint, participantEngines, reader, new ArrayList<>(participantEngines.keySet()), snapshotManager::withArchiveStateBarrier); candidate = new ArchiveRuntimeAttachment(collector, projectionPreparer, asyncSink, - (payloads, refresh) -> publishOneTarget(coordinator, payloads, refresh)); + (payloads, refresh) -> publishTargets(coordinator, payloads, refresh)); snapshotManager.attachArchiveRuntime(candidate); attached = true; attachment = candidate; @@ -482,17 +482,28 @@ private void validateUniqueOwnership() { requireUnique(unique, sink, "sink"); } - private static void publishOneTarget(ArchiveTargetApplyCoordinator coordinator, + private static void publishTargets(ArchiveTargetApplyCoordinator coordinator, List payloads, ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { - if (payloads.size() != 1) { - throw new ArchivePersistenceException( - "S1 archive runtime requires one forward payload per normal flush"); - } - ArchiveBlockForwardPayload payload = payloads.get(0); - ArchiveParticipantMutationBatch batch = new ArchiveParticipantMutationBatchCollector( - payload.getAccountAssetManifest()).collect(payload.getMarker(), payload.getView()); - coordinator.apply(batch, refresh); + if (payloads.isEmpty()) { + throw new ArchivePersistenceException("Archive normal flush has no forward payload"); + } + BlockSnapshotMeta previous = null; + for (ArchiveBlockForwardPayload payload : payloads) { + BlockSnapshotMeta current = Objects.requireNonNull(payload, "forward payload").getMeta(); + if (previous != null && (current.getEpoch() != previous.getEpoch() + 1 + || current.getBlockNumber() != previous.getBlockNumber() + 1 + || !java.util.Arrays.equals(current.getParentHash(), previous.getBlockHash()))) { + throw new ArchivePersistenceException( + "Archive normal flush forward payloads are not contiguous"); + } + previous = current; + } + for (ArchiveBlockForwardPayload payload : payloads) { + ArchiveParticipantMutationBatch batch = new ArchiveParticipantMutationBatchCollector( + payload.getAccountAssetManifest()).collect(payload.getMarker(), payload.getView()); + coordinator.apply(batch, refresh); + } } private static void requireAuthority(ArchiveProgressEnvelope envelope, Kind kind, diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index 14482e53ea5..d5fbdb4c30e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -713,6 +713,10 @@ public boolean shouldBeRefreshed() { } private void refresh() { + refresh(flushCount); + } + + private void refresh(int count) { List> futures = new ArrayList<>(dbs.size()); Chainbase properties = null; if (oldValueCollector != null) { @@ -723,14 +727,14 @@ private void refresh() { if (properties != null) { // Account root projection reads the durable optimization flag. Make that dependency // deterministic when archive mode projects account-asset changes at block boundaries. - refreshOne(properties); + refreshOne(properties, count); } } for (Chainbase db : dbs) { if (db == properties) { continue; } - futures.add(flushServices.get(db.getDbName()).submit(() -> refreshOne(db))); + futures.add(flushServices.get(db.getDbName()).submit(() -> refreshOne(db, count))); } Future future = Futures.allAsList(futures); try { @@ -743,7 +747,7 @@ private void refresh() { } } - private void refreshOne(Chainbase db) { + private void refreshOne(Chainbase db, int count) { if (Snapshot.isRoot(db.getHead())) { return; } @@ -752,7 +756,7 @@ private void refreshOne(Chainbase db) { SnapshotRoot root = (SnapshotRoot) db.getHead().getRoot(); Snapshot next = root; - for (int i = 0; i < flushCount; ++i) { + for (int i = 0; i < count; ++i) { next = next.getNext(); snapshots.add(next); } @@ -821,11 +825,6 @@ private Long publishArchiveHistoryForFlush() { if (!(blockReverseDiffSink instanceof DurableBlockReverseDiffSink)) { throw new TronDBException("Archive sink cannot prove durable history before checkpoint"); } - if (archiveRuntimeAttachment != null - && archiveRuntimeAttachment.hasForwardFlushPublisher() && flushCount != 1) { - throw new TronDBException( - "S1 archive runtime requires exactly one target per normal flush"); - } FrozenBatch frozenForward = archiveBlockProjectionPreparer == null ? null : freezeArchiveForwardFlushRange(); Chainbase stateDatabase = dbs.stream() @@ -890,13 +889,17 @@ private boolean publishArchiveForwardStateForFlush() { } List payloads = claimArchiveForwardFlushPayloads(); try { - runtime.publishForwardFlush(payloads, this::refresh); + runtime.publishForwardFlush(payloads, this::refreshOneArchiveTarget); return true; } catch (IOException | RuntimeException failure) { throw new TronDBException("Archive forward publication failed", failure); } } + private void refreshOneArchiveTarget() { + refresh(1); + } + public void createCheckpoint() { TronDatabase checkPointStore = null; try { diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index 1feb16ea31d..8bcfc0ccf29 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -167,6 +167,47 @@ public void managerRunsTwoNormalFlushTargetsThroughExact27FixedPoint() throws Ex } } + @Test + public void managerRunsMultiTargetNormalFlushThroughExact27FixedPoint() throws Exception { + for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { + Path output = temporaryFolder.newFolder("multi-target-" + engine.toLowerCase()).toPath(); + Path archive = output.resolve("state-archive"); + HistoryCommitMarker head = initializeRecoverableTail(archive, engine); + SnapshotFixture fixture = snapshotFixture(); + SnapshotManager snapshots = fixture.snapshots; + Manager manager = manager(snapshots, head); + + withArchiveConfig(output, engine, true, () -> invoke(manager, "initStateArchive")); + + byte[] key = new byte[]{3, 1, 5}; + BlockSnapshotMeta target = null; + for (int epoch = 7; epoch <= 8; epoch++) { + target = new BlockSnapshotMeta(epoch, epoch, hash(epoch), + hash(epoch - 1), epoch * 1_000L); + try (ISession block = snapshots.buildSession()) { + fixture.databases.get("proposal").put(key, new byte[]{(byte) epoch}); + block.commit(target); + } + } + setField(snapshots, "flushCount", 2); + + snapshots.flushPending(); + + assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertTrue(fixture.databases.values().stream() + .allMatch(database -> database.getHead() instanceof SnapshotRoot)); + invoke(manager, "closeStateArchive"); + assertNativeParticipantsReopen(archive, engine, PARTICIPANTS); + try (Closeable participant = (Closeable) openParticipant(archive, engine, "proposal")) { + byte[] value = engine.equals("ROCKSDB") + ? ((RocksDbArchiveParticipant) participant).get(key) + : ((LevelDbArchiveParticipant) participant).get(key); + assertArrayEquals(new byte[]{8}, value); + } + snapshots.shutdown(); + } + } + @Test public void partialParticipantOpenRollsBackAndPreservesFailureEvidence() throws Exception { for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { From 6f99995997df0b26d05afb2fa6340015c989e80b Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 18:10:42 +0800 Subject: [PATCH 037/161] fix(chainbase): bind batched flush topology --- .../tron/core/db2/core/SnapshotManager.java | 20 ++++--------- ...eArchiveManagerStartupIntegrationTest.java | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index d5fbdb4c30e..def4ccc882d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -111,7 +111,6 @@ public class SnapshotManager implements RevokingDatabase { private FrozenBatch pendingArchiveForwardFlush; private List sealedArchiveForwardFlush; private Long submittedArchiveForwardHistoryEpoch; - private Integer archiveForwardTopologySize; private BlockReverseDiffSink blockReverseDiffSink; @Getter private volatile long archiveReadableEpoch = -1; @@ -159,7 +158,6 @@ public synchronized ISession buildSession(boolean forceEnable) { } if (size > maxSize.get() && !hitDown) { - archiveForwardTopologySize = size; flushCount = flushCount + (size - maxSize.get()); updateSolidity(size - maxSize.get()); size = maxSize.get(); @@ -431,15 +429,10 @@ public synchronized FrozenBatch freezeArchiveForwardFlushRange() { if (archiveBlockProjectionPreparer == null) { throw new IllegalStateException("Archive projection preparer is not installed"); } - int topologySize = archiveForwardTopologySize == null ? size : archiveForwardTopologySize; - if (flushCount <= 0 || flushCount > topologySize) { + List topology = stateLayerMetas(); + if (flushCount <= 0 || flushCount > topology.size()) { throw new IllegalStateException("Archive forward flush range is empty or exceeds topology"); } - - List topology = stateLayerMetas(topologySize); - if (topology.size() != topologySize) { - throw new IllegalStateException("Archive state topology size mismatch"); - } java.util.Set unique = new java.util.LinkedHashSet<>(topology); if (unique.size() != topology.size()) { throw new IllegalStateException("Archive state topology contains duplicate block metadata"); @@ -519,15 +512,16 @@ private FrozenBatch requirePendingArchiveForwardFlush() { return pendingArchiveForwardFlush; } - private List stateLayerMetas(int layerCount) { + private List stateLayerMetas() { List reference = null; for (Chainbase db : dbs) { if (!ArchiveStoreScope.isStateDatabase(db.getDbName())) { continue; } - List candidate = new ArrayList<>(layerCount); + List candidate = new ArrayList<>(); + Snapshot head = db.getHead(); Snapshot next = db.getHead().getRoot(); - for (int i = 0; i < layerCount; i++) { + while (next != head) { next = next.getNext(); if (!Snapshot.isImpl(next)) { throw new IllegalStateException("Archive state topology is missing a snapshot layer"); @@ -689,7 +683,6 @@ private synchronized void abortArchiveForwardPayloads() { } sealedArchiveForwardFlush = null; submittedArchiveForwardHistoryEpoch = null; - archiveForwardTopologySize = null; for (Map.Entry entry : archiveForwardPayloadOwners.entrySet()) { AccountAssetPreparedBlockPayloadOwner owner = entry.getValue(); @@ -804,7 +797,6 @@ private synchronized void flush(boolean force) { ((DurableBlockReverseDiffSink) blockReverseDiffSink).releaseThrough(archiveEpoch); } flushCount = 0; - archiveForwardTopologySize = null; logger.info("Flush cost: {} ms, create checkpoint cost: {} ms, refresh cost: {} ms.", System.currentTimeMillis() - start, checkPointEnd - start, diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index 8bcfc0ccf29..b2cd37b3e79 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -83,6 +83,36 @@ public void automaticOverflowFlushKeepsForwardRegistryBoundToFullTopology() thro snapshots.shutdown(); } + @Test + public void batchedAutomaticOverflowFlushUsesCompletePhysicalTopology() throws Exception { + Path output = temporaryFolder.newFolder("batched-overflow-manager").toPath(); + Path archive = output.resolve("state-archive"); + HistoryCommitMarker head = initializeRecoverableTail(archive, "ROCKSDB"); + SnapshotFixture fixture = snapshotFixture(); + SnapshotManager snapshots = fixture.snapshots; + snapshots.setMaxSize(1); + snapshots.setMaxFlushCount(2); + Manager manager = manager(snapshots, head); + + withArchiveConfig(output, "ROCKSDB", true, () -> invoke(manager, "initStateArchive")); + + byte[] key = new byte[]{1, 6, 1, 9}; + for (int epoch = 7; epoch <= 10; epoch++) { + BlockSnapshotMeta target = new BlockSnapshotMeta(epoch, epoch, hash(epoch), + hash(epoch - 1), epoch * 1_000L); + try (ISession block = snapshots.buildSession()) { + fixture.databases.get("proposal").put(key, new byte[]{(byte) epoch}); + block.commit(target); + } + } + + assertEquals(new BlockSnapshotMeta(8, 8, hash(8), hash(7), 8_000L), + manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertEquals(2, snapshots.getArchiveForwardPayloadOwnerCount()); + invoke(manager, "closeStateArchive"); + snapshots.shutdown(); + } + @Test public void managerBootstrapsFreshBaseAndContinuesNormalFlush() throws Exception { for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { From 14b5a84aaf378cbd82d7c6d1d184f3e2060b00f8 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 18:22:53 +0800 Subject: [PATCH 038/161] feat(chainbase): add path state commitment codec --- .../stateroot/PathStateCommitmentCodec.java | 149 ++++++++++++++++++ .../PathStateCommitmentCodecTest.java | 126 +++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java new file mode 100644 index 00000000000..169de35877b --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java @@ -0,0 +1,149 @@ +package org.tron.core.db2.stateroot; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; +import org.tron.common.crypto.Hash; + +/** + * Experimental byte contract for the TASK-016 current path-state commitment. + * + *

This codec is independent from the existing account trie and State Archive formats. Its + * output is not a persistent compatibility promise until the H1-L1 gate approves the root domain + * and golden vectors. + */ +public final class PathStateCommitmentCodec { + + public static final int FORMAT_VERSION = 1; + public static final int ROOT_LENGTH = 32; + + private static final byte PRESENT_TAG = 1; + private static final byte[] STORE_LEAF_KEY_DOMAIN = + "java-tron/path-state/store-leaf-key".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SUPER_LEAF_KEY_DOMAIN = + "java-tron/path-state/super-leaf-key".getBytes(StandardCharsets.US_ASCII); + private static final int RLP_SHORT_LIMIT = 56; + private static final int RLP_SHORT_ITEM_OFFSET = 0x80; + private static final int RLP_LONG_ITEM_OFFSET = 0xb7; + private static final int RLP_SHORT_LIST_OFFSET = 0xc0; + private static final int RLP_LONG_LIST_OFFSET = 0xf7; + + private PathStateCommitmentCodec() { + } + + /** Returns the secure per-Store trie key for one canonical present value. */ + public static byte[] storeLeafKey(int stableStoreId, byte[] canonicalKey) { + requireStoreId(stableStoreId); + byte[] key = nonEmpty(canonicalKey, "canonicalKey"); + ByteBuffer material = ByteBuffer.allocate(Short.BYTES + STORE_LEAF_KEY_DOMAIN.length + + Short.BYTES + Integer.BYTES + Integer.BYTES + key.length); + putDomain(material, STORE_LEAF_KEY_DOMAIN); + material.putShort((short) FORMAT_VERSION); + material.putInt(stableStoreId); + material.putInt(key.length); + material.put(key); + return Hash.sha3(material.array()); + } + + /** Encodes PRESENT(empty) distinctly from PRESENT(0x00); ABSENT has no leaf. */ + public static byte[] presentLeafValue(byte[] canonicalValue) { + byte[] value = Arrays.copyOf(Objects.requireNonNull(canonicalValue, "canonicalValue"), + canonicalValue.length); + return rlpList(new byte[]{PRESENT_TAG}, value); + } + + /** Returns the secure super-trie key for one stable Store identity. */ + public static byte[] superLeafKey(int stableStoreId) { + requireStoreId(stableStoreId); + ByteBuffer material = ByteBuffer.allocate(Short.BYTES + SUPER_LEAF_KEY_DOMAIN.length + + Short.BYTES + Integer.BYTES); + putDomain(material, SUPER_LEAF_KEY_DOMAIN); + material.putShort((short) FORMAT_VERSION); + material.putInt(stableStoreId); + return Hash.sha3(material.array()); + } + + /** Encodes a Store identity and current Store root as one super-trie leaf value. */ + public static byte[] superLeafValue(int stableStoreId, String dbName, int storeFormatVersion, + byte[] storeRoot) { + requireStoreId(stableStoreId); + if (storeFormatVersion <= 0) { + throw new IllegalArgumentException("storeFormatVersion must be positive"); + } + String name = Objects.requireNonNull(dbName, "dbName"); + byte[] encodedName = name.getBytes(StandardCharsets.UTF_8); + if (encodedName.length == 0 || encodedName.length > 128) { + throw new IllegalArgumentException("dbName must encode to 1..128 bytes"); + } + byte[] root = Objects.requireNonNull(storeRoot, "storeRoot"); + if (root.length != ROOT_LENGTH) { + throw new IllegalArgumentException("storeRoot must be exactly 32 bytes"); + } + return rlpList(intBytes(stableStoreId), encodedName, intBytes(storeFormatVersion), root); + } + + private static void putDomain(ByteBuffer target, byte[] domain) { + target.putShort((short) domain.length); + target.put(domain); + } + + private static byte[] intBytes(int value) { + return ByteBuffer.allocate(Integer.BYTES).putInt(value).array(); + } + + private static void requireStoreId(int stableStoreId) { + if (stableStoreId <= 0) { + throw new IllegalArgumentException("stableStoreId must be positive"); + } + } + + private static byte[] nonEmpty(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length == 0) { + throw new IllegalArgumentException(name + " must not be empty"); + } + return copy; + } + + private static byte[] rlpList(byte[]... rawItems) { + byte[][] encoded = new byte[rawItems.length][]; + int payloadLength = 0; + for (int i = 0; i < rawItems.length; i++) { + encoded[i] = rlpItem(Objects.requireNonNull(rawItems[i], "raw RLP item")); + payloadLength = Math.addExact(payloadLength, encoded[i].length); + } + byte[] prefix = rlpLength(payloadLength, RLP_SHORT_LIST_OFFSET, RLP_LONG_LIST_OFFSET); + ByteBuffer result = ByteBuffer.allocate(Math.addExact(prefix.length, payloadLength)); + result.put(prefix); + for (byte[] item : encoded) { + result.put(item); + } + return result.array(); + } + + private static byte[] rlpItem(byte[] raw) { + if (raw.length == 1 && (raw[0] & 0xff) < RLP_SHORT_ITEM_OFFSET) { + return Arrays.copyOf(raw, raw.length); + } + byte[] prefix = rlpLength(raw.length, RLP_SHORT_ITEM_OFFSET, RLP_LONG_ITEM_OFFSET); + ByteBuffer result = ByteBuffer.allocate(Math.addExact(prefix.length, raw.length)); + result.put(prefix); + result.put(raw); + return result.array(); + } + + private static byte[] rlpLength(int length, int shortOffset, int longOffset) { + if (length < RLP_SHORT_LIMIT) { + return new byte[]{(byte) (shortOffset + length)}; + } + int lengthOfLength = (Integer.SIZE - Integer.numberOfLeadingZeros(length) + 7) / 8; + byte[] encoded = new byte[lengthOfLength + 1]; + encoded[0] = (byte) (longOffset + lengthOfLength); + for (int i = lengthOfLength; i > 0; i--) { + encoded[i] = (byte) length; + length >>>= Byte.SIZE; + } + return encoded; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java new file mode 100644 index 00000000000..61c68f6e7ad --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java @@ -0,0 +1,126 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.bouncycastle.jcajce.provider.digest.Keccak; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.tron.common.crypto.Hash; +import org.tron.core.capsule.utils.RLP; + +public class PathStateCommitmentCodecTest { + + private static final byte[] STORE_DOMAIN = + "java-tron/path-state/store-leaf-key".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SUPER_DOMAIN = + "java-tron/path-state/super-leaf-key".getBytes(StandardCharsets.US_ASCII); + + @Test + public void fixedStoreKeyGoldensMatchIndependentKeccakOracle() throws Exception { + byte[] accountKey = Hex.decode("410000000000000000000000000000000000000001"); + byte[] storageKey = new byte[32]; + for (int i = 0; i < storageKey.length; i++) { + storageKey[i] = (byte) i; + } + + assertGolden("0b7f18d3381a9e44da93058f4214d7f0d818d1824be0f30839ed5200eb7f946a", + 4, accountKey); + assertGolden("90ad9575451bd26f005db5063deaeac71d8b221edd0a0bb0a345f21b40c16ff5", + 22, storageKey); + assertGolden("ec6a48ade48f24cd456a89de3a5f86d282b0ae9892f265be58e5e8a704856350", + 21, new byte[]{1}); + } + + @Test + public void presentValuesKeepEmptyAndZeroDistinct() { + assertArrayEquals(Hex.decode("c20180"), + PathStateCommitmentCodec.presentLeafValue(new byte[0])); + assertArrayEquals(Hex.decode("c20100"), + PathStateCommitmentCodec.presentLeafValue(new byte[]{0})); + assertArrayEquals(Hex.decode("c7018568656c6c6f"), + PathStateCommitmentCodec.presentLeafValue("hello".getBytes(StandardCharsets.US_ASCII))); + assertFalse(Arrays.equals(PathStateCommitmentCodec.presentLeafValue(new byte[0]), + PathStateCommitmentCodec.presentLeafValue(new byte[]{0}))); + } + + @Test + public void superLeafGoldensBindStableIdentityFormatAndRoot() throws Exception { + byte[] storeRoot = new byte[32]; + for (int i = 0; i < storeRoot.length; i++) { + storeRoot[i] = (byte) i; + } + + assertArrayEquals( + Hex.decode("cf8715b85b2ac18d2b63e57b9e8902887f1986df7dc6a46da01fbc1f8f99f8bf"), + PathStateCommitmentCodec.superLeafKey(4)); + assertArrayEquals(referenceSuperKey(4), PathStateCommitmentCodec.superLeafKey(4)); + assertArrayEquals(Hex.decode("f38400000004876163636f756e748400000001a0000102030405060708090a0b" + + "0c0d0e0f101112131415161718191a1b1c1d1e1f"), + PathStateCommitmentCodec.superLeafValue(4, "account", 1, storeRoot)); + assertArrayEquals(referenceSuperValue(4, "account", 1, storeRoot), + PathStateCommitmentCodec.superLeafValue(4, "account", 1, storeRoot)); + } + + @Test + public void rejectsAmbiguousOrUnboundInputs() { + assertThrows(IllegalArgumentException.class, + () -> PathStateCommitmentCodec.storeLeafKey(0, new byte[]{1})); + assertThrows(IllegalArgumentException.class, + () -> PathStateCommitmentCodec.storeLeafKey(1, new byte[0])); + assertThrows(NullPointerException.class, + () -> PathStateCommitmentCodec.presentLeafValue(null)); + assertThrows(IllegalArgumentException.class, + () -> PathStateCommitmentCodec.superLeafValue(1, "", 1, new byte[32])); + assertThrows(IllegalArgumentException.class, + () -> PathStateCommitmentCodec.superLeafValue(1, "account", 0, new byte[32])); + assertThrows(IllegalArgumentException.class, + () -> PathStateCommitmentCodec.superLeafValue(1, "account", 1, new byte[31])); + } + + private static void assertGolden(String expectedHex, int storeId, byte[] key) + throws IOException { + byte[] actual = PathStateCommitmentCodec.storeLeafKey(storeId, key); + assertArrayEquals(Hex.decode(expectedHex), actual); + assertArrayEquals(referenceStoreKey(storeId, key), actual); + } + + private static byte[] referenceStoreKey(int storeId, byte[] key) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream output = new DataOutputStream(bytes)) { + output.writeShort(STORE_DOMAIN.length); + output.write(STORE_DOMAIN); + output.writeShort(PathStateCommitmentCodec.FORMAT_VERSION); + output.writeInt(storeId); + output.writeInt(key.length); + output.write(key); + } + return new Keccak.Digest256().digest(bytes.toByteArray()); + } + + private static byte[] referenceSuperKey(int storeId) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream output = new DataOutputStream(bytes)) { + output.writeShort(SUPER_DOMAIN.length); + output.write(SUPER_DOMAIN); + output.writeShort(PathStateCommitmentCodec.FORMAT_VERSION); + output.writeInt(storeId); + } + return new Keccak.Digest256().digest(bytes.toByteArray()); + } + + private static byte[] referenceSuperValue(int storeId, String dbName, int formatVersion, + byte[] storeRoot) { + return RLP.encodeList(Hash.encodeElement(ByteBuffer.allocate(4).putInt(storeId).array()), + Hash.encodeElement(dbName.getBytes(StandardCharsets.UTF_8)), + Hash.encodeElement(ByteBuffer.allocate(4).putInt(formatVersion).array()), + Hash.encodeElement(storeRoot)); + } +} From db7c00513d10ed582a23a51fd9a8790d79aef527 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 18:34:42 +0800 Subject: [PATCH 039/161] feat(chainbase): add path-addressed trie core --- .../core/db2/stateroot/PathMerkleTrie.java | 294 ++++++++++++++++++ .../core/db2/stateroot/PathNodeStore.java | 17 + .../db2/stateroot/PathMerkleTrieTest.java | 154 +++++++++ .../PathStateCommitmentCodecTest.java | 10 + 4 files changed, 475 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathNodeStore.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java new file mode 100644 index 00000000000..8e23c2af65c --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -0,0 +1,294 @@ +package org.tron.core.db2.stateroot; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import org.tron.common.crypto.Hash; + +/** + * Backend-neutral secure-key Merkle Patricia trie with path-addressed node persistence. + * + *

This TASK-016 P1 core deliberately owns no database, block, history, or recovery lifecycle. + * It rebuilds the canonical node set from current leaves when committed, then reconciles that set + * through {@link PathNodeStore}. The later durable backend can replace the rebuild strategy without + * changing the node byte contract. + */ +public final class PathMerkleTrie { + + public static final int SECURE_KEY_LENGTH = 32; + + private static final byte[] EMPTY_PATH = new byte[0]; + private static final byte[] EMPTY_RLP_ITEM = new byte[]{(byte) 0x80}; + private static final Comparator UNSIGNED_KEY_COMPARATOR = (left, right) -> { + byte[] leftBytes = left.bytes; + byte[] rightBytes = right.bytes; + int length = Math.min(leftBytes.length, rightBytes.length); + for (int i = 0; i < length; i++) { + int compared = Integer.compare(leftBytes[i] & 0xff, rightBytes[i] & 0xff); + if (compared != 0) { + return compared; + } + } + return Integer.compare(leftBytes.length, rightBytes.length); + }; + + private final PathNodeStore nodeStore; + private final Map leaves = new TreeMap<>(UNSIGNED_KEY_COMPARATOR); + private final Set committedPaths = new LinkedHashSet<>(); + private byte[] rootHash = Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length); + private boolean dirty; + + public PathMerkleTrie(PathNodeStore nodeStore) { + this.nodeStore = Objects.requireNonNull(nodeStore, "nodeStore"); + } + + public void put(byte[] secureKey, byte[] encodedValue) { + BytesKey key = secureKey(secureKey); + byte[] value = nonEmpty(encodedValue, "encodedValue"); + byte[] previous = leaves.put(key, value); + dirty |= !Arrays.equals(previous, value); + } + + public void delete(byte[] secureKey) { + dirty |= leaves.remove(secureKey(secureKey)) != null; + } + + public byte[] get(byte[] secureKey) { + byte[] value = leaves.get(secureKey(secureKey)); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + /** Reconciles path-addressed nodes and returns the canonical root hash. */ + public byte[] rootHash() { + if (dirty) { + commit(); + } + return Arrays.copyOf(rootHash, rootHash.length); + } + + public int size() { + return leaves.size(); + } + + private void commit() { + Map nextNodes = new LinkedHashMap<>(); + if (leaves.isEmpty()) { + rootHash = Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length); + } else { + List entries = new ArrayList<>(leaves.size()); + for (Map.Entry entry : leaves.entrySet()) { + entries.add(new Leaf(toNibbles(entry.getKey().bytes), entry.getValue())); + } + byte[] root = build(entries, 0, EMPTY_PATH, nextNodes); + rootHash = Hash.sha3(root); + } + + Set stalePaths = new LinkedHashSet<>(committedPaths); + stalePaths.removeAll(nextNodes.keySet()); + for (BytesKey stalePath : stalePaths) { + nodeStore.delete(stalePath.copy()); + } + for (Map.Entry entry : nextNodes.entrySet()) { + byte[] existing = nodeStore.get(entry.getKey().bytes); + if (!Arrays.equals(existing, entry.getValue())) { + nodeStore.put(entry.getKey().copy(), Arrays.copyOf(entry.getValue(), entry.getValue().length)); + } + } + committedPaths.clear(); + committedPaths.addAll(nextNodes.keySet()); + dirty = false; + } + + private static byte[] build(List entries, int depth, byte[] nodePath, + Map nodes) { + if (entries.size() == 1) { + Leaf leaf = entries.get(0); + byte[] encoded = rlpList(rlpItem(compactPath(leaf.nibbles, depth, true)), + rlpItem(leaf.value)); + nodes.put(new BytesKey(nodePath), encoded); + return encoded; + } + + int shared = sharedPrefix(entries, depth); + if (shared > 0) { + byte[] childPath = append(nodePath, entries.get(0).nibbles, depth, shared); + byte[] child = build(entries, depth + shared, childPath, nodes); + byte[] prefix = Arrays.copyOfRange(entries.get(0).nibbles, depth, depth + shared); + byte[] encoded = rlpList(rlpItem(compactPath(prefix, 0, false)), nodeReference(child)); + nodes.put(new BytesKey(nodePath), encoded); + return encoded; + } + + List encodedChildren = new ArrayList<>(Collections.nCopies(17, EMPTY_RLP_ITEM)); + int start = 0; + while (start < entries.size()) { + int nibble = entries.get(start).nibbles[depth]; + int end = start + 1; + while (end < entries.size() && entries.get(end).nibbles[depth] == nibble) { + end++; + } + byte[] childPath = append(nodePath, new byte[]{(byte) nibble}, 0, 1); + byte[] child = build(entries.subList(start, end), depth + 1, childPath, nodes); + encodedChildren.set(nibble, nodeReference(child)); + start = end; + } + byte[] encoded = rlpList(encodedChildren.toArray(new byte[encodedChildren.size()][])); + nodes.put(new BytesKey(nodePath), encoded); + return encoded; + } + + private static int sharedPrefix(List entries, int depth) { + int shared = 0; + int keyLength = entries.get(0).nibbles.length; + while (depth + shared < keyLength) { + byte expected = entries.get(0).nibbles[depth + shared]; + for (int i = 1; i < entries.size(); i++) { + if (entries.get(i).nibbles[depth + shared] != expected) { + return shared; + } + } + shared++; + } + return shared; + } + + private static byte[] nodeReference(byte[] encodedNode) { + return encodedNode.length < SECURE_KEY_LENGTH ? encodedNode : rlpItem(Hash.sha3(encodedNode)); + } + + private static byte[] compactPath(byte[] nibbles, int offset, boolean leaf) { + int length = nibbles.length - offset; + boolean odd = (length & 1) != 0; + byte[] compact = new byte[1 + length / 2]; + int flag = leaf ? 2 : 0; + int source = offset; + if (odd) { + compact[0] = (byte) ((flag + 1) << 4 | nibbles[source++]); + } else { + compact[0] = (byte) (flag << 4); + } + int target = 1; + while (source < nibbles.length) { + compact[target++] = (byte) (nibbles[source++] << 4 | nibbles[source++]); + } + return compact; + } + + private static byte[] toNibbles(byte[] key) { + byte[] nibbles = new byte[key.length * 2]; + for (int i = 0; i < key.length; i++) { + nibbles[i * 2] = (byte) ((key[i] >>> 4) & 0x0f); + nibbles[i * 2 + 1] = (byte) (key[i] & 0x0f); + } + return nibbles; + } + + private static byte[] append(byte[] prefix, byte[] suffix, int offset, int length) { + byte[] result = Arrays.copyOf(prefix, prefix.length + length); + System.arraycopy(suffix, offset, result, prefix.length, length); + return result; + } + + private static BytesKey secureKey(byte[] value) { + byte[] key = Objects.requireNonNull(value, "secureKey"); + if (key.length != SECURE_KEY_LENGTH) { + throw new IllegalArgumentException("secureKey must be exactly 32 bytes"); + } + return new BytesKey(key); + } + + private static byte[] nonEmpty(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length == 0) { + throw new IllegalArgumentException(name + " must not be empty"); + } + return copy; + } + + private static byte[] rlpItem(byte[] raw) { + if (raw.length == 1 && (raw[0] & 0xff) < 0x80) { + return Arrays.copyOf(raw, raw.length); + } + byte[] prefix = rlpLength(raw.length, 0x80, 0xb7); + return concatenate(prefix, raw); + } + + private static byte[] rlpList(byte[]... encodedItems) { + int payloadLength = 0; + for (byte[] item : encodedItems) { + payloadLength = Math.addExact(payloadLength, item.length); + } + byte[] prefix = rlpLength(payloadLength, 0xc0, 0xf7); + byte[] result = Arrays.copyOf(prefix, Math.addExact(prefix.length, payloadLength)); + int offset = prefix.length; + for (byte[] item : encodedItems) { + System.arraycopy(item, 0, result, offset, item.length); + offset += item.length; + } + return result; + } + + private static byte[] rlpLength(int length, int shortOffset, int longOffset) { + if (length < 56) { + return new byte[]{(byte) (shortOffset + length)}; + } + int lengthOfLength = (Integer.SIZE - Integer.numberOfLeadingZeros(length) + 7) / 8; + byte[] encoded = new byte[lengthOfLength + 1]; + encoded[0] = (byte) (longOffset + lengthOfLength); + int remaining = length; + for (int i = lengthOfLength; i > 0; i--) { + encoded[i] = (byte) remaining; + remaining >>>= Byte.SIZE; + } + return encoded; + } + + private static byte[] concatenate(byte[] first, byte[] second) { + byte[] result = Arrays.copyOf(first, first.length + second.length); + System.arraycopy(second, 0, result, first.length, second.length); + return result; + } + + private static final class Leaf { + + private final byte[] nibbles; + private final byte[] value; + + private Leaf(byte[] nibbles, byte[] value) { + this.nibbles = nibbles; + this.value = value; + } + } + + private static final class BytesKey { + + private final byte[] bytes; + + private BytesKey(byte[] bytes) { + this.bytes = Arrays.copyOf(bytes, bytes.length); + } + + private byte[] copy() { + return Arrays.copyOf(bytes, bytes.length); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof BytesKey + && Arrays.equals(bytes, ((BytesKey) other).bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathNodeStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathNodeStore.java new file mode 100644 index 00000000000..899c8684035 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathNodeStore.java @@ -0,0 +1,17 @@ +package org.tron.core.db2.stateroot; + +/** + * Minimal path-addressed node boundary for the TASK-016 Merkle Patricia trie. + * + *

Paths contain one byte per nibble, each in the range {@code 0..15}; the empty path identifies + * the root node. Implementations must copy mutable input and output arrays at their ownership + * boundary. + */ +public interface PathNodeStore { + + byte[] get(byte[] path); + + void put(byte[] path, byte[] encodedNode); + + void delete(byte[] path); +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java new file mode 100644 index 00000000000..8125bf0f906 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java @@ -0,0 +1,154 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.tron.common.crypto.Hash; +import org.tron.core.trie.TrieImpl; + +public class PathMerkleTrieTest { + + @Test + public void emptyAndSingleLeafMatchIndependentTrieOracle() { + InMemoryPathNodeStore store = new InMemoryPathNodeStore(); + PathMerkleTrie trie = new PathMerkleTrie(store); + assertArrayEquals(Hash.EMPTY_TRIE_HASH, trie.rootHash()); + assertTrue(store.nodes.isEmpty()); + + byte[] key = filledKey(0x11); + byte[] value = Hex.decode("c20180"); + trie.put(key, value); + + assertArrayEquals(referenceRoot(new byte[][]{key}, new byte[][]{value}), trie.rootHash()); + assertArrayEquals(value, trie.get(key)); + assertEquals(1, trie.size()); + assertTrue(store.nodes.containsKey("")); + } + + @Test + public void mutationOrderProducesSameRootAndPathNodeSet() { + byte[][] keys = {filledKey(0x11), keyWithTail(0x11, 0x12), filledKey(0x21), + keyWithTail(0x21, 0x22)}; + byte[][] values = {value("one"), value("two"), value("three"), value("four")}; + + InMemoryPathNodeStore forwardStore = new InMemoryPathNodeStore(); + PathMerkleTrie forward = new PathMerkleTrie(forwardStore); + for (int i = 0; i < keys.length; i++) { + forward.put(keys[i], values[i]); + } + + InMemoryPathNodeStore reverseStore = new InMemoryPathNodeStore(); + PathMerkleTrie reverse = new PathMerkleTrie(reverseStore); + for (int i = keys.length - 1; i >= 0; i--) { + reverse.put(keys[i], values[i]); + } + + byte[] expected = referenceRoot(keys, values); + assertArrayEquals( + Hex.decode("dc1c7bfcacb455baeca2454d8aedbe4b17da4a66364fbc0baa7e5919cacf2bdc"), + expected); + assertArrayEquals(expected, forward.rootHash()); + assertArrayEquals(expected, reverse.rootHash()); + assertNodeMapsEqual(forwardStore.nodes, reverseStore.nodes); + } + + @Test + public void updateAndDeleteCompressCanonicalPaths() { + byte[] first = filledKey(0x33); + byte[] second = keyWithTail(0x33, 0x34); + byte[] third = filledKey(0x44); + InMemoryPathNodeStore store = new InMemoryPathNodeStore(); + PathMerkleTrie trie = new PathMerkleTrie(store); + trie.put(first, value("first")); + trie.put(second, value("second")); + trie.put(third, value("third")); + trie.rootHash(); + int expandedNodeCount = store.nodes.size(); + + trie.put(first, value("updated")); + trie.delete(second); + byte[] expected = referenceRoot(new byte[][]{first, third}, + new byte[][]{value("updated"), value("third")}); + assertArrayEquals(expected, trie.rootHash()); + assertNull(trie.get(second)); + assertTrue(store.nodes.size() < expandedNodeCount); + + trie.delete(first); + trie.delete(third); + assertArrayEquals(Hash.EMPTY_TRIE_HASH, trie.rootHash()); + assertTrue(store.nodes.isEmpty()); + } + + @Test + public void rejectsInvalidKeysAndEmptyValues() { + PathMerkleTrie trie = new PathMerkleTrie(new InMemoryPathNodeStore()); + assertThrows(NullPointerException.class, () -> new PathMerkleTrie(null)); + assertThrows(IllegalArgumentException.class, () -> trie.put(new byte[31], value("x"))); + assertThrows(IllegalArgumentException.class, + () -> trie.put(new byte[PathMerkleTrie.SECURE_KEY_LENGTH], new byte[0])); + assertThrows(NullPointerException.class, () -> trie.delete(null)); + } + + private static byte[] referenceRoot(byte[][] keys, byte[][] values) { + TrieImpl reference = new TrieImpl(); + reference.setAsync(false); + for (int i = 0; i < keys.length; i++) { + reference.put(keys[i], values[i]); + } + return reference.getRootHash(); + } + + private static byte[] filledKey(int value) { + byte[] key = new byte[PathMerkleTrie.SECURE_KEY_LENGTH]; + Arrays.fill(key, (byte) value); + return key; + } + + private static byte[] keyWithTail(int prefix, int tail) { + byte[] key = filledKey(prefix); + key[key.length - 1] = (byte) tail; + return key; + } + + private static byte[] value(String value) { + return PathStateCommitmentCodec.presentLeafValue(value.getBytes(StandardCharsets.UTF_8)); + } + + private static void assertNodeMapsEqual(Map expected, + Map actual) { + assertEquals(expected.keySet(), actual.keySet()); + for (String path : expected.keySet()) { + assertArrayEquals(expected.get(path), actual.get(path)); + } + } + + private static final class InMemoryPathNodeStore implements PathNodeStore { + + private final Map nodes = new LinkedHashMap<>(); + + @Override + public byte[] get(byte[] path) { + byte[] value = nodes.get(Hex.toHexString(path)); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + nodes.put(Hex.toHexString(path), Arrays.copyOf(encodedNode, encodedNode.length)); + } + + @Override + public void delete(byte[] path) { + nodes.remove(Hex.toHexString(path)); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java index 61c68f6e7ad..07bfb5aac5d 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java @@ -39,6 +39,16 @@ public void fixedStoreKeyGoldensMatchIndependentKeccakOracle() throws Exception 21, new byte[]{1}); } + @Test + public void approvedAbiAndAssetIssueStoresHaveIndependentLeafDomains() throws Exception { + assertGolden("14af9866899065b509f6ea5d45902d443a4357efad5e6478c47ef108d603b8d7", + 1, new byte[]{1}); + assertGolden("5c0a5639b07fa98f7e067c3e0c1d067ca1de752810b7576bc20fd2c75ba89eb5", + 6, new byte[]{1}); + assertGolden("99951328ba6d7d4a7fa199cf727a7c4494bd885ce2ac8c04475dd1fd699af7de", + 7, new byte[]{1}); + } + @Test public void presentValuesKeepEmptyAndZeroDistinct() { assertArrayEquals(Hex.decode("c20180"), From ccf3694cb5c23c90a9dc600a237dc9db0943f16d Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 18:45:08 +0800 Subject: [PATCH 040/161] feat(chainbase): aggregate path state roots --- .../db2/stateroot/PathStateParticipant.java | 50 +++++ .../stateroot/PathStateParticipantScope.java | 74 ++++++ .../core/db2/stateroot/PathStateRoot.java | 77 +++++++ .../core/db2/stateroot/PathStateRootTest.java | 211 ++++++++++++++++++ 4 files changed, 412 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipant.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantScope.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipant.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipant.java new file mode 100644 index 00000000000..ebf74a49639 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipant.java @@ -0,0 +1,50 @@ +package org.tron.core.db2.stateroot; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** Immutable identity and byte-format version for one path-state participant. */ +public final class PathStateParticipant implements Comparable { + + private final int storeId; + private final String dbName; + private final int storeFormatVersion; + + public PathStateParticipant(int storeId, String dbName, int storeFormatVersion) { + if (storeId <= 0) { + throw new IllegalArgumentException("storeId must be positive"); + } + this.dbName = Objects.requireNonNull(dbName, "dbName"); + int encodedNameLength = dbName.getBytes(StandardCharsets.UTF_8).length; + if (encodedNameLength == 0 || encodedNameLength > 128) { + throw new IllegalArgumentException("dbName must encode to 1..128 bytes"); + } + if (storeFormatVersion <= 0) { + throw new IllegalArgumentException("storeFormatVersion must be positive"); + } + this.storeId = storeId; + this.storeFormatVersion = storeFormatVersion; + } + + public int getStoreId() { + return storeId; + } + + public String getDbName() { + return dbName; + } + + public int getStoreFormatVersion() { + return storeFormatVersion; + } + + @Override + public int compareTo(PathStateParticipant other) { + return Integer.compare(storeId, other.storeId); + } + + @Override + public String toString() { + return storeId + ":" + dbName + ":v" + storeFormatVersion; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantScope.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantScope.java new file mode 100644 index 00000000000..42861bcb67a --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantScope.java @@ -0,0 +1,74 @@ +package org.tron.core.db2.stateroot; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Fail-closed participant registry for one path-state root format. + * + *

TASK-016 has approved ABI and both AssetIssue stores for inclusion, while the complete + * execution-state exact-set remains an H1-L1 gate. This class therefore validates the supplied + * immutable scope and the three mandatory names without inventing the remaining registry. + */ +public final class PathStateParticipantScope { + + public static final String ABI_DB = "abi"; + public static final String ASSET_ISSUE_DB = "asset-issue"; + public static final String ASSET_ISSUE_V2_DB = "asset-issue-v2"; + + private final List participants; + private final Map participantsByName; + + public PathStateParticipantScope(Collection participants) { + List sorted = new ArrayList<>( + Objects.requireNonNull(participants, "participants")); + if (sorted.isEmpty()) { + throw new IllegalArgumentException("participant scope must not be empty"); + } + Collections.sort(sorted); + + Set storeIds = new LinkedHashSet<>(); + Map byName = new LinkedHashMap<>(); + for (PathStateParticipant participant : sorted) { + PathStateParticipant present = Objects.requireNonNull(participant, "participant"); + if (!storeIds.add(present.getStoreId())) { + throw new IllegalArgumentException("duplicate Store ID: " + present.getStoreId()); + } + if (byName.put(present.getDbName(), present) != null) { + throw new IllegalArgumentException("duplicate database: " + present.getDbName()); + } + } + requireMandatory(byName, ABI_DB); + requireMandatory(byName, ASSET_ISSUE_DB); + requireMandatory(byName, ASSET_ISSUE_V2_DB); + this.participants = Collections.unmodifiableList(sorted); + this.participantsByName = Collections.unmodifiableMap(byName); + } + + public List getParticipants() { + return participants; + } + + public PathStateParticipant require(String dbName) { + PathStateParticipant participant = participantsByName.get( + Objects.requireNonNull(dbName, "dbName")); + if (participant == null) { + throw new IllegalArgumentException("unknown path-state participant: " + dbName); + } + return participant; + } + + private static void requireMandatory(Map participants, + String dbName) { + if (!participants.containsKey(dbName)) { + throw new IllegalArgumentException("missing mandatory path-state participant: " + dbName); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java new file mode 100644 index 00000000000..19d429cac5c --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -0,0 +1,77 @@ +package org.tron.core.db2.stateroot; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Current-only per-Store trie and super-trie aggregator for TASK-016. + * + *

Every participant, including an empty Store, has one super-trie leaf. Consequently the root + * commits to the supplied participant exact-set as well as each Store's current entries. This + * component has no block, database, history, restart, or publication lifecycle. + */ +public final class PathStateRoot { + + private final PathStateParticipantScope scope; + private final Map participantTries = new LinkedHashMap<>(); + private final PathMerkleTrie superTrie; + + public PathStateRoot(PathStateParticipantScope scope, PathNodeStoreFactory storeFactory, + PathNodeStore superNodeStore) { + this.scope = Objects.requireNonNull(scope, "scope"); + PathNodeStoreFactory factory = Objects.requireNonNull(storeFactory, "storeFactory"); + Set uniqueStores = Collections.newSetFromMap(new IdentityHashMap<>()); + for (PathStateParticipant participant : scope.getParticipants()) { + PathNodeStore nodeStore = Objects.requireNonNull(factory.open(participant), + "participant node store"); + if (!uniqueStores.add(nodeStore)) { + throw new IllegalArgumentException("participant node Stores must have distinct identities"); + } + participantTries.put(participant.getDbName(), new PathMerkleTrie(nodeStore)); + } + PathNodeStore rootStore = Objects.requireNonNull(superNodeStore, "superNodeStore"); + if (!uniqueStores.add(rootStore)) { + throw new IllegalArgumentException("super node Store must have a distinct identity"); + } + superTrie = new PathMerkleTrie(rootStore); + } + + public void put(String dbName, byte[] canonicalKey, byte[] canonicalValue) { + PathStateParticipant participant = scope.require(dbName); + participantTries.get(participant.getDbName()).put( + PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), canonicalKey), + PathStateCommitmentCodec.presentLeafValue(canonicalValue)); + } + + public void delete(String dbName, byte[] canonicalKey) { + PathStateParticipant participant = scope.require(dbName); + participantTries.get(participant.getDbName()).delete( + PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), canonicalKey)); + } + + public byte[] participantRoot(String dbName) { + PathStateParticipant participant = scope.require(dbName); + return participantTries.get(participant.getDbName()).rootHash(); + } + + /** Returns the super root after binding every participant identity, format, and current root. */ + public byte[] rootHash() { + for (PathStateParticipant participant : scope.getParticipants()) { + byte[] storeRoot = participantTries.get(participant.getDbName()).rootHash(); + superTrie.put(PathStateCommitmentCodec.superLeafKey(participant.getStoreId()), + PathStateCommitmentCodec.superLeafValue(participant.getStoreId(), + participant.getDbName(), participant.getStoreFormatVersion(), storeRoot)); + } + return superTrie.rootHash(); + } + + /** Creates an independent node Store for one immutable participant identity. */ + public interface PathNodeStoreFactory { + + PathNodeStore open(PathStateParticipant participant); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java new file mode 100644 index 00000000000..4ab09b9c6c8 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java @@ -0,0 +1,211 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.tron.core.trie.TrieImpl; + +public class PathStateRootTest { + + private static final PathStateParticipant ABI = participant(1, "abi"); + private static final PathStateParticipant ACCOUNT = participant(4, "account"); + private static final PathStateParticipant ASSET_ISSUE = participant(6, "asset-issue"); + private static final PathStateParticipant ASSET_ISSUE_V2 = participant(7, "asset-issue-v2"); + private static final PathStateParticipant STORAGE = participant(22, "storage-row"); + + @Test + public void aggregatesEveryParticipantIntoIndependentOracleSuperRoot() { + List participants = participants(); + PathStateRoot stateRoot = stateRoot(participants); + Mutation[] mutations = { + mutation("abi", "contract", "abi-v1"), + mutation("asset-issue", "asset", "legacy"), + mutation("asset-issue-v2", "asset", "v2"), + mutation("account", "address", "account-value"), + mutation("storage-row", "slot", "storage-value") + }; + for (Mutation mutation : mutations) { + stateRoot.put(mutation.dbName, mutation.key, mutation.value); + } + + byte[] expected = referenceRoot(participants, mutations); + assertArrayEquals( + Hex.decode("f8d0364fdb0432016c12f9a660de2bd34513257014e35d90ac289d9024e6d216"), + expected); + assertArrayEquals(expected, stateRoot.rootHash()); + } + + @Test + public void participantAndMutationOrderDoNotChangeSuperRoot() { + List forwardParticipants = participants(); + List reverseParticipants = new ArrayList<>(forwardParticipants); + java.util.Collections.reverse(reverseParticipants); + Mutation[] mutations = { + mutation("abi", "a", "1"), mutation("asset-issue", "b", "2"), + mutation("asset-issue-v2", "c", "3"), mutation("account", "d", "4"), + mutation("storage-row", "e", "5") + }; + + PathStateRoot forward = stateRoot(forwardParticipants); + for (Mutation mutation : mutations) { + forward.put(mutation.dbName, mutation.key, mutation.value); + } + PathStateRoot reverse = stateRoot(reverseParticipants); + for (int i = mutations.length - 1; i >= 0; i--) { + reverse.put(mutations[i].dbName, mutations[i].key, mutations[i].value); + } + assertArrayEquals(forward.rootHash(), reverse.rootHash()); + } + + @Test + public void deleteUpdatesOnlyNamedParticipantAndSuperRoot() { + PathStateRoot stateRoot = stateRoot(participants()); + byte[] key = bytes("asset"); + stateRoot.put("asset-issue", key, bytes("legacy")); + stateRoot.put("asset-issue-v2", key, bytes("v2")); + byte[] originalRoot = stateRoot.rootHash(); + byte[] v2Root = stateRoot.participantRoot("asset-issue-v2"); + + stateRoot.delete("asset-issue", key); + assertArrayEquals(v2Root, stateRoot.participantRoot("asset-issue-v2")); + org.junit.Assert.assertFalse(Arrays.equals(originalRoot, stateRoot.rootHash())); + } + + @Test + public void emptyParticipantStillChangesCommittedScope() { + PathStateRoot base = stateRoot(participants()); + List extended = new ArrayList<>(participants()); + extended.add(participant(21, "proposal")); + PathStateRoot withEmptyProposal = stateRoot(extended); + + org.junit.Assert.assertFalse(Arrays.equals(base.rootHash(), withEmptyProposal.rootHash())); + } + + @Test + public void scopeRejectsMissingDuplicateAndUnknownParticipants() { + assertThrows(IllegalArgumentException.class, + () -> new PathStateParticipantScope(Arrays.asList(ABI, ASSET_ISSUE))); + assertThrows(IllegalArgumentException.class, + () -> new PathStateParticipantScope(Arrays.asList(ABI, ASSET_ISSUE, ASSET_ISSUE_V2, + participant(1, "other")))); + assertThrows(IllegalArgumentException.class, + () -> new PathStateParticipantScope(Arrays.asList(ABI, ASSET_ISSUE, ASSET_ISSUE_V2, + participant(9, "abi")))); + + PathStateRoot stateRoot = stateRoot(participants()); + assertThrows(IllegalArgumentException.class, + () -> stateRoot.put("unknown", bytes("key"), bytes("value"))); + assertThrows(IllegalArgumentException.class, + () -> stateRoot.delete("unknown", bytes("key"))); + + PathStateParticipantScope scope = new PathStateParticipantScope(participants()); + InMemoryPathNodeStore sharedStore = new InMemoryPathNodeStore(); + assertThrows(IllegalArgumentException.class, + () -> new PathStateRoot(scope, ignored -> sharedStore, new InMemoryPathNodeStore())); + assertThrows(IllegalArgumentException.class, + () -> new PathStateRoot(scope, + participant -> participant.getDbName().equals("abi") + ? sharedStore : new InMemoryPathNodeStore(), sharedStore)); + } + + private static byte[] referenceRoot(List participants, + Mutation[] mutations) { + Map stores = new LinkedHashMap<>(); + for (PathStateParticipant participant : participants) { + stores.put(participant.getDbName(), referenceTrie()); + } + for (Mutation mutation : mutations) { + PathStateParticipant participant = find(participants, mutation.dbName); + stores.get(mutation.dbName).put( + PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), mutation.key), + PathStateCommitmentCodec.presentLeafValue(mutation.value)); + } + TrieImpl superTrie = referenceTrie(); + for (PathStateParticipant participant : participants) { + superTrie.put(PathStateCommitmentCodec.superLeafKey(participant.getStoreId()), + PathStateCommitmentCodec.superLeafValue(participant.getStoreId(), + participant.getDbName(), participant.getStoreFormatVersion(), + stores.get(participant.getDbName()).getRootHash())); + } + return superTrie.getRootHash(); + } + + private static PathStateParticipant find(List participants, + String dbName) { + for (PathStateParticipant participant : participants) { + if (participant.getDbName().equals(dbName)) { + return participant; + } + } + throw new AssertionError("missing test participant " + dbName); + } + + private static PathStateRoot stateRoot(List participants) { + PathStateParticipantScope scope = new PathStateParticipantScope(participants); + return new PathStateRoot(scope, ignored -> new InMemoryPathNodeStore(), + new InMemoryPathNodeStore()); + } + + private static TrieImpl referenceTrie() { + TrieImpl trie = new TrieImpl(); + trie.setAsync(false); + return trie; + } + + private static List participants() { + return Arrays.asList(ABI, ACCOUNT, ASSET_ISSUE, ASSET_ISSUE_V2, STORAGE); + } + + private static PathStateParticipant participant(int storeId, String dbName) { + return new PathStateParticipant(storeId, dbName, 1); + } + + private static Mutation mutation(String dbName, String key, String value) { + return new Mutation(dbName, bytes(key), bytes(value)); + } + + private static byte[] bytes(String value) { + return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + private static final class Mutation { + + private final String dbName; + private final byte[] key; + private final byte[] value; + + private Mutation(String dbName, byte[] key, byte[] value) { + this.dbName = dbName; + this.key = key; + this.value = value; + } + } + + private static final class InMemoryPathNodeStore implements PathNodeStore { + + private final Map nodes = new LinkedHashMap<>(); + + @Override + public byte[] get(byte[] path) { + byte[] node = nodes.get(Hex.toHexString(path)); + return node == null ? null : Arrays.copyOf(node, node.length); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + nodes.put(Hex.toHexString(path), Arrays.copyOf(encodedNode, encodedNode.length)); + } + + @Override + public void delete(byte[] path) { + nodes.remove(Hex.toHexString(path)); + } + } +} From e93973d0331cf9326718027799dfb7f3731a5bdc Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 18:56:07 +0800 Subject: [PATCH 041/161] feat(chainbase): validate path state mutations --- .../core/db2/stateroot/PathMerkleTrie.java | 64 +++++++-- .../core/db2/stateroot/PathStateMutation.java | 49 +++++++ .../core/db2/stateroot/PathStateRoot.java | 131 ++++++++++++++++-- .../db2/stateroot/PathMerkleTrieTest.java | 22 +++ .../core/db2/stateroot/PathStateRootTest.java | 65 +++++++++ 5 files changed, 304 insertions(+), 27 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index 8e23c2af65c..0312571ba31 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -50,46 +50,57 @@ public PathMerkleTrie(PathNodeStore nodeStore) { this.nodeStore = Objects.requireNonNull(nodeStore, "nodeStore"); } - public void put(byte[] secureKey, byte[] encodedValue) { + public synchronized void put(byte[] secureKey, byte[] encodedValue) { BytesKey key = secureKey(secureKey); byte[] value = nonEmpty(encodedValue, "encodedValue"); byte[] previous = leaves.put(key, value); dirty |= !Arrays.equals(previous, value); } - public void delete(byte[] secureKey) { + public synchronized void delete(byte[] secureKey) { dirty |= leaves.remove(secureKey(secureKey)) != null; } - public byte[] get(byte[] secureKey) { + public synchronized byte[] get(byte[] secureKey) { byte[] value = leaves.get(secureKey(secureKey)); return value == null ? null : Arrays.copyOf(value, value.length); } /** Reconciles path-addressed nodes and returns the canonical root hash. */ - public byte[] rootHash() { + public synchronized byte[] rootHash() { if (dirty) { commit(); } return Arrays.copyOf(rootHash, rootHash.length); } - public int size() { + public synchronized int size() { return leaves.size(); } - private void commit() { - Map nextNodes = new LinkedHashMap<>(); - if (leaves.isEmpty()) { - rootHash = Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length); - } else { - List entries = new ArrayList<>(leaves.size()); - for (Map.Entry entry : leaves.entrySet()) { - entries.add(new Leaf(toNibbles(entry.getKey().bytes), entry.getValue())); + /** Verifies every path owned by the current committed node set without repairing corruption. */ + public synchronized void verifyNodeStore() { + if (dirty) { + throw new IllegalStateException("cannot verify a dirty path trie"); + } + Map expectedNodes = buildCurrentNodes(); + if (!committedPaths.equals(expectedNodes.keySet())) { + throw new IllegalStateException("committed path set does not match current leaves"); + } + byte[] expectedRoot = rootHash(expectedNodes); + if (!Arrays.equals(rootHash, expectedRoot)) { + throw new IllegalStateException("committed path root does not match current leaves"); + } + for (Map.Entry entry : expectedNodes.entrySet()) { + if (!Arrays.equals(nodeStore.get(entry.getKey().bytes), entry.getValue())) { + throw new IllegalStateException("missing or corrupt committed path node"); } - byte[] root = build(entries, 0, EMPTY_PATH, nextNodes); - rootHash = Hash.sha3(root); } + } + + private void commit() { + Map nextNodes = buildCurrentNodes(); + rootHash = rootHash(nextNodes); Set stalePaths = new LinkedHashSet<>(committedPaths); stalePaths.removeAll(nextNodes.keySet()); @@ -107,6 +118,29 @@ private void commit() { dirty = false; } + private Map buildCurrentNodes() { + Map nodes = new LinkedHashMap<>(); + if (!leaves.isEmpty()) { + List entries = new ArrayList<>(leaves.size()); + for (Map.Entry entry : leaves.entrySet()) { + entries.add(new Leaf(toNibbles(entry.getKey().bytes), entry.getValue())); + } + build(entries, 0, EMPTY_PATH, nodes); + } + return nodes; + } + + private static byte[] rootHash(Map nodes) { + if (nodes.isEmpty()) { + return Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length); + } + byte[] root = nodes.get(new BytesKey(EMPTY_PATH)); + if (root == null) { + throw new IllegalStateException("path node set has no root"); + } + return Hash.sha3(root); + } + private static byte[] build(List entries, int depth, byte[] nodePath, Map nodes) { if (entries.size() == 1) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java new file mode 100644 index 00000000000..50d6a0b4cce --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java @@ -0,0 +1,49 @@ +package org.tron.core.db2.stateroot; + +import java.util.Arrays; +import java.util.Objects; + +/** Immutable current-state mutation consumed by {@link PathStateRoot}. */ +public final class PathStateMutation { + + private final String dbName; + private final byte[] canonicalKey; + private final byte[] canonicalValue; + + private PathStateMutation(String dbName, byte[] canonicalKey, byte[] canonicalValue) { + this.dbName = Objects.requireNonNull(dbName, "dbName"); + this.canonicalKey = copy(canonicalKey, "canonicalKey"); + this.canonicalValue = canonicalValue == null ? null + : Arrays.copyOf(canonicalValue, canonicalValue.length); + } + + public static PathStateMutation put(String dbName, byte[] canonicalKey, byte[] canonicalValue) { + return new PathStateMutation(dbName, canonicalKey, + Objects.requireNonNull(canonicalValue, "canonicalValue")); + } + + public static PathStateMutation delete(String dbName, byte[] canonicalKey) { + return new PathStateMutation(dbName, canonicalKey, null); + } + + public String getDbName() { + return dbName; + } + + public byte[] getCanonicalKey() { + return Arrays.copyOf(canonicalKey, canonicalKey.length); + } + + public boolean isDelete() { + return canonicalValue == null; + } + + public byte[] getCanonicalValue() { + return canonicalValue == null ? null : Arrays.copyOf(canonicalValue, canonicalValue.length); + } + + private static byte[] copy(byte[] value, String name) { + byte[] source = Objects.requireNonNull(value, name); + return Arrays.copyOf(source, source.length); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index 19d429cac5c..52ac76d8caa 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -1,8 +1,14 @@ package org.tron.core.db2.stateroot; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.IdentityHashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -16,9 +22,17 @@ */ public final class PathStateRoot { + private static final Comparator MUTATION_COMPARATOR = (left, right) -> { + int participantOrder = Integer.compare(left.participant.getStoreId(), + right.participant.getStoreId()); + return participantOrder != 0 ? participantOrder : compareUnsigned(left.secureKey, + right.secureKey); + }; + private final PathStateParticipantScope scope; private final Map participantTries = new LinkedHashMap<>(); private final PathMerkleTrie superTrie; + private boolean rootMaterialized; public PathStateRoot(PathStateParticipantScope scope, PathNodeStoreFactory storeFactory, PathNodeStore superNodeStore) { @@ -40,33 +54,89 @@ public PathStateRoot(PathStateParticipantScope scope, PathNodeStoreFactory store superTrie = new PathMerkleTrie(rootStore); } - public void put(String dbName, byte[] canonicalKey, byte[] canonicalValue) { - PathStateParticipant participant = scope.require(dbName); - participantTries.get(participant.getDbName()).put( - PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), canonicalKey), - PathStateCommitmentCodec.presentLeafValue(canonicalValue)); + public synchronized void put(String dbName, byte[] canonicalKey, byte[] canonicalValue) { + apply(Collections.singletonList(PathStateMutation.put(dbName, canonicalKey, canonicalValue))); } - public void delete(String dbName, byte[] canonicalKey) { - PathStateParticipant participant = scope.require(dbName); - participantTries.get(participant.getDbName()).delete( - PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), canonicalKey)); + public synchronized void delete(String dbName, byte[] canonicalKey) { + apply(Collections.singletonList(PathStateMutation.delete(dbName, canonicalKey))); } - public byte[] participantRoot(String dbName) { + /** Validates a complete mutation set before changing any participant trie. */ + public synchronized void apply(Collection mutations) { + List prepared = prepare(mutations); + for (PreparedMutation mutation : prepared) { + PathMerkleTrie trie = participantTries.get(mutation.participant.getDbName()); + if (mutation.encodedValue == null) { + trie.delete(mutation.secureKey); + } else { + trie.put(mutation.secureKey, mutation.encodedValue); + } + } + rootMaterialized = false; + } + + public synchronized byte[] participantRoot(String dbName) { PathStateParticipant participant = scope.require(dbName); return participantTries.get(participant.getDbName()).rootHash(); } /** Returns the super root after binding every participant identity, format, and current root. */ - public byte[] rootHash() { + public synchronized byte[] rootHash() { for (PathStateParticipant participant : scope.getParticipants()) { byte[] storeRoot = participantTries.get(participant.getDbName()).rootHash(); superTrie.put(PathStateCommitmentCodec.superLeafKey(participant.getStoreId()), PathStateCommitmentCodec.superLeafValue(participant.getStoreId(), participant.getDbName(), participant.getStoreFormatVersion(), storeRoot)); } - return superTrie.rootHash(); + byte[] root = superTrie.rootHash(); + rootMaterialized = true; + return root; + } + + /** Verifies all current participant nodes and the already-published super-trie nodes. */ + public synchronized void verifyNodeStores() { + if (!rootMaterialized) { + throw new IllegalStateException("path state root is not materialized"); + } + for (PathMerkleTrie trie : participantTries.values()) { + trie.verifyNodeStore(); + } + superTrie.verifyNodeStore(); + } + + private List prepare(Collection mutations) { + List supplied = new ArrayList<>( + Objects.requireNonNull(mutations, "mutations")); + if (supplied.isEmpty()) { + throw new IllegalArgumentException("mutation batch must not be empty"); + } + List prepared = new ArrayList<>(supplied.size()); + Set uniqueKeys = new LinkedHashSet<>(); + for (PathStateMutation mutation : supplied) { + PathStateMutation present = Objects.requireNonNull(mutation, "mutation"); + PathStateParticipant participant = scope.require(present.getDbName()); + byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), + present.getCanonicalKey()); + if (!uniqueKeys.add(new MutationKey(participant.getStoreId(), secureKey))) { + throw new IllegalArgumentException("duplicate path-state mutation key"); + } + byte[] encodedValue = present.isDelete() ? null + : PathStateCommitmentCodec.presentLeafValue(present.getCanonicalValue()); + prepared.add(new PreparedMutation(participant, secureKey, encodedValue)); + } + Collections.sort(prepared, MUTATION_COMPARATOR); + return prepared; + } + + private static int compareUnsigned(byte[] left, byte[] right) { + for (int i = 0; i < Math.min(left.length, right.length); i++) { + int result = Integer.compare(left[i] & 0xff, right[i] & 0xff); + if (result != 0) { + return result; + } + } + return Integer.compare(left.length, right.length); } /** Creates an independent node Store for one immutable participant identity. */ @@ -74,4 +144,41 @@ public interface PathNodeStoreFactory { PathNodeStore open(PathStateParticipant participant); } + + private static final class PreparedMutation { + + private final PathStateParticipant participant; + private final byte[] secureKey; + private final byte[] encodedValue; + + private PreparedMutation(PathStateParticipant participant, byte[] secureKey, + byte[] encodedValue) { + this.participant = participant; + this.secureKey = secureKey; + this.encodedValue = encodedValue; + } + } + + private static final class MutationKey { + + private final int storeId; + private final byte[] secureKey; + + private MutationKey(int storeId, byte[] secureKey) { + this.storeId = storeId; + this.secureKey = secureKey; + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof MutationKey + && storeId == ((MutationKey) other).storeId + && Arrays.equals(secureKey, ((MutationKey) other).secureKey); + } + + @Override + public int hashCode() { + return 31 * storeId + Arrays.hashCode(secureKey); + } + } } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java index 8125bf0f906..0ab3df06299 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java @@ -98,6 +98,28 @@ public void rejectsInvalidKeysAndEmptyValues() { assertThrows(NullPointerException.class, () -> trie.delete(null)); } + @Test + public void detectsMissingCorruptAndDirtyCommittedNodes() { + InMemoryPathNodeStore corruptStore = new InMemoryPathNodeStore(); + PathMerkleTrie corruptTrie = new PathMerkleTrie(corruptStore); + corruptTrie.put(filledKey(0x55), value("value")); + corruptTrie.rootHash(); + corruptTrie.verifyNodeStore(); + corruptStore.nodes.put("", new byte[]{1}); + assertThrows(IllegalStateException.class, corruptTrie::verifyNodeStore); + + InMemoryPathNodeStore missingStore = new InMemoryPathNodeStore(); + PathMerkleTrie missingTrie = new PathMerkleTrie(missingStore); + missingTrie.put(filledKey(0x66), value("value")); + missingTrie.rootHash(); + missingStore.nodes.remove(""); + assertThrows(IllegalStateException.class, missingTrie::verifyNodeStore); + + PathMerkleTrie dirtyTrie = new PathMerkleTrie(new InMemoryPathNodeStore()); + dirtyTrie.put(filledKey(0x77), value("value")); + assertThrows(IllegalStateException.class, dirtyTrie::verifyNodeStore); + } + private static byte[] referenceRoot(byte[][] keys, byte[][] values) { TrieImpl reference = new TrieImpl(); reference.setAsync(false); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java index 4ab09b9c6c8..98b5146129b 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java @@ -8,6 +8,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import org.bouncycastle.util.encoders.Hex; import org.junit.Test; import org.tron.core.trie.TrieImpl; @@ -115,6 +118,68 @@ public void scopeRejectsMissingDuplicateAndUnknownParticipants() { ? sharedStore : new InMemoryPathNodeStore(), sharedStore)); } + @Test + public void batchValidationRejectsPartialAndDuplicateMutationSets() { + PathStateRoot stateRoot = stateRoot(participants()); + byte[] originalRoot = stateRoot.rootHash(); + List partial = Arrays.asList( + PathStateMutation.put("account", bytes("valid"), bytes("value")), + PathStateMutation.put("unknown", bytes("invalid"), bytes("value"))); + assertThrows(IllegalArgumentException.class, () -> stateRoot.apply(partial)); + assertArrayEquals(originalRoot, stateRoot.rootHash()); + + List duplicate = Arrays.asList( + PathStateMutation.put("abi", bytes("key"), new byte[0]), + PathStateMutation.delete("abi", bytes("key"))); + assertThrows(IllegalArgumentException.class, () -> stateRoot.apply(duplicate)); + assertArrayEquals(originalRoot, stateRoot.rootHash()); + assertThrows(IllegalArgumentException.class, + () -> stateRoot.apply(java.util.Collections.emptyList())); + } + + @Test + public void emptyAndZeroValuesProduceDifferentRoots() { + PathStateRoot empty = stateRoot(participants()); + empty.put("abi", bytes("key"), new byte[0]); + PathStateRoot zero = stateRoot(participants()); + zero.put("abi", bytes("key"), new byte[]{0}); + org.junit.Assert.assertFalse(Arrays.equals(empty.rootHash(), zero.rootHash())); + } + + @Test + public void verificationRequiresCurrentMaterializedSuperRoot() { + PathStateRoot stateRoot = stateRoot(participants()); + assertThrows(IllegalStateException.class, stateRoot::verifyNodeStores); + stateRoot.rootHash(); + stateRoot.verifyNodeStores(); + stateRoot.put("abi", bytes("key"), bytes("value")); + assertThrows(IllegalStateException.class, stateRoot::verifyNodeStores); + stateRoot.rootHash(); + stateRoot.verifyNodeStores(); + } + + @Test + public void concurrentUniqueMutationsMatchSequentialRoot() throws Exception { + PathStateRoot concurrent = stateRoot(participants()); + PathStateRoot sequential = stateRoot(participants()); + ExecutorService executor = Executors.newFixedThreadPool(4); + List> futures = new ArrayList<>(); + try { + for (int i = 0; i < 64; i++) { + final byte[] key = bytes("key-" + i); + final byte[] value = bytes("value-" + i); + sequential.put("account", key, value); + futures.add(executor.submit(() -> concurrent.put("account", key, value))); + } + for (Future future : futures) { + future.get(); + } + } finally { + executor.shutdownNow(); + } + assertArrayEquals(sequential.rootHash(), concurrent.rootHash()); + } + private static byte[] referenceRoot(List participants, Mutation[] mutations) { Map stores = new LinkedHashMap<>(); From 5f7bab3a375fc0a75d0b58b33839e30d9fa84a31 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 19:06:35 +0800 Subject: [PATCH 042/161] test(chainbase): verify path state engines --- .../stateroot/PathNodeStoreEngineTest.java | 305 ++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathNodeStoreEngineTest.java diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathNodeStoreEngineTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathNodeStoreEngineTest.java new file mode 100644 index 00000000000..5a1dfce9ecc --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathNodeStoreEngineTest.java @@ -0,0 +1,305 @@ +package org.tron.core.db2.stateroot; + +import static org.fusesource.leveldbjni.JniDBFactory.factory; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.bouncycastle.util.encoders.Hex; +import org.iq80.leveldb.DBIterator; +import org.iq80.leveldb.Options; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.tron.common.arch.Arch; + +/** Test-only LevelDB/RocksDB evidence for the backend-neutral TASK-016 node boundary. */ +public class PathNodeStoreEngineTest { + + private static final String LEVELDB = "LEVELDB"; + private static final String ROCKSDB = "ROCKSDB"; + private static final PathStateParticipant ABI = participant(1, "abi"); + private static final PathStateParticipant ACCOUNT = participant(4, "account"); + private static final PathStateParticipant ASSET_ISSUE = participant(6, "asset-issue"); + private static final PathStateParticipant ASSET_ISSUE_V2 = participant(7, "asset-issue-v2"); + private static final PathStateParticipant STORAGE = participant(22, "storage-row"); + + static { + org.rocksdb.RocksDB.loadLibrary(); + } + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void levelDbAndRocksDbProduceIdenticalRootAndPathNodes() throws Exception { + org.junit.Assume.assumeFalse(Arch.isArm64()); + EngineFixture level = new EngineFixture(LEVELDB, + temporaryFolder.newFolder("path-state-level")); + EngineFixture rocks = new EngineFixture(ROCKSDB, + temporaryFolder.newFolder("path-state-rocks")); + try { + List mutations = mutations(); + level.root.apply(mutations); + List reversed = new ArrayList<>(mutations); + Collections.reverse(reversed); + rocks.root.apply(reversed); + + byte[] expected = Hex.decode( + "f8d0364fdb0432016c12f9a660de2bd34513257014e35d90ac289d9024e6d216"); + assertArrayEquals(expected, level.root.rootHash()); + assertArrayEquals(expected, rocks.root.rootHash()); + level.root.verifyNodeStores(); + rocks.root.verifyNodeStores(); + assertEquals(level.snapshotNodes(), rocks.snapshotNodes()); + + byte[] originalSuperRootNode = rocks.superStore.get(new byte[0]); + rocks.superStore.put(new byte[0], new byte[]{1}); + assertThrows(IllegalStateException.class, rocks.root::verifyNodeStores); + rocks.superStore.put(new byte[0], originalSuperRootNode); + rocks.root.verifyNodeStores(); + } finally { + rocks.close(); + level.close(); + } + } + + @Test + public void engineAdaptersPreserveRootPathBytesAcrossReopen() throws Exception { + for (String engine : availableEngines()) { + File parent = temporaryFolder.newFolder("path-reopen-" + engine.toLowerCase()); + byte[] rootPath = new byte[0]; + byte[] encodedNode = Hex.decode("c22080"); + EnginePathNodeStore first = open(engine, parent, "root"); + first.put(rootPath, encodedNode); + first.close(); + + EnginePathNodeStore reopened = open(engine, parent, "root"); + try { + assertArrayEquals(encodedNode, reopened.get(rootPath)); + reopened.delete(rootPath); + assertNull(reopened.get(rootPath)); + } finally { + reopened.close(); + } + } + } + + private static List availableEngines() { + return Arch.isArm64() ? Collections.singletonList(ROCKSDB) + : Arrays.asList(LEVELDB, ROCKSDB); + } + + private static List participants() { + return Arrays.asList(ABI, ACCOUNT, ASSET_ISSUE, ASSET_ISSUE_V2, STORAGE); + } + + private static List mutations() { + return Arrays.asList( + mutation("abi", "contract", "abi-v1"), + mutation("asset-issue", "asset", "legacy"), + mutation("asset-issue-v2", "asset", "v2"), + mutation("account", "address", "account-value"), + mutation("storage-row", "slot", "storage-value")); + } + + private static PathStateMutation mutation(String dbName, String key, String value) { + return PathStateMutation.put(dbName, bytes(key), bytes(value)); + } + + private static byte[] bytes(String value) { + return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + private static PathStateParticipant participant(int storeId, String dbName) { + return new PathStateParticipant(storeId, dbName, 1); + } + + private static EnginePathNodeStore open(String engine, File parent, String dbName) { + Path directory = new File(parent, dbName).toPath(); + return LEVELDB.equals(engine) ? new LevelPathNodeStore(directory) + : new RocksPathNodeStore(directory); + } + + private static final class EngineFixture implements AutoCloseable { + + private final String engine; + private final File parent; + private final Map participantStores = new LinkedHashMap<>(); + private final List stores = new ArrayList<>(); + private final EnginePathNodeStore superStore; + private final PathStateRoot root; + + private EngineFixture(String engine, File parent) { + this.engine = engine; + this.parent = parent; + PathStateParticipantScope scope = new PathStateParticipantScope(participants()); + superStore = create("super"); + root = new PathStateRoot(scope, participant -> { + EnginePathNodeStore store = create("store-" + participant.getStoreId()); + participantStores.put(participant.getDbName(), store); + return store; + }, superStore); + } + + private EnginePathNodeStore create(String dbName) { + EnginePathNodeStore store = open(engine, parent, dbName); + stores.add(store); + return store; + } + + private Map> snapshotNodes() { + Map> snapshot = new LinkedHashMap<>(); + for (Map.Entry entry : participantStores.entrySet()) { + snapshot.put(entry.getKey(), entry.getValue().snapshot()); + } + snapshot.put("super", superStore.snapshot()); + return snapshot; + } + + @Override + public void close() { + for (int i = stores.size() - 1; i >= 0; i--) { + stores.get(i).close(); + } + } + } + + private abstract static class EnginePathNodeStore implements PathNodeStore, AutoCloseable { + + abstract Map snapshot(); + + @Override + public abstract void close(); + } + + private static final class LevelPathNodeStore extends EnginePathNodeStore { + + private final Options options = new Options().createIfMissing(true); + private final org.iq80.leveldb.DB database; + + private LevelPathNodeStore(Path directory) { + try { + Files.createDirectories(directory); + database = factory.open(directory.toFile(), options); + } catch (IOException failure) { + throw new IllegalStateException("failed to open test LevelDB", failure); + } + } + + @Override + public byte[] get(byte[] path) { + return database.get(path); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + database.put(path, encodedNode); + } + + @Override + public void delete(byte[] path) { + database.delete(path); + } + + @Override + Map snapshot() { + Map nodes = new LinkedHashMap<>(); + try (DBIterator iterator = database.iterator()) { + iterator.seekToFirst(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + nodes.put(Hex.toHexString(entry.getKey()), Hex.toHexString(entry.getValue())); + } + } catch (IOException failure) { + throw new IllegalStateException("failed to iterate test LevelDB", failure); + } + return nodes; + } + + @Override + public void close() { + try { + database.close(); + } catch (IOException failure) { + throw new IllegalStateException("failed to close test LevelDB", failure); + } + } + } + + private static final class RocksPathNodeStore extends EnginePathNodeStore { + + private final org.rocksdb.Options options = new org.rocksdb.Options().setCreateIfMissing(true); + private final org.rocksdb.RocksDB database; + + private RocksPathNodeStore(Path directory) { + try { + Files.createDirectories(directory); + database = org.rocksdb.RocksDB.open(options, directory.toString()); + } catch (IOException | RocksDBException failure) { + options.close(); + throw new IllegalStateException("failed to open test RocksDB", failure); + } + } + + @Override + public byte[] get(byte[] path) { + try { + return database.get(path); + } catch (RocksDBException failure) { + throw new IllegalStateException("failed to read test RocksDB", failure); + } + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + try { + database.put(path, encodedNode); + } catch (RocksDBException failure) { + throw new IllegalStateException("failed to write test RocksDB", failure); + } + } + + @Override + public void delete(byte[] path) { + try { + database.delete(path); + } catch (RocksDBException failure) { + throw new IllegalStateException("failed to delete test RocksDB", failure); + } + } + + @Override + Map snapshot() { + Map nodes = new LinkedHashMap<>(); + try (RocksIterator iterator = database.newIterator()) { + for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) { + nodes.put(Hex.toHexString(iterator.key()), Hex.toHexString(iterator.value())); + } + iterator.status(); + } catch (RocksDBException failure) { + throw new IllegalStateException("failed to iterate test RocksDB", failure); + } + return nodes; + } + + @Override + public void close() { + database.close(); + options.close(); + } + } +} From f1cc23ffb42e82666fe9e41a42bf4fd9a17f4a62 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 19:23:34 +0800 Subject: [PATCH 043/161] feat(chainbase): define path state participants --- .../PathStateParticipantDescriptor.java | 144 ++++++++++++++++++ .../stateroot/PathStateParticipantScope.java | 6 +- .../PathStateParticipantDescriptorTest.java | 81 ++++++++++ 3 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantDescriptor.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateParticipantDescriptorTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantDescriptor.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantDescriptor.java new file mode 100644 index 00000000000..d2351e5250f --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantDescriptor.java @@ -0,0 +1,144 @@ +package org.tron.core.db2.stateroot; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** Stable participant and comparator identity for the current path-state root domain. */ +public final class PathStateParticipantDescriptor { + + public static final String SCOPE_ID = "path-state-root/exact-27/v1"; + public static final String UNSIGNED_RAW_COMPARATOR = "unsigned-raw/v1"; + public static final String MARKET_PRICE_COMPARATOR = "market-pair-price/v1"; + public static final String MARKET_PRICE_DATABASE = "market_pair_price_to_order"; + + private static final PathStateParticipantDescriptor CURRENT = + new PathStateParticipantDescriptor(); + + private final List stores; + private final Map storesByName; + + private PathStateParticipantDescriptor() { + LinkedHashMap names = new LinkedHashMap<>(); + names.put(1, "abi"); + names.put(2, "accountid-index"); + names.put(3, "account-index"); + names.put(4, "account"); + names.put(5, "account-asset"); + names.put(6, "asset-issue"); + names.put(7, "asset-issue-v2"); + names.put(8, "code"); + names.put(9, "contract-state"); + names.put(10, "contract"); + names.put(11, "DelegatedResourceAccountIndex"); + names.put(12, "DelegatedResource"); + names.put(13, "delegation"); + names.put(14, "properties"); + names.put(15, "exchange"); + names.put(16, "exchange-v2"); + names.put(17, "market_account"); + names.put(18, "market_order"); + names.put(19, MARKET_PRICE_DATABASE); + names.put(20, "market_pair_to_price"); + names.put(21, "proposal"); + names.put(22, "storage-row"); + names.put(23, "votes"); + names.put(24, "witness_schedule"); + names.put(25, "witness"); + names.put(26, "nullifier"); + names.put(27, "IncrementalMerkleTree"); + + List ordered = new ArrayList<>(); + LinkedHashMap byName = new LinkedHashMap<>(); + for (Map.Entry entry : names.entrySet()) { + String comparator = MARKET_PRICE_DATABASE.equals(entry.getValue()) + ? MARKET_PRICE_COMPARATOR : UNSIGNED_RAW_COMPARATOR; + StoreIdentity identity = new StoreIdentity(entry.getKey(), entry.getValue(), comparator); + ordered.add(identity); + if (byName.put(identity.getDbName(), identity) != null) { + throw new IllegalStateException("duplicate path-state database: " + identity.getDbName()); + } + } + if (ordered.size() != 27) { + throw new IllegalStateException("path-state participant descriptor must contain exact-27"); + } + stores = Collections.unmodifiableList(ordered); + storesByName = Collections.unmodifiableMap(byName); + } + + public static PathStateParticipantDescriptor current() { + return CURRENT; + } + + public List getStores() { + return stores; + } + + public StoreIdentity require(String dbName) { + StoreIdentity identity = storesByName.get(Objects.requireNonNull(dbName, "dbName")); + if (identity == null) { + throw new IllegalArgumentException("unknown path-state database: " + dbName); + } + return identity; + } + + /** Requires exact membership while allowing callers to enumerate databases in any order. */ + public void requireExactDatabases(Collection dbNames) { + Collection supplied = Objects.requireNonNull(dbNames, "dbNames"); + LinkedHashSet unique = new LinkedHashSet<>(); + for (String dbName : supplied) { + if (dbName == null) { + throw new IllegalArgumentException("path-state database must not be null"); + } + if (!unique.add(dbName)) { + throw new IllegalArgumentException("duplicate path-state database: " + dbName); + } + } + Set expected = storesByName.keySet(); + if (!expected.equals(unique)) { + LinkedHashSet missing = new LinkedHashSet<>(expected); + missing.removeAll(unique); + LinkedHashSet unexpected = new LinkedHashSet<>(unique); + unexpected.removeAll(expected); + throw new IllegalArgumentException( + "path-state exact-27 mismatch, missing=" + missing + ", unexpected=" + unexpected); + } + } + + /** Immutable Store identity; canonical key/value format is approved in a separate gate. */ + public static final class StoreIdentity { + + private final int storeId; + private final String dbName; + private final String comparatorId; + + private StoreIdentity(int storeId, String dbName, String comparatorId) { + this.storeId = storeId; + this.dbName = dbName; + this.comparatorId = comparatorId; + } + + public int getStoreId() { + return storeId; + } + + public String getDbName() { + return dbName; + } + + public String getComparatorId() { + return comparatorId; + } + + @Override + public String toString() { + return storeId + ":" + dbName + ":" + comparatorId; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantScope.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantScope.java index 42861bcb67a..cc3a0fba9f4 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantScope.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateParticipantScope.java @@ -13,9 +13,9 @@ /** * Fail-closed participant registry for one path-state root format. * - *

TASK-016 has approved ABI and both AssetIssue stores for inclusion, while the complete - * execution-state exact-set remains an H1-L1 gate. This class therefore validates the supplied - * immutable scope and the three mandatory names without inventing the remaining registry. + *

The current exact-set is defined by {@link PathStateParticipantDescriptor}. This lower-level + * class still accepts a supplied immutable scope so standalone trie tests do not imply an approved + * per-Store canonical value format before that separate gate is complete. */ public final class PathStateParticipantScope { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateParticipantDescriptorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateParticipantDescriptorTest.java new file mode 100644 index 00000000000..e1a018bf93b --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateParticipantDescriptorTest.java @@ -0,0 +1,81 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.tron.core.db2.stateroot.PathStateParticipantDescriptor.StoreIdentity; + +public class PathStateParticipantDescriptorTest { + + private static final List EXACT_27 = Arrays.asList( + "abi", "accountid-index", "account-index", "account", "account-asset", + "asset-issue", "asset-issue-v2", "code", "contract-state", "contract", + "DelegatedResourceAccountIndex", "DelegatedResource", "delegation", "properties", + "exchange", "exchange-v2", "market_account", "market_order", + "market_pair_price_to_order", "market_pair_to_price", "proposal", "storage-row", + "votes", "witness_schedule", "witness", "nullifier", "IncrementalMerkleTree"); + + @Test + public void definesIndependentExact27IdentityGolden() { + PathStateParticipantDescriptor descriptor = PathStateParticipantDescriptor.current(); + assertEquals("path-state-root/exact-27/v1", PathStateParticipantDescriptor.SCOPE_ID); + assertEquals(27, descriptor.getStores().size()); + + for (int index = 0; index < EXACT_27.size(); index++) { + StoreIdentity identity = descriptor.getStores().get(index); + assertEquals(index + 1, identity.getStoreId()); + assertEquals(EXACT_27.get(index), identity.getDbName()); + assertEquals(identity, descriptor.require(EXACT_27.get(index))); + } + } + + @Test + public void assignsOnlyTheMarketPriceComparatorProfile() { + PathStateParticipantDescriptor descriptor = PathStateParticipantDescriptor.current(); + for (StoreIdentity identity : descriptor.getStores()) { + String expected = identity.getDbName().equals( + PathStateParticipantDescriptor.MARKET_PRICE_DATABASE) + ? PathStateParticipantDescriptor.MARKET_PRICE_COMPARATOR + : PathStateParticipantDescriptor.UNSIGNED_RAW_COMPARATOR; + assertEquals(expected, identity.getComparatorId()); + } + } + + @Test + public void requiresExactMembershipIndependentOfInputOrder() { + PathStateParticipantDescriptor descriptor = PathStateParticipantDescriptor.current(); + List reversed = new ArrayList<>(EXACT_27); + Collections.reverse(reversed); + descriptor.requireExactDatabases(reversed); + + List missing = new ArrayList<>(EXACT_27); + missing.remove("abi"); + assertThrows(IllegalArgumentException.class, + () -> descriptor.requireExactDatabases(missing)); + + List unexpected = new ArrayList<>(EXACT_27); + unexpected.set(0, "accountTrie"); + assertThrows(IllegalArgumentException.class, + () -> descriptor.requireExactDatabases(unexpected)); + + List duplicate = new ArrayList<>(EXACT_27); + duplicate.add("account"); + assertThrows(IllegalArgumentException.class, + () -> descriptor.requireExactDatabases(duplicate)); + assertThrows(IllegalArgumentException.class, + () -> descriptor.requireExactDatabases(Arrays.asList("abi", null))); + assertThrows(IllegalArgumentException.class, + () -> descriptor.require("unknown")); + } + + @Test + public void exposesAnImmutableOrderedDescriptor() { + assertThrows(UnsupportedOperationException.class, + () -> PathStateParticipantDescriptor.current().getStores().clear()); + } +} From 1b7766f9ec9ee1c61df0c8812046c512bab84a9b Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 19:30:32 +0800 Subject: [PATCH 044/161] feat(chainbase): canonicalize path state values --- .../db2/stateroot/PathStateCanonicalizer.java | 304 ++++++++++++++++++ .../stateroot/PathStateCanonicalizerTest.java | 192 +++++++++++ 2 files changed, 496 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java new file mode 100644 index 00000000000..96c081a0d45 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java @@ -0,0 +1,304 @@ +package org.tron.core.db2.stateroot; + +import com.google.protobuf.InvalidProtocolBufferException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.tron.protos.Protocol.Account; +import org.tron.protos.contract.AssetIssueContractOuterClass.AssetIssueContract; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI; + +/** Canonical key/value boundary for the current path-state root. */ +public final class PathStateCanonicalizer { + + public static final String PHYSICAL_RAW = "physical-raw/v1"; + public static final String P66_ACCOUNT = "p66-account/v1"; + public static final String P66_ACCOUNT_ASSET = "p66-account-asset/v1"; + public static final String STORAGE_ROW = "storage-physical-row/v1"; + public static final String ABI_PROTOBUF = "abi-protobuf/v1"; + public static final String LEGACY_ASSET_PROTOBUF = "asset-name-protobuf/v1"; + public static final String ASSET_V2_PROTOBUF = "asset-id-protobuf/v1"; + + private static final int ADDRESS_LENGTH = 21; + private static final int STORAGE_KEY_LENGTH = 32; + private static final int STORAGE_VALUE_LENGTH = 32; + private static final int BALANCE_LENGTH = Long.BYTES; + private static final int STORE_FORMAT_VERSION = 1; + + private final PathStateParticipantDescriptor descriptor; + private final Map formats; + + public PathStateCanonicalizer() { + descriptor = PathStateParticipantDescriptor.current(); + LinkedHashMap configured = new LinkedHashMap<>(); + for (PathStateParticipantDescriptor.StoreIdentity identity : descriptor.getStores()) { + configured.put(identity.getDbName(), new StoreFormat(identity.getDbName(), PHYSICAL_RAW)); + } + configure(configured, "account", P66_ACCOUNT); + configure(configured, "account-asset", P66_ACCOUNT_ASSET); + configure(configured, "storage-row", STORAGE_ROW); + configure(configured, "abi", ABI_PROTOBUF); + configure(configured, "asset-issue", LEGACY_ASSET_PROTOBUF); + configure(configured, "asset-issue-v2", ASSET_V2_PROTOBUF); + formats = Collections.unmodifiableMap(configured); + } + + public StoreFormat requireFormat(String dbName) { + StoreFormat format = formats.get(Objects.requireNonNull(dbName, "dbName")); + if (format == null) { + throw new IllegalArgumentException("unknown path-state database: " + dbName); + } + return format; + } + + /** Creates the approved exact-27 scope after every Store has an explicit format identity. */ + public PathStateParticipantScope participantScope() { + List participants = new ArrayList<>(); + for (PathStateParticipantDescriptor.StoreIdentity identity : descriptor.getStores()) { + StoreFormat format = requireFormat(identity.getDbName()); + participants.add(new PathStateParticipant(identity.getStoreId(), identity.getDbName(), + format.getStoreFormatVersion())); + } + return new PathStateParticipantScope(participants); + } + + /** Canonicalizes one physical PRESENT value for the target P66 phase. */ + public PathStateMutation put(P66Phase phase, String dbName, byte[] physicalKey, + byte[] rawValue) { + P66Phase target = Objects.requireNonNull(phase, "phase"); + String name = requireFormat(dbName).getDbName(); + byte[] key = canonicalKey(target, name, physicalKey); + byte[] value = canonicalValue(target, name, key, + Objects.requireNonNull(rawValue, "rawValue")); + return PathStateMutation.put(name, key, value); + } + + /** Canonicalizes one physical delete; no PRESENT value is synthesized. */ + public PathStateMutation delete(P66Phase phase, String dbName, byte[] physicalKey) { + P66Phase target = Objects.requireNonNull(phase, "phase"); + String name = requireFormat(dbName).getDbName(); + return PathStateMutation.delete(name, canonicalKey(target, name, physicalKey)); + } + + /** Encodes a P66 direct balance; zero is represented only by a delete mutation. */ + public PathStateMutation accountAsset(P66Phase phase, byte[] address, String tokenId, + long balance) { + P66Phase target = Objects.requireNonNull(phase, "phase"); + if (!target.directAssetsEnabled()) { + throw new IllegalArgumentException("P66-off state must not contain account-asset rows"); + } + byte[] key = accountAssetKey(address, tokenId); + return balance == 0 ? PathStateMutation.delete("account-asset", key) + : PathStateMutation.put("account-asset", key, + ByteBuffer.allocate(BALANCE_LENGTH).putLong(balance).array()); + } + + private static void configure(Map formats, String dbName, String codecId) { + if (formats.replace(dbName, new StoreFormat(dbName, codecId)) == null) { + throw new IllegalStateException("missing path-state format participant: " + dbName); + } + } + + private static byte[] canonicalKey(P66Phase phase, String dbName, byte[] physicalKey) { + byte[] key = copyNonEmpty(physicalKey, "physicalKey"); + switch (dbName) { + case "account": + case "abi": + requireLength(key, ADDRESS_LENGTH, dbName + " key"); + break; + case "account-asset": + if (!phase.directAssetsEnabled()) { + throw new IllegalArgumentException("P66-off state must not contain account-asset rows"); + } + decodeAccountAssetKey(key); + break; + case "asset-issue-v2": + requireCanonicalDecimal(key, "asset-issue-v2 key"); + break; + case "storage-row": + requireLength(key, STORAGE_KEY_LENGTH, "storage-row key"); + break; + default: + break; + } + return key; + } + + private static byte[] canonicalValue(P66Phase phase, String dbName, byte[] physicalKey, + byte[] rawValue) { + byte[] value = Arrays.copyOf(rawValue, rawValue.length); + switch (dbName) { + case "account": + return canonicalAccount(phase, physicalKey, value); + case "account-asset": + requireLength(value, BALANCE_LENGTH, "account-asset value"); + if (ByteBuffer.wrap(value).getLong() == 0) { + throw new IllegalArgumentException("account-asset zero balance must be ABSENT"); + } + return value; + case "storage-row": + requireLength(value, STORAGE_VALUE_LENGTH, "storage-row value"); + if (isZero(value)) { + throw new IllegalArgumentException("storage-row zero word must be ABSENT"); + } + return value; + case "abi": + parseAbi(value); + return value; + case "asset-issue": + requireAssetKey(physicalKey, value, false); + return value; + case "asset-issue-v2": + requireAssetKey(physicalKey, value, true); + return value; + default: + return value; + } + } + + private static byte[] canonicalAccount(P66Phase phase, byte[] physicalKey, byte[] rawValue) { + Account account; + try { + account = Account.parseFrom(rawValue); + } catch (InvalidProtocolBufferException invalid) { + throw new IllegalArgumentException("account value is not valid protobuf", invalid); + } + if (!Arrays.equals(physicalKey, account.getAddress().toByteArray())) { + throw new IllegalArgumentException("account protobuf address does not match physical key"); + } + if (!phase.directAssetsEnabled()) { + if (account.getAssetOptimized()) { + throw new IllegalArgumentException("P66-off Account must not use direct asset layout"); + } + return rawValue; + } + return account.toBuilder() + .setAssetOptimized(true) + .clearAsset() + .clearAssetV2() + .build() + .toByteArray(); + } + + private static void parseAbi(byte[] value) { + try { + ABI.parseFrom(value); + } catch (InvalidProtocolBufferException invalid) { + throw new IllegalArgumentException("abi value is not valid protobuf", invalid); + } + } + + private static void requireAssetKey(byte[] physicalKey, byte[] value, boolean v2) { + AssetIssueContract asset; + try { + asset = AssetIssueContract.parseFrom(value); + } catch (InvalidProtocolBufferException invalid) { + throw new IllegalArgumentException("asset value is not valid protobuf", invalid); + } + byte[] expected = v2 ? asset.getId().getBytes(StandardCharsets.US_ASCII) + : asset.getName().toByteArray(); + if (v2) { + requireCanonicalDecimal(expected, "asset protobuf ID"); + } + if (!Arrays.equals(physicalKey, expected)) { + throw new IllegalArgumentException("asset protobuf identity does not match physical key"); + } + } + + private static byte[] accountAssetKey(byte[] address, String tokenId) { + byte[] canonicalAddress = Arrays.copyOf(Objects.requireNonNull(address, "address"), + address.length); + requireLength(canonicalAddress, ADDRESS_LENGTH, "account-asset address"); + byte[] token = Objects.requireNonNull(tokenId, "tokenId").getBytes(StandardCharsets.US_ASCII); + if (!tokenId.equals(new String(token, StandardCharsets.US_ASCII))) { + throw new IllegalArgumentException("account-asset token ID must be ASCII decimal"); + } + requireCanonicalDecimal(token, "account-asset token ID"); + return ByteBuffer.allocate(canonicalAddress.length + token.length) + .put(canonicalAddress) + .put(token) + .array(); + } + + private static void decodeAccountAssetKey(byte[] key) { + if (key.length <= ADDRESS_LENGTH) { + throw new IllegalArgumentException("account-asset key is too short"); + } + requireCanonicalDecimal(Arrays.copyOfRange(key, ADDRESS_LENGTH, key.length), + "account-asset token ID"); + } + + private static void requireCanonicalDecimal(byte[] value, String name) { + if (value.length == 0 || value.length > 1 && value[0] == '0') { + throw new IllegalArgumentException(name + " is not canonical decimal"); + } + for (byte digit : value) { + if (digit < '0' || digit > '9') { + throw new IllegalArgumentException(name + " is not canonical decimal"); + } + } + } + + private static byte[] copyNonEmpty(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length == 0) { + throw new IllegalArgumentException(name + " must not be empty"); + } + return copy; + } + + private static void requireLength(byte[] value, int length, String name) { + if (value.length != length) { + throw new IllegalArgumentException(name + " must be exactly " + length + " bytes"); + } + } + + private static boolean isZero(byte[] value) { + for (byte current : value) { + if (current != 0) { + return false; + } + } + return true; + } + + public enum P66Phase { + P66_OFF, + P66_ACTIVATION, + P66_ON; + + private boolean directAssetsEnabled() { + return this != P66_OFF; + } + } + + /** Immutable codec identity committed through a participant's Store format version. */ + public static final class StoreFormat { + + private final String dbName; + private final String codecId; + + private StoreFormat(String dbName, String codecId) { + this.dbName = dbName; + this.codecId = codecId; + } + + public String getDbName() { + return dbName; + } + + public String getCodecId() { + return codecId; + } + + public int getStoreFormatVersion() { + return STORE_FORMAT_VERSION; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java new file mode 100644 index 00000000000..66fd65e5470 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java @@ -0,0 +1,192 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import org.junit.Test; +import org.tron.common.utils.ByteArray; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.protos.Protocol.Account; +import org.tron.protos.contract.AssetIssueContractOuterClass.AssetIssueContract; + +public class PathStateCanonicalizerTest { + + private final PathStateCanonicalizer canonicalizer = new PathStateCanonicalizer(); + + @Test + public void assignsFormatsToTheExact27Scope() { + PathStateParticipantScope scope = canonicalizer.participantScope(); + assertEquals(27, scope.getParticipants().size()); + assertEquals(PathStateCanonicalizer.ABI_PROTOBUF, + canonicalizer.requireFormat("abi").getCodecId()); + assertEquals(PathStateCanonicalizer.P66_ACCOUNT, + canonicalizer.requireFormat("account").getCodecId()); + assertEquals(PathStateCanonicalizer.P66_ACCOUNT_ASSET, + canonicalizer.requireFormat("account-asset").getCodecId()); + assertEquals(PathStateCanonicalizer.LEGACY_ASSET_PROTOBUF, + canonicalizer.requireFormat("asset-issue").getCodecId()); + assertEquals(PathStateCanonicalizer.ASSET_V2_PROTOBUF, + canonicalizer.requireFormat("asset-issue-v2").getCodecId()); + assertEquals(PathStateCanonicalizer.STORAGE_ROW, + canonicalizer.requireFormat("storage-row").getCodecId()); + assertEquals(PathStateCanonicalizer.PHYSICAL_RAW, + canonicalizer.requireFormat("proposal").getCodecId()); + } + + @Test + public void p66OffPreservesAccountBytesAndRejectsDirectRows() throws Exception { + byte[] address = address(1); + byte[] raw = account(address).toBuilder() + .putAsset("legacy-name", 7L) + .putAssetV2("1000001", 11L) + .build() + .toByteArray(); + + PathStateMutation account = canonicalizer.put(P66Phase.P66_OFF, "account", address, raw); + assertArrayEquals(raw, account.getCanonicalValue()); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.accountAsset(P66Phase.P66_OFF, address, "1000001", 11L)); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.put(P66Phase.P66_OFF, "account-asset", + accountAssetKey(address, "1000001"), longBytes(11L))); + + byte[] mixed = Account.parseFrom(raw).toBuilder() + .setAssetOptimized(true) + .build() + .toByteArray(); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.put(P66Phase.P66_OFF, "account", address, mixed)); + } + + @Test + public void activationAndOnProduceTheSameCanonicalAccountGolden() throws Exception { + byte[] address = address(2); + byte[] raw = account(address).toBuilder() + .setBalance(99L) + .putAsset("legacy-name", 5L) + .putAssetV2("1000001", 17L) + .build() + .toByteArray(); + + PathStateMutation activation = canonicalizer.put(P66Phase.P66_ACTIVATION, + "account", address, raw); + PathStateMutation on = canonicalizer.put(P66Phase.P66_ON, "account", address, raw); + assertArrayEquals(activation.getCanonicalValue(), on.getCanonicalValue()); + Account canonical = Account.parseFrom(activation.getCanonicalValue()); + assertTrue(canonical.getAssetOptimized()); + assertTrue(canonical.getAssetMap().isEmpty()); + assertTrue(canonical.getAssetV2Map().isEmpty()); + assertEquals("1a154100000000000000000000000000000000000000022063e00301", + ByteArray.toHexString(activation.getCanonicalValue())); + } + + @Test + public void accountAssetZeroIsDeleteAndPresentRowsAreExactSignedLongs() { + byte[] address = address(3); + PathStateMutation present = canonicalizer.accountAsset( + P66Phase.P66_ON, address, "1000001", -9L); + assertFalse(present.isDelete()); + assertArrayEquals(accountAssetKey(address, "1000001"), present.getCanonicalKey()); + assertArrayEquals(longBytes(-9L), present.getCanonicalValue()); + + PathStateMutation zero = canonicalizer.accountAsset( + P66Phase.P66_ON, address, "1000001", 0L); + assertTrue(zero.isDelete()); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.put(P66Phase.P66_ON, "account-asset", + accountAssetKey(address, "1000001"), longBytes(0L))); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.accountAsset(P66Phase.P66_ON, address, "01", 1L)); + } + + @Test + public void storageZeroIsDeleteWhileNonzeroWordIsExact() { + byte[] key = new byte[32]; + key[31] = 1; + byte[] word = new byte[32]; + word[31] = 2; + PathStateMutation present = canonicalizer.put( + P66Phase.P66_ON, "storage-row", key, word); + assertArrayEquals(word, present.getCanonicalValue()); + assertTrue(canonicalizer.delete(P66Phase.P66_ON, "storage-row", key).isDelete()); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.put(P66Phase.P66_ON, "storage-row", key, new byte[32])); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.put(P66Phase.P66_ON, "storage-row", new byte[31], word)); + } + + @Test + public void abiAllowsPresentEmptyButRejectsMalformedProtobufAndAddress() { + byte[] address = address(4); + PathStateMutation cleared = canonicalizer.put(P66Phase.P66_ON, "abi", address, new byte[0]); + assertFalse(cleared.isDelete()); + assertEquals(0, cleared.getCanonicalValue().length); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.put(P66Phase.P66_ON, "abi", address, new byte[]{-1})); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.put(P66Phase.P66_ON, "abi", new byte[20], new byte[0])); + } + + @Test + public void assetStoresRetainIndependentPhysicalIdentities() { + AssetIssueContract asset = AssetIssueContract.newBuilder() + .setName(ByteString.copyFromUtf8("legacy-name")) + .setId("1000001") + .build(); + byte[] raw = asset.toByteArray(); + PathStateMutation legacy = canonicalizer.put(P66Phase.P66_ON, "asset-issue", + "legacy-name".getBytes(StandardCharsets.UTF_8), raw); + PathStateMutation v2 = canonicalizer.put(P66Phase.P66_ON, "asset-issue-v2", + "1000001".getBytes(StandardCharsets.US_ASCII), raw); + assertFalse(legacy.isDelete()); + assertFalse(v2.isDelete()); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.put(P66Phase.P66_ON, "asset-issue", + "other".getBytes(StandardCharsets.UTF_8), raw)); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.put(P66Phase.P66_ON, "asset-issue-v2", + "01000001".getBytes(StandardCharsets.US_ASCII), raw)); + } + + @Test + public void genericPresentEmptyRemainsDistinctFromDelete() { + byte[] key = new byte[]{1}; + PathStateMutation present = canonicalizer.put( + P66Phase.P66_ON, "proposal", key, new byte[0]); + PathStateMutation absent = canonicalizer.delete(P66Phase.P66_ON, "proposal", key); + assertFalse(present.isDelete()); + assertTrue(absent.isDelete()); + assertArrayEquals(new byte[0], present.getCanonicalValue()); + assertThrows(IllegalArgumentException.class, + () -> canonicalizer.put(P66Phase.P66_ON, "unknown", key, new byte[0])); + } + + private static Account account(byte[] address) { + return Account.newBuilder().setAddress(ByteString.copyFrom(address)).build(); + } + + private static byte[] address(int suffix) { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = (byte) suffix; + return address; + } + + private static byte[] accountAssetKey(byte[] address, String tokenId) { + byte[] token = tokenId.getBytes(StandardCharsets.US_ASCII); + return ByteBuffer.allocate(address.length + token.length) + .put(address) + .put(token) + .array(); + } + + private static byte[] longBytes(long value) { + return ByteBuffer.allocate(Long.BYTES).putLong(value).array(); + } +} From db9e8869ba35b71c54d4a049c1139016f2f1f1ca Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 19:40:16 +0800 Subject: [PATCH 045/161] feat(chainbase): bind path state transitions --- .../stateroot/PathStateBlockTransition.java | 249 ++++++++++++++++++ .../PathStateBlockTransitionTest.java | 138 ++++++++++ 2 files changed, 387 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java new file mode 100644 index 00000000000..b5467d021a6 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java @@ -0,0 +1,249 @@ +package org.tron.core.db2.stateroot; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateParticipantDescriptor.StoreIdentity; + +/** + * Immutable, origin-free evidence for one block-final path-state transition. + * + *

The transition intentionally does not distinguish locally generated, normally pushed, or + * fork-reapplied blocks. Once execution reaches the metadata-aware block commit boundary, equal + * block identity, phase, and canonical mutations produce equal evidence. This type does not + * capture sessions, apply a trie, publish a root, or retain historical state. + */ +public final class PathStateBlockTransition { + + public static final int FORMAT_VERSION = 1; + public static final int HASH_LENGTH = 32; + + private static final byte DELETE_TAG = 0; + private static final byte PUT_TAG = 1; + private static final byte[] DOMAIN = + "java-tron/path-state/block-transition".getBytes(StandardCharsets.US_ASCII); + private static final Comparator MUTATION_ORDER = (left, right) -> { + int storeOrder = Integer.compare(left.store.getStoreId(), right.store.getStoreId()); + return storeOrder != 0 ? storeOrder + : compareUnsigned(left.mutation.getCanonicalKey(), right.mutation.getCanonicalKey()); + }; + + private final long blockNumber; + private final byte[] blockHash; + private final byte[] parentHash; + private final long timestamp; + private final P66Phase phase; + private final List mutations; + private final byte[] payloadDigest; + + public PathStateBlockTransition(long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase, Collection mutations) { + if (blockNumber < 0) { + throw new IllegalArgumentException("blockNumber must not be negative"); + } + this.blockNumber = blockNumber; + this.blockHash = copyHash(blockHash, "blockHash"); + this.parentHash = copyHash(parentHash, "parentHash"); + this.timestamp = timestamp; + this.phase = Objects.requireNonNull(phase, "phase"); + List prepared = prepare(mutations); + List canonical = new ArrayList<>(prepared.size()); + for (PreparedMutation mutation : prepared) { + canonical.add(mutation.mutation); + } + this.mutations = Collections.unmodifiableList(canonical); + this.payloadDigest = sha256(encode(prepared)); + } + + public long getBlockNumber() { + return blockNumber; + } + + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + public byte[] getParentHash() { + return Arrays.copyOf(parentHash, parentHash.length); + } + + public long getTimestamp() { + return timestamp; + } + + public P66Phase getPhase() { + return phase; + } + + public String getScopeId() { + return PathStateParticipantDescriptor.SCOPE_ID; + } + + public List getMutations() { + return mutations; + } + + public byte[] getPayloadDigest() { + return Arrays.copyOf(payloadDigest, payloadDigest.length); + } + + private List prepare(Collection supplied) { + PathStateParticipantDescriptor descriptor = PathStateParticipantDescriptor.current(); + List prepared = new ArrayList<>(); + Set unique = new LinkedHashSet<>(); + for (PathStateMutation candidate : Objects.requireNonNull(supplied, "mutations")) { + PathStateMutation mutation = copyMutation(Objects.requireNonNull(candidate, "mutation")); + StoreIdentity store = descriptor.require(mutation.getDbName()); + byte[] key = mutation.getCanonicalKey(); + if (key.length == 0) { + throw new IllegalArgumentException("canonicalKey must not be empty"); + } + if (!unique.add(new MutationKey(store.getStoreId(), key))) { + throw new IllegalArgumentException("duplicate path-state mutation key"); + } + prepared.add(new PreparedMutation(store, mutation)); + } + prepared.sort(MUTATION_ORDER); + return prepared; + } + + private byte[] encode(List prepared) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + writeBytes(output, DOMAIN); + output.writeShort(FORMAT_VERSION); + writeBytes(output, PathStateParticipantDescriptor.SCOPE_ID + .getBytes(StandardCharsets.UTF_8)); + output.writeByte(phaseTag(phase)); + output.writeLong(blockNumber); + output.write(blockHash); + output.write(parentHash); + output.writeLong(timestamp); + output.writeInt(prepared.size()); + for (PreparedMutation current : prepared) { + PathStateMutation mutation = current.mutation; + output.writeInt(current.store.getStoreId()); + writeBytes(output, current.store.getDbName().getBytes(StandardCharsets.UTF_8)); + output.writeByte(mutation.isDelete() ? DELETE_TAG : PUT_TAG); + writeSizedBytes(output, mutation.getCanonicalKey()); + byte[] value = mutation.getCanonicalValue(); + if (value == null) { + output.writeInt(-1); + } else { + writeSizedBytes(output, value); + } + } + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory path-state transition encoding failed", impossible); + } + } + + private static PathStateMutation copyMutation(PathStateMutation mutation) { + return mutation.isDelete() + ? PathStateMutation.delete(mutation.getDbName(), mutation.getCanonicalKey()) + : PathStateMutation.put(mutation.getDbName(), mutation.getCanonicalKey(), + mutation.getCanonicalValue()); + } + + private static byte[] copyHash(byte[] value, String name) { + byte[] hash = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (hash.length != HASH_LENGTH) { + throw new IllegalArgumentException(name + " must be exactly " + HASH_LENGTH + " bytes"); + } + return hash; + } + + private static void writeBytes(DataOutputStream output, byte[] value) throws IOException { + if (value.length > 0xffff) { + throw new IllegalArgumentException("identity field exceeds unsigned-short length"); + } + output.writeShort(value.length); + output.write(value); + } + + private static void writeSizedBytes(DataOutputStream output, byte[] value) throws IOException { + output.writeInt(value.length); + output.write(value); + } + + private static int phaseTag(P66Phase phase) { + switch (phase) { + case P66_OFF: + return 0; + case P66_ACTIVATION: + return 1; + case P66_ON: + return 2; + default: + throw new IllegalArgumentException("unknown P66 phase: " + phase); + } + } + + private static byte[] sha256(byte[] value) { + try { + return MessageDigest.getInstance("SHA-256").digest(value); + } catch (NoSuchAlgorithmException impossible) { + throw new IllegalStateException("SHA-256 is unavailable", impossible); + } + } + + private static int compareUnsigned(byte[] left, byte[] right) { + for (int i = 0; i < Math.min(left.length, right.length); i++) { + int comparison = Integer.compare(left[i] & 0xff, right[i] & 0xff); + if (comparison != 0) { + return comparison; + } + } + return Integer.compare(left.length, right.length); + } + + private static final class PreparedMutation { + + private final StoreIdentity store; + private final PathStateMutation mutation; + + private PreparedMutation(StoreIdentity store, PathStateMutation mutation) { + this.store = store; + this.mutation = mutation; + } + } + + private static final class MutationKey { + + private final int storeId; + private final byte[] key; + + private MutationKey(int storeId, byte[] key) { + this.storeId = storeId; + this.key = Arrays.copyOf(key, key.length); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof MutationKey + && storeId == ((MutationKey) other).storeId + && Arrays.equals(key, ((MutationKey) other).key); + } + + @Override + public int hashCode() { + return 31 * storeId + Arrays.hashCode(key); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java new file mode 100644 index 00000000000..4ab8b57ca29 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java @@ -0,0 +1,138 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.tron.common.utils.ByteArray; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; + +public class PathStateBlockTransitionTest { + + private static final byte[] BLOCK_HASH = bytes(1); + private static final byte[] PARENT_HASH = bytes(33); + + @Test + public void canonicalEvidenceIsOriginFreeAndMutationOrderInvariant() { + PathStateMutation proposal = PathStateMutation.put("proposal", new byte[]{2}, new byte[]{3}); + PathStateMutation accountDelete = PathStateMutation.delete("account", new byte[]{1}); + + PathStateBlockTransition pushed = transition(Arrays.asList(proposal, accountDelete)); + PathStateBlockTransition forkReapplied = transition(Arrays.asList(accountDelete, proposal)); + + assertArrayEquals(pushed.getPayloadDigest(), forkReapplied.getPayloadDigest()); + assertEquals("account", pushed.getMutations().get(0).getDbName()); + assertEquals("proposal", pushed.getMutations().get(1).getDbName()); + assertEquals(PathStateParticipantDescriptor.SCOPE_ID, pushed.getScopeId()); + assertEquals( + "b216f6028db74c2457f1fd625ff4218ad1c358f74d016baa4bc17828e9b4ac7a", + ByteArray.toHexString(pushed.getPayloadDigest())); + } + + @Test + public void digestBindsBlockPhaseAndMutationSemantics() { + PathStateMutation put = PathStateMutation.put("proposal", new byte[]{2}, new byte[0]); + PathStateBlockTransition baseline = transition(Collections.singletonList(put)); + PathStateBlockTransition otherBlock = new PathStateBlockTransition(43, BLOCK_HASH, PARENT_HASH, + 1234, P66Phase.P66_ON, Collections.singletonList(put)); + PathStateBlockTransition otherPhase = new PathStateBlockTransition(42, BLOCK_HASH, PARENT_HASH, + 1234, P66Phase.P66_ACTIVATION, Collections.singletonList(put)); + PathStateBlockTransition delete = transition(Collections.singletonList( + PathStateMutation.delete("proposal", new byte[]{2}))); + + assertNotEquals(ByteArray.toHexString(baseline.getPayloadDigest()), + ByteArray.toHexString(otherBlock.getPayloadDigest())); + assertNotEquals(ByteArray.toHexString(baseline.getPayloadDigest()), + ByteArray.toHexString(otherPhase.getPayloadDigest())); + assertNotEquals(ByteArray.toHexString(baseline.getPayloadDigest()), + ByteArray.toHexString(delete.getPayloadDigest())); + } + + @Test + public void noOpBlockHasDeterministicIndependentOracleDigest() throws Exception { + PathStateBlockTransition transition = transition(Collections.emptyList()); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + writeIdentity(output, "java-tron/path-state/block-transition"); + output.writeShort(1); + writeIdentity(output, PathStateParticipantDescriptor.SCOPE_ID); + output.writeByte(2); + output.writeLong(42); + output.write(BLOCK_HASH); + output.write(PARENT_HASH); + output.writeLong(1234); + output.writeInt(0); + + assertArrayEquals(Hashing.sha256().hashBytes(bytes.toByteArray()).asBytes(), + transition.getPayloadDigest()); + } + + @Test + public void rejectsAmbiguousOrOutOfScopeMutations() { + PathStateMutation first = PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}); + PathStateMutation duplicate = PathStateMutation.delete("proposal", new byte[]{1}); + + assertThrows(IllegalArgumentException.class, + () -> transition(Arrays.asList(first, duplicate))); + assertThrows(IllegalArgumentException.class, + () -> transition(Collections.singletonList( + PathStateMutation.put("unknown", new byte[]{1}, new byte[]{2})))); + assertThrows(IllegalArgumentException.class, + () -> transition(Collections.singletonList( + PathStateMutation.put("proposal", new byte[0], new byte[]{2})))); + } + + @Test + public void ownsBlockAndMutationBytes() { + byte[] blockHash = Arrays.copyOf(BLOCK_HASH, BLOCK_HASH.length); + byte[] key = new byte[]{1}; + byte[] value = new byte[]{2}; + PathStateBlockTransition transition = new PathStateBlockTransition(42, blockHash, PARENT_HASH, + 1234, P66Phase.P66_ON, + Collections.singletonList(PathStateMutation.put("proposal", key, value))); + byte[] digest = transition.getPayloadDigest(); + + blockHash[0] = 99; + key[0] = 99; + value[0] = 99; + transition.getBlockHash()[0] = 98; + transition.getMutations().get(0).getCanonicalKey()[0] = 98; + transition.getPayloadDigest()[0] = 98; + + assertArrayEquals(BLOCK_HASH, transition.getBlockHash()); + assertArrayEquals(new byte[]{1}, transition.getMutations().get(0).getCanonicalKey()); + assertArrayEquals(new byte[]{2}, transition.getMutations().get(0).getCanonicalValue()); + assertArrayEquals(digest, transition.getPayloadDigest()); + assertThrows(UnsupportedOperationException.class, + () -> transition.getMutations().add(PathStateMutation.delete("proposal", new byte[]{3}))); + } + + private static PathStateBlockTransition transition(List mutations) { + return new PathStateBlockTransition(42, BLOCK_HASH, PARENT_HASH, 1234, + P66Phase.P66_ON, mutations); + } + + private static void writeIdentity(DataOutputStream output, String value) throws Exception { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + output.writeShort(encoded.length); + output.write(encoded); + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int i = 0; i < value.length; i++) { + value[i] = (byte) (seed + i); + } + return value; + } +} From 356f8b355434b057179d215f6a31addb22eddd31 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 19:48:52 +0800 Subject: [PATCH 046/161] feat(chainbase): define path state persistence --- .../db2/stateroot/PathStateRootMetadata.java | 266 ++++++++++++++++++ .../db2/stateroot/PathStateStoreManifest.java | 241 ++++++++++++++++ .../PathStatePersistentFormatTest.java | 140 +++++++++ 3 files changed, 647 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRootMetadata.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRootMetadata.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRootMetadata.java new file mode 100644 index 00000000000..6b8302047ef --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRootMetadata.java @@ -0,0 +1,266 @@ +package org.tron.core.db2.stateroot; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; + +/** Immutable identity shared by a durable base or one reversible current-root layer. */ +public final class PathStateRootMetadata { + + public static final int DIGEST_LENGTH = 32; + + private static final int MAGIC = 0x50534d54; // PSMT + private static final short VERSION = 1; + private static final int MAX_LENGTH = 16 * 1024; + + private final Kind kind; + private final long blockNumber; + private final byte[] blockHash; + private final byte[] parentHash; + private final long timestamp; + private final P66Phase phase; + private final byte[] parentStateRoot; + private final byte[] stateRoot; + private final byte[] payloadDigest; + + private PathStateRootMetadata(Kind kind, long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase, byte[] parentStateRoot, byte[] stateRoot, + byte[] payloadDigest) { + if (blockNumber < 0) { + throw new IllegalArgumentException("blockNumber must not be negative"); + } + this.kind = Objects.requireNonNull(kind, "kind"); + this.blockNumber = blockNumber; + this.blockHash = copy32(blockHash, "blockHash"); + this.parentHash = copy32(parentHash, "parentHash"); + this.timestamp = timestamp; + this.phase = Objects.requireNonNull(phase, "phase"); + this.parentStateRoot = parentStateRoot == null ? null + : copy32(parentStateRoot, "parentStateRoot"); + this.stateRoot = copy32(stateRoot, "stateRoot"); + this.payloadDigest = copy32(payloadDigest, "payloadDigest"); + if (kind == Kind.BASE && this.parentStateRoot != null) { + throw new IllegalArgumentException("base metadata must not contain a parent state root"); + } + if (kind == Kind.LAYER && this.parentStateRoot == null) { + throw new IllegalArgumentException("layer metadata requires a parent state root"); + } + } + + /** Creates metadata for a rebuilt or compacted durable base. */ + public static PathStateRootMetadata base(long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase, byte[] stateRoot, byte[] sourceDigest) { + return new PathStateRootMetadata(Kind.BASE, blockNumber, blockHash, parentHash, timestamp, + phase, null, stateRoot, sourceDigest); + } + + /** Creates metadata for one immutable reversible transition above a parent root. */ + public static PathStateRootMetadata layer(long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase, byte[] parentStateRoot, byte[] stateRoot, + byte[] transitionDigest) { + return new PathStateRootMetadata(Kind.LAYER, blockNumber, blockHash, parentHash, timestamp, + phase, parentStateRoot, stateRoot, transitionDigest); + } + + public Kind getKind() { + return kind; + } + + public long getBlockNumber() { + return blockNumber; + } + + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + public byte[] getParentHash() { + return Arrays.copyOf(parentHash, parentHash.length); + } + + public long getTimestamp() { + return timestamp; + } + + public P66Phase getPhase() { + return phase; + } + + public byte[] getParentStateRoot() { + return parentStateRoot == null ? null : Arrays.copyOf(parentStateRoot, parentStateRoot.length); + } + + public byte[] getStateRoot() { + return Arrays.copyOf(stateRoot, stateRoot.length); + } + + public byte[] getPayloadDigest() { + return Arrays.copyOf(payloadDigest, payloadDigest.length); + } + + /** Encodes metadata with an exact scope identity and CRC32C corruption check. */ + public byte[] encode() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(0); + writeString(output, PathStateParticipantDescriptor.SCOPE_ID); + output.writeByte(kind.tag); + output.writeLong(blockNumber); + output.write(blockHash); + output.write(parentHash); + output.writeLong(timestamp); + output.writeByte(phaseTag(phase)); + if (parentStateRoot == null) { + output.writeByte(0); + } else { + output.writeByte(parentStateRoot.length); + output.write(parentStateRoot); + } + output.write(stateRoot); + output.write(payloadDigest); + output.flush(); + byte[] payload = bytes.toByteArray(); + int length = payload.length + Integer.BYTES; + ByteBuffer.wrap(payload).putInt(8, length); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(payload); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory path-state metadata encoding failed", impossible); + } + } + + public static PathStateRootMetadata decode(byte[] encoded) { + byte[] bytes = Arrays.copyOf(Objects.requireNonNull(encoded, "encoded"), encoded.length); + if (bytes.length <= Integer.BYTES || bytes.length > MAX_LENGTH) { + throw new IllegalArgumentException("path-state metadata length is invalid"); + } + byte[] payload = Arrays.copyOf(bytes, bytes.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(bytes, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new IllegalArgumentException("path-state metadata checksum mismatch"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != bytes.length) { + throw new IllegalArgumentException("unsupported path-state metadata header"); + } + if (!PathStateParticipantDescriptor.SCOPE_ID.equals(readString(input))) { + throw new IllegalArgumentException("path-state metadata scope mismatch"); + } + Kind kind = Kind.fromTag(input.readUnsignedByte()); + long blockNumber = input.readLong(); + byte[] blockHash = read32(input); + byte[] parentHash = read32(input); + long timestamp = input.readLong(); + P66Phase phase = phase(input.readUnsignedByte()); + int parentLength = input.readUnsignedByte(); + byte[] parentRoot = null; + if (parentLength == DIGEST_LENGTH) { + parentRoot = read32(input); + } else if (parentLength != 0) { + throw new IllegalArgumentException("path-state parent root length is invalid"); + } + byte[] stateRoot = read32(input); + byte[] digest = read32(input); + if (input.available() != Integer.BYTES) { + throw new IllegalArgumentException("path-state metadata payload mismatch"); + } + return new PathStateRootMetadata(kind, blockNumber, blockHash, parentHash, timestamp, phase, + parentRoot, stateRoot, digest); + } catch (IOException invalid) { + throw new IllegalArgumentException("path-state metadata is truncated", invalid); + } + } + + private static byte[] copy32(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length != DIGEST_LENGTH) { + throw new IllegalArgumentException(name + " must be exactly " + DIGEST_LENGTH + " bytes"); + } + return copy; + } + + private static void writeString(DataOutputStream output, String value) throws IOException { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + output.writeShort(encoded.length); + output.write(encoded); + } + + private static String readString(DataInputStream input) throws IOException { + int length = input.readUnsignedShort(); + if (length == 0 || length > 1024 || length > input.available() - Integer.BYTES) { + throw new IllegalArgumentException("path-state metadata string is invalid"); + } + byte[] value = new byte[length]; + input.readFully(value); + return new String(value, StandardCharsets.UTF_8); + } + + private static byte[] read32(DataInputStream input) throws IOException { + byte[] value = new byte[DIGEST_LENGTH]; + input.readFully(value); + return value; + } + + private static int phaseTag(P66Phase phase) { + switch (phase) { + case P66_OFF: + return 0; + case P66_ACTIVATION: + return 1; + case P66_ON: + return 2; + default: + throw new IllegalArgumentException("unknown P66 phase: " + phase); + } + } + + private static P66Phase phase(int tag) { + switch (tag) { + case 0: + return P66Phase.P66_OFF; + case 1: + return P66Phase.P66_ACTIVATION; + case 2: + return P66Phase.P66_ON; + default: + throw new IllegalArgumentException("unknown path-state P66 phase tag: " + tag); + } + } + + public enum Kind { + BASE(0), + LAYER(1); + + private final int tag; + + Kind(int tag) { + this.tag = tag; + } + + private static Kind fromTag(int tag) { + for (Kind value : values()) { + if (value.tag == tag) { + return value; + } + } + throw new IllegalArgumentException("unknown path-state metadata kind: " + tag); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java new file mode 100644 index 00000000000..e0b88d9fb4e --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java @@ -0,0 +1,241 @@ +package org.tron.core.db2.stateroot; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.UUID; +import org.tron.core.db2.stateroot.PathStateParticipantDescriptor.StoreIdentity; + +/** Durable format admission anchor for an enabled current path-state backend. */ +public final class PathStateStoreManifest { + + public static final String MANIFEST_FILE = "MANIFEST"; + public static final String BASE_DIRECTORY = "base"; + public static final String LAYERS_DIRECTORY = "layers"; + + private static final int MAGIC = 0x50534d46; // PSMF + private static final short VERSION = 1; + private static final int MAX_LENGTH = 1024 * 1024; + + private final Path directory; + private final Engine engine; + + private PathStateStoreManifest(Path directory, Engine engine) { + this.directory = directory; + this.engine = engine; + } + + /** Creates a new exact-format manifest or validates an existing one without rewriting it. */ + public static PathStateStoreManifest createOrOpen(Path directory, Engine engine) + throws IOException { + Path root = directory.toAbsolutePath().normalize(); + Engine selected = requireEngine(engine); + rejectSymbolicLink(root); + Files.createDirectories(root); + requireDirectory(root, "path-state root"); + + byte[] expected = encode(selected); + Path manifest = root.resolve(MANIFEST_FILE); + if (Files.exists(manifest, LinkOption.NOFOLLOW_LINKS)) { + validateExisting(manifest, expected); + } else { + publish(manifest, expected); + } + ensureChildDirectory(root.resolve(BASE_DIRECTORY)); + ensureChildDirectory(root.resolve(LAYERS_DIRECTORY)); + return new PathStateStoreManifest(root, selected); + } + + /** Validates an existing manifest without creating or modifying filesystem entries. */ + public static PathStateStoreManifest validateExisting(Path directory, Engine engine) + throws IOException { + Path root = directory.toAbsolutePath().normalize(); + Engine selected = requireEngine(engine); + rejectSymbolicLink(root); + requireDirectory(root, "path-state root"); + Path manifest = root.resolve(MANIFEST_FILE); + validateExisting(manifest, encode(selected)); + requireDirectory(root.resolve(BASE_DIRECTORY), "path-state base"); + requireDirectory(root.resolve(LAYERS_DIRECTORY), "path-state layers"); + return new PathStateStoreManifest(root, selected); + } + + public Path getDirectory() { + return directory; + } + + public Path getBaseDirectory() { + return directory.resolve(BASE_DIRECTORY); + } + + public Path getLayersDirectory() { + return directory.resolve(LAYERS_DIRECTORY); + } + + public Engine getEngine() { + return engine; + } + + private static byte[] encode(Engine engine) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(0); + writeString(output, PathStateParticipantDescriptor.SCOPE_ID); + output.writeByte(engine.tag); + output.writeShort(PathStateCommitmentCodec.FORMAT_VERSION); + PathStateParticipantDescriptor descriptor = PathStateParticipantDescriptor.current(); + PathStateCanonicalizer canonicalizer = new PathStateCanonicalizer(); + output.writeInt(descriptor.getStores().size()); + for (StoreIdentity store : descriptor.getStores()) { + PathStateCanonicalizer.StoreFormat format = + canonicalizer.requireFormat(store.getDbName()); + output.writeInt(store.getStoreId()); + writeString(output, store.getDbName()); + writeString(output, store.getComparatorId()); + output.writeInt(format.getStoreFormatVersion()); + writeString(output, format.getCodecId()); + } + output.flush(); + byte[] payload = bytes.toByteArray(); + int length = payload.length + Integer.BYTES; + if (length > MAX_LENGTH) { + throw new IllegalArgumentException("path-state manifest is too large"); + } + ByteBuffer.wrap(payload).putInt(8, length); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(payload); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory path-state manifest encoding failed", impossible); + } + } + + private static void validateExisting(Path manifest, byte[] expected) throws IOException { + if (!Files.isRegularFile(manifest, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state manifest is missing or not a regular file"); + } + long size = Files.size(manifest); + if (size <= Integer.BYTES || size > MAX_LENGTH) { + throw new IOException("path-state manifest length is invalid"); + } + byte[] actual = Files.readAllBytes(manifest); + decodeHeader(actual); + if (!Arrays.equals(expected, actual)) { + throw new IOException("path-state manifest identity mismatch"); + } + } + + private static void decodeHeader(byte[] encoded) throws IOException { + if (encoded.length <= Integer.BYTES || encoded.length > MAX_LENGTH) { + throw new IOException("path-state manifest length is invalid"); + } + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(encoded, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new IOException("path-state manifest checksum mismatch"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != encoded.length) { + throw new IOException("unsupported path-state manifest header"); + } + } + } + + private static void publish(Path manifest, byte[] encoded) throws IOException { + Path directory = manifest.getParent(); + Path temporary = directory.resolve(".MANIFEST-" + UUID.randomUUID()); + try { + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + writeFully(channel, ByteBuffer.wrap(encoded)); + channel.force(true); + } + try { + Files.move(temporary, manifest, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("path-state manifest requires atomic publication", unsupported); + } + syncDirectory(directory); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static void ensureChildDirectory(Path path) throws IOException { + rejectSymbolicLink(path); + Files.createDirectories(path); + requireDirectory(path, "path-state child"); + } + + private static void rejectSymbolicLink(Path path) throws IOException { + if (Files.isSymbolicLink(path)) { + throw new IOException("path-state path must not be a symbolic link: " + path); + } + } + + private static void requireDirectory(Path path, String name) throws IOException { + if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException(name + " is missing or not a directory: " + path); + } + } + + private static Engine requireEngine(Engine engine) { + if (engine == null) { + throw new IllegalArgumentException("path-state engine must not be null"); + } + return engine; + } + + private static void writeString(DataOutputStream output, String value) throws IOException { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + if (encoded.length == 0 || encoded.length > 1024) { + throw new IllegalArgumentException("path-state manifest string is invalid"); + } + output.writeShort(encoded.length); + output.write(encoded); + } + + private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } + + private static void syncDirectory(Path directory) throws IOException { + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + public enum Engine { + LEVELDB(1), + ROCKSDB(2); + + private final int tag; + + Engine(int tag) { + this.tag = tag; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java new file mode 100644 index 00000000000..842eb61c59e --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java @@ -0,0 +1,140 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.common.hash.Hashing; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.utils.ByteArray; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStatePersistentFormatTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void manifestCreatesIndependentCurrentOnlyLayoutAndReopensWithoutRewrite() + throws Exception { + Path root = new File(temporaryFolder.getRoot(), "path-state-root").toPath(); + PathStateStoreManifest created = PathStateStoreManifest.createOrOpen(root, Engine.LEVELDB); + Path manifest = root.resolve(PathStateStoreManifest.MANIFEST_FILE); + byte[] original = Files.readAllBytes(manifest); + + PathStateStoreManifest reopened = PathStateStoreManifest.createOrOpen(root, Engine.LEVELDB); + + assertEquals(root.toAbsolutePath(), created.getDirectory()); + assertEquals(Engine.LEVELDB, reopened.getEngine()); + assertTrue(Files.isDirectory(created.getBaseDirectory())); + assertTrue(Files.isDirectory(created.getLayersDirectory())); + assertArrayEquals(original, Files.readAllBytes(manifest)); + assertEquals( + "d0fc17ad2ea70578b2400c8c3563b05407ff6d7f53f26ea7ad47b513565d404e", + ByteArray.toHexString(Hashing.sha256().hashBytes(original).asBytes())); + assertFalse(Files.exists(root.resolve("history"))); + } + + @Test + public void manifestRejectsEngineDriftAndCorruptionWithoutRepair() throws Exception { + Path root = temporaryFolder.newFolder("manifest-fail-closed").toPath(); + PathStateStoreManifest.createOrOpen(root, Engine.LEVELDB); + Path manifest = root.resolve(PathStateStoreManifest.MANIFEST_FILE); + byte[] expected = Files.readAllBytes(manifest); + + assertThrows(IOException.class, + () -> PathStateStoreManifest.createOrOpen(root, Engine.ROCKSDB)); + assertArrayEquals(expected, Files.readAllBytes(manifest)); + + byte[] corrupt = Arrays.copyOf(expected, expected.length); + corrupt[corrupt.length - 1] ^= 1; + Files.write(manifest, corrupt); + assertThrows(IOException.class, + () -> PathStateStoreManifest.validateExisting(root, Engine.LEVELDB)); + assertArrayEquals(corrupt, Files.readAllBytes(manifest)); + } + + @Test + public void validateExistingHasNoCreationSideEffects() { + Path absent = new File(temporaryFolder.getRoot(), "absent").toPath(); + + assertThrows(IOException.class, + () -> PathStateStoreManifest.validateExisting(absent, Engine.LEVELDB)); + assertFalse(Files.exists(absent)); + } + + @Test + public void baseMetadataRoundTripsWithoutInventingAParentRoot() { + PathStateRootMetadata base = PathStateRootMetadata.base(100, bytes(1), bytes(33), 9000, + P66Phase.P66_ACTIVATION, bytes(65), bytes(97)); + + PathStateRootMetadata decoded = PathStateRootMetadata.decode(base.encode()); + + assertEquals(Kind.BASE, decoded.getKind()); + assertEquals(100, decoded.getBlockNumber()); + assertEquals(P66Phase.P66_ACTIVATION, decoded.getPhase()); + assertNull(decoded.getParentStateRoot()); + assertArrayEquals(bytes(65), decoded.getStateRoot()); + assertEquals( + "c4bfd4fac0a8b536c6350270b63f9937a789fec846331b8c21bc6f5d7d183757", + ByteArray.toHexString(Hashing.sha256().hashBytes(base.encode()).asBytes())); + } + + @Test + public void layerMetadataBindsParentRootAndTransitionDigest() { + PathStateRootMetadata layer = PathStateRootMetadata.layer(101, bytes(2), bytes(1), 12000, + P66Phase.P66_ON, bytes(65), bytes(66), bytes(98)); + + PathStateRootMetadata decoded = PathStateRootMetadata.decode(layer.encode()); + + assertEquals(Kind.LAYER, decoded.getKind()); + assertArrayEquals(bytes(65), decoded.getParentStateRoot()); + assertArrayEquals(bytes(66), decoded.getStateRoot()); + assertArrayEquals(bytes(98), decoded.getPayloadDigest()); + assertNotNull(decoded.getBlockHash()); + } + + @Test + public void metadataRejectsKindAmbiguityAndCorruption() { + assertThrows(IllegalArgumentException.class, + () -> PathStateRootMetadata.layer(1, bytes(1), bytes(2), 3, P66Phase.P66_ON, + null, bytes(3), bytes(4))); + byte[] encoded = PathStateRootMetadata.base(1, bytes(1), bytes(2), 3, P66Phase.P66_OFF, + bytes(3), bytes(4)).encode(); + encoded[encoded.length - 1] ^= 1; + assertThrows(IllegalArgumentException.class, () -> PathStateRootMetadata.decode(encoded)); + } + + @Test + public void metadataOwnsAllByteArrays() { + byte[] root = bytes(65); + PathStateRootMetadata metadata = PathStateRootMetadata.base(1, bytes(1), bytes(2), 3, + P66Phase.P66_ON, root, bytes(97)); + root[0] = 0; + byte[] returned = metadata.getStateRoot(); + returned[0] = 0; + + assertArrayEquals(bytes(65), metadata.getStateRoot()); + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int i = 0; i < value.length; i++) { + value[i] = (byte) (seed + i); + } + return value; + } +} From dc910f87501b02922c0e89d062642fa5726f4719 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 20:46:50 +0800 Subject: [PATCH 047/161] feat(chainbase): persist path state authority --- .../db2/stateroot/PathStateCurrentStore.java | 162 ++++++++++++++ .../db2/stateroot/PathStateMetadataFile.java | 124 +++++++++++ .../db2/stateroot/PathStateRootMetadata.java | 23 +- .../db2/stateroot/PathStateStoreManifest.java | 6 + .../stateroot/PathStateCurrentStoreTest.java | 202 ++++++++++++++++++ .../PathStatePersistentFormatTest.java | 17 +- 6 files changed, 521 insertions(+), 13 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentStoreTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java new file mode 100644 index 00000000000..1a9323e6c68 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java @@ -0,0 +1,162 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Locale; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; + +/** + * Current-head authority over one durable base and its immutable reversible metadata chain. + * + *

This component intentionally exposes only the verified current record. Traversal of older + * layers is an internal startup integrity check, not a historical-root query API. + */ +public final class PathStateCurrentStore { + + public static final String METADATA_FILE = "METADATA"; + public static final String CURRENT_FILE = "CURRENT"; + + private static final int MAX_VALIDATION_LAYERS = 65_536; + + private final PathStateStoreManifest manifest; + private final Path currentPath; + + public PathStateCurrentStore(PathStateStoreManifest manifest) { + this.manifest = Objects.requireNonNull(manifest, "manifest"); + this.currentPath = manifest.getDirectory().resolve(CURRENT_FILE); + } + + public boolean isInitialized() { + return Files.exists(currentPath, LinkOption.NOFOLLOW_LINKS); + } + + /** Publishes the first durable base and then atomically makes it current. */ + public synchronized PathStateRootMetadata publishBase(PathStateRootMetadata base) + throws IOException { + PathStateRootMetadata metadata = requireKind(base, Kind.BASE); + requireFormat(metadata); + if (isInitialized()) { + PathStateRootMetadata current = current(); + if (!Arrays.equals(current.encode(), metadata.encode())) { + throw new IOException("path-state CURRENT already identifies another root"); + } + return current; + } + PathStateMetadataFile.publishImmutable(basePath(), metadata); + PathStateMetadataFile.replaceCurrent(currentPath, metadata); + return current(); + } + + /** Publishes one immutable child layer before atomically advancing CURRENT. */ + public synchronized PathStateRootMetadata appendLayer(PathStateRootMetadata layer) + throws IOException { + PathStateRootMetadata child = requireKind(layer, Kind.LAYER); + requireFormat(child); + PathStateRootMetadata parent = current(); + if (same(parent, child)) { + return parent; + } + requireChild(parent, child); + PathStateMetadataFile.publishImmutable(layerPath(child), child); + PathStateMetadataFile.replaceCurrent(currentPath, child); + return current(); + } + + /** Loads CURRENT and verifies that every referenced layer reaches the single durable base. */ + public synchronized PathStateRootMetadata current() throws IOException { + PathStateRootMetadata base = requireKind(PathStateMetadataFile.load(basePath()), Kind.BASE); + requireFormat(base); + PathStateRootMetadata head = PathStateMetadataFile.load(currentPath); + requireFormat(head); + PathStateRootMetadata storedHead = PathStateMetadataFile.load(metadataPath(head)); + requireFormat(storedHead); + requireSame(head, storedHead, "CURRENT metadata differs from its immutable record"); + + PathStateRootMetadata child = head; + int layers = 0; + while (child.getKind() == Kind.LAYER) { + if (++layers > MAX_VALIDATION_LAYERS) { + throw new IOException("path-state layer chain exceeds the validation safety limit"); + } + if (child.getBlockNumber() == 0) { + throw new IOException("path-state layer zero cannot have a parent"); + } + PathStateRootMetadata parent; + if (base.getBlockNumber() == child.getBlockNumber() - 1) { + parent = base; + } else { + parent = requireKind(PathStateMetadataFile.load( + layerPath(child.getBlockNumber() - 1, child.getParentHash())), Kind.LAYER); + requireFormat(parent); + } + requireChild(parent, child); + child = parent; + } + requireSame(base, child, "path-state layer chain does not terminate at the durable base"); + return head; + } + + private Path metadataPath(PathStateRootMetadata metadata) { + return metadata.getKind() == Kind.BASE ? basePath() : layerPath(metadata); + } + + private Path basePath() { + return manifest.getBaseDirectory().resolve(METADATA_FILE); + } + + private Path layerPath(PathStateRootMetadata metadata) { + return layerPath(metadata.getBlockNumber(), metadata.getBlockHash()); + } + + private Path layerPath(long blockNumber, byte[] blockHash) { + String directory = String.format(Locale.ROOT, "%020d-%s", blockNumber, hex(blockHash)); + return manifest.getLayersDirectory().resolve(directory).resolve(METADATA_FILE); + } + + private static PathStateRootMetadata requireKind(PathStateRootMetadata metadata, Kind kind) { + PathStateRootMetadata present = Objects.requireNonNull(metadata, "metadata"); + if (present.getKind() != kind) { + throw new IllegalArgumentException("expected path-state " + kind + " metadata"); + } + return present; + } + + private void requireFormat(PathStateRootMetadata metadata) throws IOException { + if (!Arrays.equals(metadata.getFormatDigest(), manifest.getIdentityDigest())) { + throw new IOException("path-state metadata manifest identity mismatch"); + } + } + + private static void requireChild(PathStateRootMetadata parent, PathStateRootMetadata child) + throws IOException { + if (child.getBlockNumber() != parent.getBlockNumber() + 1 + || !Arrays.equals(child.getParentHash(), parent.getBlockHash()) + || !Arrays.equals(child.getParentStateRoot(), parent.getStateRoot())) { + throw new IOException("path-state layer does not extend CURRENT"); + } + } + + private static void requireSame(PathStateRootMetadata expected, + PathStateRootMetadata actual, String error) throws IOException { + if (!same(expected, actual)) { + throw new IOException(error); + } + } + + private static boolean same(PathStateRootMetadata left, PathStateRootMetadata right) { + return Arrays.equals(left.encode(), right.encode()); + } + + private static String hex(byte[] value) { + StringBuilder encoded = new StringBuilder(value.length * 2); + for (byte current : value) { + encoded.append(Character.forDigit(current >>> 4 & 0xf, 16)); + encoded.append(Character.forDigit(current & 0xf, 16)); + } + return encoded.toString(); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java new file mode 100644 index 00000000000..25d8c7a0baa --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java @@ -0,0 +1,124 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; + +/** Atomic-file boundary for immutable BASE/LAYER metadata and the replaceable CURRENT authority. */ +final class PathStateMetadataFile { + + private PathStateMetadataFile() { + } + + static PathStateRootMetadata load(Path path) throws IOException { + Path target = Objects.requireNonNull(path, "path"); + if (!Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state metadata is missing or not a regular file: " + target); + } + try { + return PathStateRootMetadata.decode(Files.readAllBytes(target)); + } catch (IllegalArgumentException invalid) { + throw new IOException("path-state metadata is corrupt: " + target, invalid); + } + } + + /** Publishes once; an exact existing record is an idempotent retry, not a rewrite. */ + static void publishImmutable(Path path, PathStateRootMetadata metadata) throws IOException { + Path target = Objects.requireNonNull(path, "path"); + byte[] encoded = Objects.requireNonNull(metadata, "metadata").encode(); + if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) { + requireExact(target, encoded); + return; + } + publish(target, encoded, false, temporary -> { }); + } + + static void replaceCurrent(Path path, PathStateRootMetadata metadata) throws IOException { + replaceCurrent(path, metadata, temporary -> { }); + } + + static void replaceCurrent(Path path, PathStateRootMetadata metadata, FaultHook faultHook) + throws IOException { + publish(Objects.requireNonNull(path, "path"), + Objects.requireNonNull(metadata, "metadata").encode(), true, + Objects.requireNonNull(faultHook, "faultHook")); + } + + static void requireExact(Path path, PathStateRootMetadata metadata) throws IOException { + requireExact(path, Objects.requireNonNull(metadata, "metadata").encode()); + } + + private static void requireExact(Path path, byte[] expected) throws IOException { + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state metadata is not a regular file: " + path); + } + byte[] actual = Files.readAllBytes(path); + try { + PathStateRootMetadata.decode(actual); + } catch (IllegalArgumentException invalid) { + throw new IOException("path-state metadata is corrupt: " + path, invalid); + } + if (!Arrays.equals(expected, actual)) { + throw new IOException("immutable path-state metadata identity mismatch: " + path); + } + } + + private static void publish(Path target, byte[] encoded, boolean replace, FaultHook faultHook) + throws IOException { + Path directory = Objects.requireNonNull(target.getParent(), "metadata directory"); + Files.createDirectories(directory); + if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS) + || Files.isSymbolicLink(directory)) { + throw new IOException("path-state metadata parent is not a direct directory: " + directory); + } + Path temporary = directory.resolve("." + target.getFileName() + "-" + UUID.randomUUID()); + try { + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + writeFully(channel, ByteBuffer.wrap(encoded)); + channel.force(true); + } + faultHook.afterTemporaryForce(temporary); + try { + if (replace) { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } else { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE); + } + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("path-state metadata requires atomic publication", unsupported); + } + syncDirectory(directory); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } + + private static void syncDirectory(Path directory) throws IOException { + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + @FunctionalInterface + interface FaultHook { + + void afterTemporaryForce(Path temporary) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRootMetadata.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRootMetadata.java index 6b8302047ef..a5c453d30b0 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRootMetadata.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRootMetadata.java @@ -27,12 +27,13 @@ public final class PathStateRootMetadata { private final byte[] parentHash; private final long timestamp; private final P66Phase phase; + private final byte[] formatDigest; private final byte[] parentStateRoot; private final byte[] stateRoot; private final byte[] payloadDigest; private PathStateRootMetadata(Kind kind, long blockNumber, byte[] blockHash, byte[] parentHash, - long timestamp, P66Phase phase, byte[] parentStateRoot, byte[] stateRoot, + long timestamp, P66Phase phase, byte[] formatDigest, byte[] parentStateRoot, byte[] stateRoot, byte[] payloadDigest) { if (blockNumber < 0) { throw new IllegalArgumentException("blockNumber must not be negative"); @@ -43,6 +44,7 @@ private PathStateRootMetadata(Kind kind, long blockNumber, byte[] blockHash, byt this.parentHash = copy32(parentHash, "parentHash"); this.timestamp = timestamp; this.phase = Objects.requireNonNull(phase, "phase"); + this.formatDigest = copy32(formatDigest, "formatDigest"); this.parentStateRoot = parentStateRoot == null ? null : copy32(parentStateRoot, "parentStateRoot"); this.stateRoot = copy32(stateRoot, "stateRoot"); @@ -57,17 +59,18 @@ private PathStateRootMetadata(Kind kind, long blockNumber, byte[] blockHash, byt /** Creates metadata for a rebuilt or compacted durable base. */ public static PathStateRootMetadata base(long blockNumber, byte[] blockHash, byte[] parentHash, - long timestamp, P66Phase phase, byte[] stateRoot, byte[] sourceDigest) { + long timestamp, P66Phase phase, byte[] formatDigest, byte[] stateRoot, + byte[] sourceDigest) { return new PathStateRootMetadata(Kind.BASE, blockNumber, blockHash, parentHash, timestamp, - phase, null, stateRoot, sourceDigest); + phase, formatDigest, null, stateRoot, sourceDigest); } /** Creates metadata for one immutable reversible transition above a parent root. */ public static PathStateRootMetadata layer(long blockNumber, byte[] blockHash, byte[] parentHash, - long timestamp, P66Phase phase, byte[] parentStateRoot, byte[] stateRoot, - byte[] transitionDigest) { + long timestamp, P66Phase phase, byte[] formatDigest, byte[] parentStateRoot, + byte[] stateRoot, byte[] transitionDigest) { return new PathStateRootMetadata(Kind.LAYER, blockNumber, blockHash, parentHash, timestamp, - phase, parentStateRoot, stateRoot, transitionDigest); + phase, formatDigest, parentStateRoot, stateRoot, transitionDigest); } public Kind getKind() { @@ -94,6 +97,10 @@ public P66Phase getPhase() { return phase; } + public byte[] getFormatDigest() { + return Arrays.copyOf(formatDigest, formatDigest.length); + } + public byte[] getParentStateRoot() { return parentStateRoot == null ? null : Arrays.copyOf(parentStateRoot, parentStateRoot.length); } @@ -116,6 +123,7 @@ public byte[] encode() { output.writeShort(0); output.writeInt(0); writeString(output, PathStateParticipantDescriptor.SCOPE_ID); + output.write(formatDigest); output.writeByte(kind.tag); output.writeLong(blockNumber); output.write(blockHash); @@ -163,6 +171,7 @@ public static PathStateRootMetadata decode(byte[] encoded) { if (!PathStateParticipantDescriptor.SCOPE_ID.equals(readString(input))) { throw new IllegalArgumentException("path-state metadata scope mismatch"); } + byte[] formatDigest = read32(input); Kind kind = Kind.fromTag(input.readUnsignedByte()); long blockNumber = input.readLong(); byte[] blockHash = read32(input); @@ -182,7 +191,7 @@ public static PathStateRootMetadata decode(byte[] encoded) { throw new IllegalArgumentException("path-state metadata payload mismatch"); } return new PathStateRootMetadata(kind, blockNumber, blockHash, parentHash, timestamp, phase, - parentRoot, stateRoot, digest); + formatDigest, parentRoot, stateRoot, digest); } catch (IOException invalid) { throw new IllegalArgumentException("path-state metadata is truncated", invalid); } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java index e0b88d9fb4e..fb870305730 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java @@ -32,10 +32,12 @@ public final class PathStateStoreManifest { private final Path directory; private final Engine engine; + private final byte[] identityDigest; private PathStateStoreManifest(Path directory, Engine engine) { this.directory = directory; this.engine = engine; + this.identityDigest = Hashing.sha256().hashBytes(encode(engine)).asBytes(); } /** Creates a new exact-format manifest or validates an existing one without rewriting it. */ @@ -89,6 +91,10 @@ public Engine getEngine() { return engine; } + public byte[] getIdentityDigest() { + return Arrays.copyOf(identityDigest, identityDigest.length); + } + private static byte[] encode(Engine engine) { try { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentStoreTest.java new file mode 100644 index 00000000000..5f44d6f5a86 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentStoreTest.java @@ -0,0 +1,202 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateCurrentStoreTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void publishesBaseAndLayersThenVerifiesCurrentAfterReopen() throws Exception { + PathStateStoreManifest manifest = manifest("normal-chain"); + PathStateCurrentStore store = new PathStateCurrentStore(manifest); + assertFalse(store.isInitialized()); + + PathStateRootMetadata base = base(manifest, 100); + PathStateRootMetadata first = layer(manifest, 101, base, 11); + PathStateRootMetadata second = layer(manifest, 102, first, 12); + store.publishBase(base); + store.appendLayer(first); + store.appendLayer(second); + + PathStateCurrentStore reopened = new PathStateCurrentStore( + PathStateStoreManifest.validateExisting(manifest.getDirectory(), Engine.LEVELDB)); + PathStateRootMetadata current = reopened.current(); + assertTrue(reopened.isInitialized()); + assertEquals(102, current.getBlockNumber()); + assertEquals(Kind.LAYER, current.getKind()); + assertArrayEquals(second.getStateRoot(), current.getStateRoot()); + assertEquals(2, childDirectoryCount(manifest.getLayersDirectory())); + } + + @Test + public void exactPublicationRetriesAreIdempotent() throws Exception { + PathStateStoreManifest manifest = manifest("idempotent"); + PathStateCurrentStore store = new PathStateCurrentStore(manifest); + PathStateRootMetadata base = base(manifest, 100); + PathStateRootMetadata child = layer(manifest, 101, base, 11); + + store.publishBase(base); + store.publishBase(base); + store.appendLayer(child); + store.appendLayer(child); + + assertEquals(101, store.current().getBlockNumber()); + assertEquals(1, childDirectoryCount(manifest.getLayersDirectory())); + } + + @Test + public void rejectsLayerThatDoesNotExtendCurrentBeforePublication() throws Exception { + PathStateStoreManifest manifest = manifest("discontinuous"); + PathStateCurrentStore store = new PathStateCurrentStore(manifest); + PathStateRootMetadata base = base(manifest, 100); + store.publishBase(base); + PathStateRootMetadata wrongParent = PathStateRootMetadata.layer(101, hash(101), hash(99), + 303, P66Phase.P66_ON, manifest.getIdentityDigest(), root(100), root(101), digest(101)); + + assertThrows(IOException.class, () -> store.appendLayer(wrongParent)); + assertEquals(100, store.current().getBlockNumber()); + assertEquals(0, childDirectoryCount(manifest.getLayersDirectory())); + } + + @Test + public void rejectsMetadataFromAnotherManifestIdentity() throws Exception { + PathStateStoreManifest manifest = manifest("foreign-format"); + PathStateCurrentStore store = new PathStateCurrentStore(manifest); + PathStateRootMetadata foreign = PathStateRootMetadata.base(100, hash(100), hash(99), 300, + P66Phase.P66_ON, digest(17), root(100), digest(100)); + + assertThrows(IOException.class, () -> store.publishBase(foreign)); + assertFalse(store.isInitialized()); + } + + @Test + public void startupFailsClosedWhenMiddleLayerIsMissing() throws Exception { + PathStateStoreManifest manifest = manifest("missing-middle"); + PathStateCurrentStore store = new PathStateCurrentStore(manifest); + PathStateRootMetadata base = base(manifest, 100); + PathStateRootMetadata first = layer(manifest, 101, base, 11); + PathStateRootMetadata second = layer(manifest, 102, first, 12); + store.publishBase(base); + store.appendLayer(first); + store.appendLayer(second); + + Path middle = onlyMetadataForHeight(manifest.getLayersDirectory(), 101); + Files.delete(middle); + + assertThrows(IOException.class, store::current); + } + + @Test + public void immutableMetadataRejectsDifferentRetryWithoutRewrite() throws Exception { + Path path = new File(temporaryFolder.getRoot(), "immutable/METADATA").toPath(); + PathStateRootMetadata original = rawBase(100); + PathStateMetadataFile.publishImmutable(path, original); + byte[] encoded = Files.readAllBytes(path); + + assertThrows(IOException.class, + () -> PathStateMetadataFile.publishImmutable(path, rawBase(101))); + assertArrayEquals(encoded, Files.readAllBytes(path)); + } + + @Test + public void failedCurrentReplacementPreservesPreviousAuthorityAndCleansTemporary() + throws Exception { + PathStateStoreManifest manifest = manifest("current-fault"); + PathStateCurrentStore store = new PathStateCurrentStore(manifest); + PathStateRootMetadata base = base(manifest, 100); + store.publishBase(base); + Path current = manifest.getDirectory().resolve(PathStateCurrentStore.CURRENT_FILE); + byte[] previous = Files.readAllBytes(current); + PathStateRootMetadata child = layer(manifest, 101, base, 11); + + assertThrows(IOException.class, () -> PathStateMetadataFile.replaceCurrent(current, child, + temporary -> { + assertTrue(Files.exists(temporary)); + throw new IOException("injected after temporary force"); + })); + + assertArrayEquals(previous, Files.readAllBytes(current)); + try (Stream paths = Files.list(manifest.getDirectory())) { + assertFalse(paths.anyMatch(path -> path.getFileName().toString().startsWith(".CURRENT-"))); + } + assertEquals(100, store.current().getBlockNumber()); + } + + private PathStateStoreManifest manifest(String name) throws Exception { + Path root = temporaryFolder.newFolder(name).toPath(); + return PathStateStoreManifest.createOrOpen(root, Engine.LEVELDB); + } + + private static PathStateRootMetadata base(PathStateStoreManifest manifest, long blockNumber) { + return PathStateRootMetadata.base(blockNumber, hash(blockNumber), hash(blockNumber - 1), + blockNumber * 3, P66Phase.P66_ON, manifest.getIdentityDigest(), root(blockNumber), + digest(blockNumber)); + } + + private static PathStateRootMetadata layer(PathStateStoreManifest manifest, long blockNumber, + PathStateRootMetadata parent, int salt) { + return PathStateRootMetadata.layer(blockNumber, hash(blockNumber), parent.getBlockHash(), + blockNumber * 3, P66Phase.P66_ON, manifest.getIdentityDigest(), parent.getStateRoot(), + root(blockNumber), digest(salt)); + } + + private static PathStateRootMetadata rawBase(long blockNumber) { + return PathStateRootMetadata.base(blockNumber, hash(blockNumber), hash(blockNumber - 1), + blockNumber * 3, P66Phase.P66_ON, digest(17), root(blockNumber), digest(blockNumber)); + } + + private static Path onlyMetadataForHeight(Path layers, long blockNumber) throws Exception { + String prefix = String.format(Locale.ROOT, "%020d-", blockNumber); + try (Stream paths = Files.list(layers)) { + Path directory = paths.filter(path -> path.getFileName().toString().startsWith(prefix)) + .findFirst() + .orElseThrow(() -> new AssertionError("layer directory not found")); + return directory.resolve(PathStateCurrentStore.METADATA_FILE); + } + } + + private static long childDirectoryCount(Path path) throws Exception { + try (Stream paths = Files.list(path)) { + return paths.filter(Files::isDirectory).count(); + } + } + + private static byte[] hash(long value) { + return bytes((int) value); + } + + private static byte[] root(long value) { + return bytes((int) value + 64); + } + + private static byte[] digest(long value) { + return bytes((int) value + 96); + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int i = 0; i < value.length; i++) { + value[i] = (byte) (seed + i); + } + return value; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java index 842eb61c59e..c3d4195ab98 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java @@ -79,7 +79,7 @@ public void validateExistingHasNoCreationSideEffects() { @Test public void baseMetadataRoundTripsWithoutInventingAParentRoot() { PathStateRootMetadata base = PathStateRootMetadata.base(100, bytes(1), bytes(33), 9000, - P66Phase.P66_ACTIVATION, bytes(65), bytes(97)); + P66Phase.P66_ACTIVATION, bytes(17), bytes(65), bytes(97)); PathStateRootMetadata decoded = PathStateRootMetadata.decode(base.encode()); @@ -89,14 +89,14 @@ public void baseMetadataRoundTripsWithoutInventingAParentRoot() { assertNull(decoded.getParentStateRoot()); assertArrayEquals(bytes(65), decoded.getStateRoot()); assertEquals( - "c4bfd4fac0a8b536c6350270b63f9937a789fec846331b8c21bc6f5d7d183757", + "ab8833ebb7c43812cd6f041aae7796da3c97c51ae3c8890636d2d6b45a471e0d", ByteArray.toHexString(Hashing.sha256().hashBytes(base.encode()).asBytes())); } @Test public void layerMetadataBindsParentRootAndTransitionDigest() { PathStateRootMetadata layer = PathStateRootMetadata.layer(101, bytes(2), bytes(1), 12000, - P66Phase.P66_ON, bytes(65), bytes(66), bytes(98)); + P66Phase.P66_ON, bytes(17), bytes(65), bytes(66), bytes(98)); PathStateRootMetadata decoded = PathStateRootMetadata.decode(layer.encode()); @@ -111,22 +111,27 @@ public void layerMetadataBindsParentRootAndTransitionDigest() { public void metadataRejectsKindAmbiguityAndCorruption() { assertThrows(IllegalArgumentException.class, () -> PathStateRootMetadata.layer(1, bytes(1), bytes(2), 3, P66Phase.P66_ON, - null, bytes(3), bytes(4))); + bytes(17), null, bytes(3), bytes(4))); byte[] encoded = PathStateRootMetadata.base(1, bytes(1), bytes(2), 3, P66Phase.P66_OFF, - bytes(3), bytes(4)).encode(); + bytes(17), bytes(3), bytes(4)).encode(); encoded[encoded.length - 1] ^= 1; assertThrows(IllegalArgumentException.class, () -> PathStateRootMetadata.decode(encoded)); } @Test public void metadataOwnsAllByteArrays() { + byte[] format = bytes(17); byte[] root = bytes(65); PathStateRootMetadata metadata = PathStateRootMetadata.base(1, bytes(1), bytes(2), 3, - P66Phase.P66_ON, root, bytes(97)); + P66Phase.P66_ON, format, root, bytes(97)); + format[0] = 0; root[0] = 0; + byte[] returnedFormat = metadata.getFormatDigest(); byte[] returned = metadata.getStateRoot(); + returnedFormat[0] = 0; returned[0] = 0; + assertArrayEquals(bytes(17), metadata.getFormatDigest()); assertArrayEquals(bytes(65), metadata.getStateRoot()); } From 36d9d95d6245cd727b972cac101aa7b9c92e7bd4 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 21:38:44 +0800 Subject: [PATCH 048/161] feat(chainbase): add native path node stores --- .../db2/stateroot/PathStateCurrentStore.java | 12 +- .../stateroot/PathStateNativeNodeStore.java | 191 ++++++++++++++++++ .../db2/stateroot/PathStateNodeStoreSet.java | 143 +++++++++++++ .../db2/stateroot/PathStateStoreManifest.java | 22 ++ .../PathStateNativeNodeStoreTest.java | 161 +++++++++++++++ 5 files changed, 518 insertions(+), 11 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java index 1a9323e6c68..42cd45a6878 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java @@ -5,7 +5,6 @@ import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.Arrays; -import java.util.Locale; import java.util.Objects; import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; @@ -113,8 +112,7 @@ private Path layerPath(PathStateRootMetadata metadata) { } private Path layerPath(long blockNumber, byte[] blockHash) { - String directory = String.format(Locale.ROOT, "%020d-%s", blockNumber, hex(blockHash)); - return manifest.getLayersDirectory().resolve(directory).resolve(METADATA_FILE); + return manifest.getLayerDirectory(blockNumber, blockHash).resolve(METADATA_FILE); } private static PathStateRootMetadata requireKind(PathStateRootMetadata metadata, Kind kind) { @@ -151,12 +149,4 @@ private static boolean same(PathStateRootMetadata left, PathStateRootMetadata ri return Arrays.equals(left.encode(), right.encode()); } - private static String hex(byte[] value) { - StringBuilder encoded = new StringBuilder(value.length * 2); - for (byte current : value) { - encoded.append(Character.forDigit(current >>> 4 & 0xf, 16)); - encoded.append(Character.forDigit(current & 0xf, 16)); - } - return encoded.toString(); - } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java new file mode 100644 index 00000000000..27255bf1a10 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java @@ -0,0 +1,191 @@ +package org.tron.core.db2.stateroot; + +import static org.fusesource.leveldbjni.JniDBFactory.factory; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Objects; +import org.iq80.leveldb.DB; +import org.iq80.leveldb.WriteOptions; +import org.rocksdb.RocksDBException; +import org.tron.common.utils.DbOptionalsUtils; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Package-owned LevelDB/RocksDB key/value engine shared by namespaced path-node views. */ +final class PathStateNativeNodeStore implements Closeable { + + static { + org.rocksdb.RocksDB.loadLibrary(); + } + + private final Path directory; + private final Engine engine; + private final Delegate delegate; + private boolean closed; + + private PathStateNativeNodeStore(Path directory, Engine engine, Delegate delegate) { + this.directory = directory; + this.engine = engine; + this.delegate = delegate; + } + + /** Opens one independent node database; every mutation is synchronously WAL-backed. */ + static PathStateNativeNodeStore open(Path directory, Engine engine) throws IOException { + Path path = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); + Engine selected = Objects.requireNonNull(engine, "engine"); + if (Files.isSymbolicLink(path)) { + throw new IOException("path-state node database must not be a symbolic link: " + path); + } + Files.createDirectories(path); + if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state node database is not a directory: " + path); + } + Delegate opened = selected == Engine.LEVELDB ? new LevelDelegate(path) + : new RocksDelegate(path); + return new PathStateNativeNodeStore(path, selected, opened); + } + + synchronized byte[] get(byte[] key) { + requireOpen(); + byte[] ownedKey = nonEmpty(key, "key"); + byte[] value = delegate.get(ownedKey); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + synchronized void put(byte[] key, byte[] value) { + requireOpen(); + delegate.put(nonEmpty(key, "key"), nonEmpty(value, "value")); + } + + synchronized void delete(byte[] key) { + requireOpen(); + delegate.delete(nonEmpty(key, "key")); + } + + Path getDirectory() { + return directory; + } + + Engine getEngine() { + return engine; + } + + @Override + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + delegate.close(); + } + } + + private void requireOpen() { + if (closed) { + throw new IllegalStateException("path-state node database is closed: " + directory); + } + } + + private static byte[] nonEmpty(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length == 0) { + throw new IllegalArgumentException(name + " must not be empty"); + } + return copy; + } + + private interface Delegate extends Closeable { + + byte[] get(byte[] key); + + void put(byte[] key, byte[] value); + + void delete(byte[] key); + } + + private static final class LevelDelegate implements Delegate { + + private final org.iq80.leveldb.Options options = DbOptionalsUtils.createDefaultDbOptions(); + private final WriteOptions syncWrites = new WriteOptions().sync(true); + private final DB database; + + private LevelDelegate(Path directory) throws IOException { + database = factory.open(directory.toFile(), options); + } + + @Override + public byte[] get(byte[] key) { + return database.get(key); + } + + @Override + public void put(byte[] key, byte[] value) { + database.put(key, value, syncWrites); + } + + @Override + public void delete(byte[] key) { + database.delete(key, syncWrites); + } + + @Override + public void close() throws IOException { + database.close(); + } + } + + private static final class RocksDelegate implements Delegate { + + private final org.rocksdb.Options options = + new org.rocksdb.Options().setCreateIfMissing(true).setParanoidChecks(true); + private final org.rocksdb.WriteOptions syncWrites = + new org.rocksdb.WriteOptions().setSync(true); + private final org.rocksdb.RocksDB database; + + private RocksDelegate(Path directory) throws IOException { + try { + database = org.rocksdb.RocksDB.open(options, directory.toString()); + } catch (RocksDBException failure) { + syncWrites.close(); + options.close(); + throw new IOException("failed to open path-state RocksDB node database", failure); + } + } + + @Override + public byte[] get(byte[] key) { + try { + return database.get(key); + } catch (RocksDBException failure) { + throw new IllegalStateException("failed to read path-state RocksDB node", failure); + } + } + + @Override + public void put(byte[] key, byte[] value) { + try { + database.put(syncWrites, key, value); + } catch (RocksDBException failure) { + throw new IllegalStateException("failed to write path-state RocksDB node", failure); + } + } + + @Override + public void delete(byte[] key) { + try { + database.delete(syncWrites, key); + } catch (RocksDBException failure) { + throw new IllegalStateException("failed to delete path-state RocksDB node", failure); + } + } + + @Override + public void close() { + syncWrites.close(); + database.close(); + options.close(); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java new file mode 100644 index 00000000000..571451acf89 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -0,0 +1,143 @@ +package org.tron.core.db2.stateroot; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; + +/** Exact-27 participant and super-trie namespace views over one BASE or LAYER native database. */ +public final class PathStateNodeStoreSet implements Closeable { + + public static final String NODES_DIRECTORY = "nodes"; + + private final Path directory; + private final PathStateParticipantScope scope; + private final Map participantStores = new LinkedHashMap<>(); + private final PathStateNativeNodeStore nativeStore; + private final PathNodeStore superStore; + private boolean rootClaimed; + private boolean closed; + + private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest) + throws IOException { + this.directory = directory.resolve(NODES_DIRECTORY); + this.scope = new PathStateCanonicalizer().participantScope(); + nativeStore = PathStateNativeNodeStore.open(this.directory, manifest.getEngine()); + for (PathStateParticipant participant : scope.getParticipants()) { + participantStores.put(participant.getDbName(), + new NamespacedNodeStore(nativeStore, participant.getStoreId())); + } + superStore = new NamespacedNodeStore(nativeStore, 0); + } + + public static PathStateNodeStoreSet openBase(PathStateStoreManifest manifest) + throws IOException { + PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); + requireUnsealed(admitted.getBaseDirectory()); + return new PathStateNodeStoreSet(admitted.getBaseDirectory(), admitted); + } + + public static PathStateNodeStoreSet openLayer(PathStateStoreManifest manifest, + PathStateRootMetadata metadata) throws IOException { + PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); + PathStateRootMetadata layer = Objects.requireNonNull(metadata, "metadata"); + if (layer.getKind() != Kind.LAYER) { + throw new IllegalArgumentException("path-state layer node set requires LAYER metadata"); + } + if (!Arrays.equals(layer.getFormatDigest(), admitted.getIdentityDigest())) { + throw new IOException("path-state layer node set manifest identity mismatch"); + } + Path layerDirectory = admitted.getLayerDirectory(layer.getBlockNumber(), layer.getBlockHash()); + requireUnsealed(layerDirectory); + return new PathStateNodeStoreSet(layerDirectory, admitted); + } + + /** Claims these databases for one in-process trie owner. */ + public synchronized PathStateRoot createRoot() { + requireOpen(); + if (rootClaimed) { + throw new IllegalStateException("path-state node database set already has a trie owner"); + } + rootClaimed = true; + return new PathStateRoot(scope, participant -> participantStores.get(participant.getDbName()), + superStore); + } + + public Path getDirectory() { + return directory; + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + nativeStore.close(); + } + + private void requireOpen() { + if (closed) { + throw new IllegalStateException("path-state node database set is closed: " + directory); + } + } + + private static void requireUnsealed(Path directory) throws IOException { + Path metadata = directory.resolve(PathStateCurrentStore.METADATA_FILE); + if (Files.exists(metadata, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state node database set is sealed by immutable metadata: " + + directory); + } + } + + private static final class NamespacedNodeStore implements PathNodeStore { + + private final PathStateNativeNodeStore nativeStore; + private final int storeId; + + private NamespacedNodeStore(PathStateNativeNodeStore nativeStore, int storeId) { + this.nativeStore = nativeStore; + this.storeId = storeId; + } + + @Override + public byte[] get(byte[] path) { + return nativeStore.get(key(path)); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + byte[] value = Arrays.copyOf(Objects.requireNonNull(encodedNode, "encodedNode"), + encodedNode.length); + if (value.length == 0) { + throw new IllegalArgumentException("encodedNode must not be empty"); + } + nativeStore.put(key(path), value); + } + + @Override + public void delete(byte[] path) { + nativeStore.delete(key(path)); + } + + private byte[] key(byte[] path) { + byte[] ownedPath = Arrays.copyOf(Objects.requireNonNull(path, "path"), path.length); + for (byte nibble : ownedPath) { + if (nibble < 0 || nibble > 15) { + throw new IllegalArgumentException("path-state node path contains a non-nibble byte"); + } + } + return ByteBuffer.allocate(Integer.BYTES + ownedPath.length) + .putInt(storeId) + .put(ownedPath) + .array(); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java index fb870305730..b7ccb0160ff 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java @@ -16,6 +16,7 @@ import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.Arrays; +import java.util.Locale; import java.util.UUID; import org.tron.core.db2.stateroot.PathStateParticipantDescriptor.StoreIdentity; @@ -87,6 +88,18 @@ public Path getLayersDirectory() { return directory.resolve(LAYERS_DIRECTORY); } + public Path getLayerDirectory(long blockNumber, byte[] blockHash) { + if (blockNumber < 0) { + throw new IllegalArgumentException("blockNumber must not be negative"); + } + byte[] hash = Arrays.copyOf(blockHash, blockHash.length); + if (hash.length != PathStateRootMetadata.DIGEST_LENGTH) { + throw new IllegalArgumentException("blockHash must be exactly 32 bytes"); + } + return getLayersDirectory().resolve(String.format(Locale.ROOT, "%020d-%s", + blockNumber, hex(hash))); + } + public Engine getEngine() { return engine; } @@ -234,6 +247,15 @@ private static void syncDirectory(Path directory) throws IOException { } } + private static String hex(byte[] value) { + StringBuilder encoded = new StringBuilder(value.length * 2); + for (byte current : value) { + encoded.append(Character.forDigit(current >>> 4 & 0xf, 16)); + encoded.append(Character.forDigit(current & 0xf, 16)); + } + return encoded.toString(); + } + public enum Engine { LEVELDB(1), ROCKSDB(2); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java new file mode 100644 index 00000000000..24b924ee0b3 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -0,0 +1,161 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.arch.Arch; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateNativeNodeStoreTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void nativeStoresOwnBytesAndPreserveSyncedMutationsAcrossReopen() throws Exception { + for (Engine engine : availableEngines()) { + Path directory = new File(temporaryFolder.getRoot(), "native-" + engine).toPath(); + byte[] path = new byte[]{1, 2, 3}; + byte[] node = new byte[]{4, 5, 6}; + try (PathStateNativeNodeStore store = PathStateNativeNodeStore.open(directory, engine)) { + store.put(path, node); + path[0] = 15; + node[0] = 15; + byte[] returned = store.get(new byte[]{1, 2, 3}); + returned[0] = 15; + assertArrayEquals(new byte[]{4, 5, 6}, store.get(new byte[]{1, 2, 3})); + } + try (PathStateNativeNodeStore reopened = PathStateNativeNodeStore.open(directory, engine)) { + assertArrayEquals(new byte[]{4, 5, 6}, reopened.get(new byte[]{1, 2, 3})); + reopened.delete(new byte[]{1, 2, 3}); + } + try (PathStateNativeNodeStore reopened = PathStateNativeNodeStore.open(directory, engine)) { + assertNull(reopened.get(new byte[]{1, 2, 3})); + } + } + } + + @Test + public void nativeStoreRejectsInvalidEntriesAndUseAfterClose() throws Exception { + Path directory = temporaryFolder.newFolder("native-invalid").toPath(); + PathStateNativeNodeStore store = PathStateNativeNodeStore.open(directory, Engine.ROCKSDB); + assertThrows(IllegalArgumentException.class, () -> store.put(new byte[0], new byte[]{1})); + assertThrows(IllegalArgumentException.class, () -> store.put(new byte[]{1}, new byte[0])); + store.close(); + store.close(); + assertThrows(IllegalStateException.class, () -> store.get(new byte[0])); + } + + @Test + public void baseStoreSetCreatesExact27PlusSuperAndPersistsRootNodes() throws Exception { + PathStateStoreManifest manifest = manifest("base-set", Engine.ROCKSDB); + byte[] rootHash; + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}))); + rootHash = root.rootHash(); + root.verifyNodeStores(); + assertThrows(IllegalStateException.class, stores::createRoot); + } + + assertEquals(1, childDirectoryCount(manifest.getBaseDirectory())); + try (PathStateNativeNodeStore nodes = PathStateNativeNodeStore.open( + manifest.getBaseDirectory().resolve("nodes"), Engine.ROCKSDB)) { + assertNotNull(nodes.get(namespaceRootKey(21))); + assertNotNull(nodes.get(namespaceRootKey(0))); + assertEquals(32, rootHash.length); + } + } + + @Test + public void levelAndRocksStoreSetsProduceTheSameCurrentRoot() throws Exception { + org.junit.Assume.assumeFalse(Arch.isArm64()); + byte[] level = rootFor(manifest("set-level", Engine.LEVELDB)); + byte[] rocks = rootFor(manifest("set-rocks", Engine.ROCKSDB)); + assertArrayEquals(level, rocks); + } + + @Test + public void layerStoreSetIsBoundToMetadataAndCanonicalDirectory() throws Exception { + PathStateStoreManifest manifest = manifest("layer-set", Engine.ROCKSDB); + PathStateRootMetadata layer = PathStateRootMetadata.layer(101, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), bytes(3), bytes(4), bytes(5)); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openLayer(manifest, layer)) { + assertEquals(manifest.getLayerDirectory(101, bytes(1)).resolve("nodes"), + stores.getDirectory()); + } + + PathStateRootMetadata foreign = PathStateRootMetadata.layer(101, bytes(1), bytes(2), 300, + P66Phase.P66_ON, bytes(9), bytes(3), bytes(4), bytes(5)); + assertThrows(java.io.IOException.class, + () -> PathStateNodeStoreSet.openLayer(manifest, foreign)); + } + + @Test + public void immutableLayerMetadataSealsTheWritableNodeSet() throws Exception { + PathStateStoreManifest manifest = manifest("sealed-layer", Engine.ROCKSDB); + PathStateRootMetadata layer = PathStateRootMetadata.layer(101, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), bytes(3), bytes(4), bytes(5)); + Path layerDirectory = manifest.getLayerDirectory(101, bytes(1)); + try (PathStateNodeStoreSet ignored = PathStateNodeStoreSet.openLayer(manifest, layer)) { + assertNotNull(ignored); + } + PathStateMetadataFile.publishImmutable( + layerDirectory.resolve(PathStateCurrentStore.METADATA_FILE), layer); + + assertThrows(java.io.IOException.class, + () -> PathStateNodeStoreSet.openLayer(manifest, layer)); + } + + private byte[] rootFor(PathStateStoreManifest manifest) throws Exception { + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4}))); + return root.rootHash(); + } + } + + private PathStateStoreManifest manifest(String name, Engine engine) throws Exception { + return PathStateStoreManifest.createOrOpen(temporaryFolder.newFolder(name).toPath(), engine); + } + + private static List availableEngines() { + return Arch.isArm64() ? Collections.singletonList(Engine.ROCKSDB) + : Arrays.asList(Engine.LEVELDB, Engine.ROCKSDB); + } + + private static long childDirectoryCount(Path directory) throws Exception { + try (Stream paths = Files.list(directory)) { + return paths.filter(Files::isDirectory).count(); + } + } + + private static byte[] namespaceRootKey(int storeId) { + return java.nio.ByteBuffer.allocate(Integer.BYTES).putInt(storeId).array(); + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } +} From 604b0aeddb66964cf3b489ef6934e13da3600dcd Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 21:56:49 +0800 Subject: [PATCH 049/161] feat(chainbase): batch path state progress --- .../stateroot/PathStateNativeNodeStore.java | 90 ++++++++--- .../db2/stateroot/PathStateNodeStoreSet.java | 150 ++++++++++++++++-- .../PathStateNativeNodeStoreTest.java | 41 ++++- 3 files changed, 239 insertions(+), 42 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java index 27255bf1a10..5105c7094bb 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java @@ -7,7 +7,10 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Objects; import org.iq80.leveldb.DB; import org.iq80.leveldb.WriteOptions; @@ -57,13 +60,23 @@ synchronized byte[] get(byte[] key) { } synchronized void put(byte[] key, byte[] value) { - requireOpen(); - delegate.put(nonEmpty(key, "key"), nonEmpty(value, "value")); + writeBatch(Collections.singletonList(BatchMutation.put(key, value))); } synchronized void delete(byte[] key) { + writeBatch(Collections.singletonList(BatchMutation.delete(key))); + } + + synchronized void writeBatch(List mutations) { requireOpen(); - delegate.delete(nonEmpty(key, "key")); + List owned = new ArrayList<>(Objects.requireNonNull(mutations, "mutations")); + if (owned.isEmpty()) { + throw new IllegalArgumentException("path-state native batch must not be empty"); + } + for (int index = 0; index < owned.size(); index++) { + owned.set(index, new BatchMutation(Objects.requireNonNull(owned.get(index), "mutation"))); + } + delegate.writeBatch(owned); } Path getDirectory() { @@ -100,9 +113,7 @@ private interface Delegate extends Closeable { byte[] get(byte[] key); - void put(byte[] key, byte[] value); - - void delete(byte[] key); + void writeBatch(List mutations); } private static final class LevelDelegate implements Delegate { @@ -121,13 +132,19 @@ public byte[] get(byte[] key) { } @Override - public void put(byte[] key, byte[] value) { - database.put(key, value, syncWrites); - } - - @Override - public void delete(byte[] key) { - database.delete(key, syncWrites); + public void writeBatch(List mutations) { + try (org.iq80.leveldb.WriteBatch batch = database.createWriteBatch()) { + for (BatchMutation mutation : mutations) { + if (mutation.value == null) { + batch.delete(mutation.key); + } else { + batch.put(mutation.key, mutation.value); + } + } + database.write(batch, syncWrites); + } catch (IOException failure) { + throw new IllegalStateException("failed to apply path-state LevelDB node batch", failure); + } } @Override @@ -164,20 +181,18 @@ public byte[] get(byte[] key) { } @Override - public void put(byte[] key, byte[] value) { - try { - database.put(syncWrites, key, value); - } catch (RocksDBException failure) { - throw new IllegalStateException("failed to write path-state RocksDB node", failure); - } - } - - @Override - public void delete(byte[] key) { - try { - database.delete(syncWrites, key); + public void writeBatch(List mutations) { + try (org.rocksdb.WriteBatch batch = new org.rocksdb.WriteBatch()) { + for (BatchMutation mutation : mutations) { + if (mutation.value == null) { + batch.delete(mutation.key); + } else { + batch.put(mutation.key, mutation.value); + } + } + database.write(syncWrites, batch); } catch (RocksDBException failure) { - throw new IllegalStateException("failed to delete path-state RocksDB node", failure); + throw new IllegalStateException("failed to apply path-state RocksDB node batch", failure); } } @@ -188,4 +203,27 @@ public void close() { options.close(); } } + + static final class BatchMutation { + + private final byte[] key; + private final byte[] value; + + private BatchMutation(byte[] key, byte[] value) { + this.key = nonEmpty(key, "key"); + this.value = value == null ? null : nonEmpty(value, "value"); + } + + private BatchMutation(BatchMutation mutation) { + this(mutation.key, mutation.value); + } + + static BatchMutation put(byte[] key, byte[] value) { + return new BatchMutation(key, Objects.requireNonNull(value, "value")); + } + + static BatchMutation delete(byte[] key) { + return new BatchMutation(key, null); + } + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index 571451acf89..3c10178e65e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -16,32 +16,54 @@ public final class PathStateNodeStoreSet implements Closeable { public static final String NODES_DIRECTORY = "nodes"; + private static final byte[] PROGRESS_KEY = new byte[]{ + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + 'p', 'r', 'o', 'g', 'r', 'e', 's', 's'}; private final Path directory; private final PathStateParticipantScope scope; private final Map participantStores = new LinkedHashMap<>(); + private final Map pending = new LinkedHashMap<>(); private final PathStateNativeNodeStore nativeStore; private final PathNodeStore superStore; + private final byte[] manifestDigest; + private final Kind kind; + private final PathStateRootMetadata expectedLayer; + private PathStateRootMetadata progress; + private PathStateRoot root; private boolean rootClaimed; private boolean closed; - private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest) + private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, Kind kind, + PathStateRootMetadata expectedLayer) throws IOException { this.directory = directory.resolve(NODES_DIRECTORY); this.scope = new PathStateCanonicalizer().participantScope(); + this.manifestDigest = manifest.getIdentityDigest(); + this.kind = kind; + this.expectedLayer = expectedLayer; nativeStore = PathStateNativeNodeStore.open(this.directory, manifest.getEngine()); - for (PathStateParticipant participant : scope.getParticipants()) { - participantStores.put(participant.getDbName(), - new NamespacedNodeStore(nativeStore, participant.getStoreId())); + try { + progress = decodeProgress(nativeStore.get(PROGRESS_KEY)); + if (progress != null) { + requireProgressIdentity(progress); + } + for (PathStateParticipant participant : scope.getParticipants()) { + participantStores.put(participant.getDbName(), + new NamespacedNodeStore(this, participant.getStoreId())); + } + superStore = new NamespacedNodeStore(this, 0); + } catch (RuntimeException | IOException failure) { + nativeStore.close(); + throw failure; } - superStore = new NamespacedNodeStore(nativeStore, 0); } public static PathStateNodeStoreSet openBase(PathStateStoreManifest manifest) throws IOException { PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); requireUnsealed(admitted.getBaseDirectory()); - return new PathStateNodeStoreSet(admitted.getBaseDirectory(), admitted); + return new PathStateNodeStoreSet(admitted.getBaseDirectory(), admitted, Kind.BASE, null); } public static PathStateNodeStoreSet openLayer(PathStateStoreManifest manifest, @@ -56,18 +78,54 @@ public static PathStateNodeStoreSet openLayer(PathStateStoreManifest manifest, } Path layerDirectory = admitted.getLayerDirectory(layer.getBlockNumber(), layer.getBlockHash()); requireUnsealed(layerDirectory); - return new PathStateNodeStoreSet(layerDirectory, admitted); + return new PathStateNodeStoreSet(layerDirectory, admitted, Kind.LAYER, layer); } - /** Claims these databases for one in-process trie owner. */ + /** Claims this empty or in-process database for one trie owner. */ public synchronized PathStateRoot createRoot() { requireOpen(); if (rootClaimed) { throw new IllegalStateException("path-state node database set already has a trie owner"); } + if (progress != null) { + throw new IllegalStateException("path-state persisted root requires leaf restoration"); + } rootClaimed = true; - return new PathStateRoot(scope, participant -> participantStores.get(participant.getDbName()), + root = new PathStateRoot(scope, + participant -> participantStores.get(participant.getDbName()), superStore); + return root; + } + + /** Atomically persists all pending path nodes and their exact root progress. */ + public synchronized void commit(PathStateRootMetadata metadata) throws IOException { + requireOpen(); + if (root == null) { + throw new IllegalStateException("path-state node database set has no trie owner"); + } + PathStateRootMetadata next = Objects.requireNonNull(metadata, "metadata"); + requireProgressIdentity(next); + byte[] currentRoot = root.rootHash(); + if (!Arrays.equals(currentRoot, next.getStateRoot())) { + throw new IllegalArgumentException("path-state progress root does not match trie root"); + } + java.util.List mutations = + new java.util.ArrayList<>(pending.size() + 1); + for (Map.Entry entry : pending.entrySet()) { + byte[] value = entry.getValue(); + mutations.add(value == null + ? PathStateNativeNodeStore.BatchMutation.delete(entry.getKey().copy()) + : PathStateNativeNodeStore.BatchMutation.put(entry.getKey().copy(), value)); + } + mutations.add(PathStateNativeNodeStore.BatchMutation.put(PROGRESS_KEY, next.encode())); + nativeStore.writeBatch(mutations); + pending.clear(); + progress = next; + } + + public synchronized PathStateRootMetadata getProgress() { + requireOpen(); + return progress; } public Path getDirectory() { @@ -89,6 +147,44 @@ private void requireOpen() { } } + private synchronized byte[] get(byte[] key) { + BytesKey ownedKey = new BytesKey(key); + if (pending.containsKey(ownedKey)) { + byte[] value = pending.get(ownedKey); + return value == null ? null : Arrays.copyOf(value, value.length); + } + return nativeStore.get(ownedKey.copy()); + } + + private synchronized void put(byte[] key, byte[] value) { + pending.put(new BytesKey(key), Arrays.copyOf(value, value.length)); + } + + private synchronized void delete(byte[] key) { + pending.put(new BytesKey(key), null); + } + + private void requireProgressIdentity(PathStateRootMetadata metadata) throws IOException { + if (metadata.getKind() != kind + || !Arrays.equals(metadata.getFormatDigest(), manifestDigest)) { + throw new IOException("path-state native progress identity mismatch"); + } + if (expectedLayer != null && !Arrays.equals(metadata.encode(), expectedLayer.encode())) { + throw new IOException("path-state native layer progress mismatch"); + } + } + + private static PathStateRootMetadata decodeProgress(byte[] encoded) throws IOException { + if (encoded == null) { + return null; + } + try { + return PathStateRootMetadata.decode(encoded); + } catch (IllegalArgumentException invalid) { + throw new IOException("path-state native progress is corrupt", invalid); + } + } + private static void requireUnsealed(Path directory) throws IOException { Path metadata = directory.resolve(PathStateCurrentStore.METADATA_FILE); if (Files.exists(metadata, LinkOption.NOFOLLOW_LINKS)) { @@ -99,17 +195,17 @@ private static void requireUnsealed(Path directory) throws IOException { private static final class NamespacedNodeStore implements PathNodeStore { - private final PathStateNativeNodeStore nativeStore; + private final PathStateNodeStoreSet owner; private final int storeId; - private NamespacedNodeStore(PathStateNativeNodeStore nativeStore, int storeId) { - this.nativeStore = nativeStore; + private NamespacedNodeStore(PathStateNodeStoreSet owner, int storeId) { + this.owner = owner; this.storeId = storeId; } @Override public byte[] get(byte[] path) { - return nativeStore.get(key(path)); + return owner.get(key(path)); } @Override @@ -119,12 +215,12 @@ public void put(byte[] path, byte[] encodedNode) { if (value.length == 0) { throw new IllegalArgumentException("encodedNode must not be empty"); } - nativeStore.put(key(path), value); + owner.put(key(path), value); } @Override public void delete(byte[] path) { - nativeStore.delete(key(path)); + owner.delete(key(path)); } private byte[] key(byte[] path) { @@ -140,4 +236,28 @@ private byte[] key(byte[] path) { .array(); } } + + private static final class BytesKey { + + private final byte[] bytes; + + private BytesKey(byte[] bytes) { + this.bytes = Arrays.copyOf(Objects.requireNonNull(bytes, "bytes"), bytes.length); + } + + private byte[] copy() { + return Arrays.copyOf(bytes, bytes.length); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof BytesKey + && Arrays.equals(bytes, ((BytesKey) other).bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + } } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index 24b924ee0b3..9327e6d5465 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -64,11 +64,16 @@ public void nativeStoreRejectsInvalidEntriesAndUseAfterClose() throws Exception public void baseStoreSetCreatesExact27PlusSuperAndPersistsRootNodes() throws Exception { PathStateStoreManifest manifest = manifest("base-set", Engine.ROCKSDB); byte[] rootHash; + PathStateRootMetadata progress; try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { PathStateRoot root = stores.createRoot(); root.apply(Collections.singletonList( PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}))); rootHash = root.rootHash(); + progress = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), rootHash, bytes(3)); + stores.commit(progress); + assertArrayEquals(progress.encode(), stores.getProgress().encode()); root.verifyNodeStores(); assertThrows(IllegalStateException.class, stores::createRoot); } @@ -80,6 +85,31 @@ public void baseStoreSetCreatesExact27PlusSuperAndPersistsRootNodes() throws Exc assertNotNull(nodes.get(namespaceRootKey(0))); assertEquals(32, rootHash.length); } + try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openBase(manifest)) { + assertArrayEquals(progress.encode(), reopened.getProgress().encode()); + assertThrows(IllegalStateException.class, reopened::createRoot); + } + } + + @Test + public void rejectedProgressDoesNotFlushPendingNodes() throws Exception { + PathStateStoreManifest manifest = manifest("rejected-progress", Engine.ROCKSDB); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}))); + root.rootHash(); + PathStateRootMetadata mismatch = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), bytes(9), bytes(3)); + assertThrows(IllegalArgumentException.class, () -> stores.commit(mismatch)); + assertNull(stores.getProgress()); + } + + try (PathStateNativeNodeStore nodes = PathStateNativeNodeStore.open( + manifest.getBaseDirectory().resolve("nodes"), Engine.ROCKSDB)) { + assertNull(nodes.get(namespaceRootKey(21))); + assertNull(nodes.get(namespaceRootKey(0))); + } } @Test @@ -123,13 +153,22 @@ public void immutableLayerMetadataSealsTheWritableNodeSet() throws Exception { } private byte[] rootFor(PathStateStoreManifest manifest) throws Exception { + byte[] stateRoot; + PathStateRootMetadata progress; try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { PathStateRoot root = stores.createRoot(); root.apply(Arrays.asList( PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}), PathStateMutation.put("account", new byte[]{3}, new byte[]{4}))); - return root.rootHash(); + stateRoot = root.rootHash(); + progress = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), stateRoot, bytes(3)); + stores.commit(progress); + } + try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openBase(manifest)) { + assertArrayEquals(progress.encode(), reopened.getProgress().encode()); } + return stateRoot; } private PathStateStoreManifest manifest(String name, Engine engine) throws Exception { From ab5a6da9769d962999480eaa729c94695c7ede46 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 22:24:30 +0800 Subject: [PATCH 050/161] feat(chainbase): recover path state base --- .../stateroot/PathStateBasePublication.java | 136 +++++++++++++++ .../db2/stateroot/PathStateMetadataFile.java | 8 + .../db2/stateroot/PathStateNodeStoreSet.java | 15 ++ .../PathStateBasePublicationTest.java | 158 ++++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBasePublication.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateBasePublicationTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBasePublication.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBasePublication.java new file mode 100644 index 00000000000..6c5fb06abb6 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBasePublication.java @@ -0,0 +1,136 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; + +/** Crash-recoverable publication boundary for the first durable path-state BASE. */ +public final class PathStateBasePublication { + + public static final String INTENT_FILE = "INTENT"; + + private final PathStateStoreManifest manifest; + private final PathStateCurrentStore currentStore; + private final Path baseDirectory; + private final Path intentPath; + private final Path metadataPath; + private final FaultHook faultHook; + + public PathStateBasePublication(PathStateStoreManifest manifest) { + this(manifest, stage -> { }); + } + + PathStateBasePublication(PathStateStoreManifest manifest, FaultHook faultHook) { + this.manifest = Objects.requireNonNull(manifest, "manifest"); + this.currentStore = new PathStateCurrentStore(manifest); + this.baseDirectory = manifest.getBaseDirectory(); + this.intentPath = baseDirectory.resolve(INTENT_FILE); + this.metadataPath = baseDirectory.resolve(PathStateCurrentStore.METADATA_FILE); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + /** Publishes a first BASE through intent, native marker, metadata, CURRENT, and retire stages. */ + public synchronized PathStateRootMetadata publish(PathStateNodeStoreSet stores, + PathStateRootMetadata metadata) throws IOException { + PathStateNodeStoreSet nodeStores = Objects.requireNonNull(stores, "stores"); + Path expectedNodes = baseDirectory.resolve(PathStateNodeStoreSet.NODES_DIRECTORY) + .toAbsolutePath().normalize(); + if (!expectedNodes.equals(nodeStores.getDirectory().toAbsolutePath().normalize())) { + throw new IllegalArgumentException("path-state BASE node database directory mismatch"); + } + PathStateRootMetadata base = requireBase(metadata); + if (currentStore.isInitialized()) { + throw new IOException("path-state initial BASE is already published"); + } + PathStateMetadataFile.publishImmutable(intentPath, base); + faultHook.after(Stage.AFTER_INTENT); + nodeStores.commit(base); + faultHook.after(Stage.AFTER_NODE_PROGRESS); + PathStateMetadataFile.publishImmutable(metadataPath, base); + faultHook.after(Stage.AFTER_METADATA); + PathStateRootMetadata current = currentStore.publishBase(base); + faultHook.after(Stage.AFTER_CURRENT); + PathStateMetadataFile.deleteDurable(intentPath); + faultHook.after(Stage.AFTER_RETIRE); + return current; + } + + /** Reconciles one interrupted first-BASE publication and returns the durable action taken. */ + public synchronized RecoveryAction recover() throws IOException { + if (!Files.exists(intentPath, LinkOption.NOFOLLOW_LINKS)) { + verifySettledState(); + return RecoveryAction.NONE; + } + PathStateRootMetadata intent = requireBase(PathStateMetadataFile.load(intentPath)); + PathStateRootMetadata progress = PathStateNodeStoreSet.loadProgress(baseDirectory, manifest); + if (progress == null) { + if (Files.exists(metadataPath, LinkOption.NOFOLLOW_LINKS) || currentStore.isInitialized()) { + throw new IOException("path-state BASE authority exists without native progress"); + } + PathStateMetadataFile.deleteDurable(intentPath); + return RecoveryAction.ROLLED_BACK_INTENT; + } + requireSame(intent, progress, "path-state BASE intent and native progress differ"); + currentStore.publishBase(intent); + PathStateMetadataFile.deleteDurable(intentPath); + verifySettledState(); + return RecoveryAction.COMPLETED_PUBLICATION; + } + + private void verifySettledState() throws IOException { + PathStateRootMetadata progress = PathStateNodeStoreSet.loadProgress(baseDirectory, manifest); + boolean metadataExists = Files.exists(metadataPath, LinkOption.NOFOLLOW_LINKS); + if (!currentStore.isInitialized()) { + if (metadataExists || progress != null) { + throw new IOException("path-state BASE has orphaned authority without CURRENT"); + } + return; + } + currentStore.current(); + PathStateRootMetadata metadata = requireBase(PathStateMetadataFile.load(metadataPath)); + if (progress == null) { + throw new IOException("path-state BASE native progress is missing"); + } + requireSame(metadata, progress, "path-state BASE metadata and native progress differ"); + } + + private PathStateRootMetadata requireBase(PathStateRootMetadata metadata) throws IOException { + PathStateRootMetadata base = Objects.requireNonNull(metadata, "metadata"); + if (base.getKind() != Kind.BASE + || !Arrays.equals(base.getFormatDigest(), manifest.getIdentityDigest())) { + throw new IOException("path-state BASE publication identity mismatch"); + } + return base; + } + + private static void requireSame(PathStateRootMetadata expected, + PathStateRootMetadata actual, String error) throws IOException { + if (!Arrays.equals(expected.encode(), actual.encode())) { + throw new IOException(error); + } + } + + public enum RecoveryAction { + NONE, + ROLLED_BACK_INTENT, + COMPLETED_PUBLICATION + } + + enum Stage { + AFTER_INTENT, + AFTER_NODE_PROGRESS, + AFTER_METADATA, + AFTER_CURRENT, + AFTER_RETIRE + } + + @FunctionalInterface + interface FaultHook { + + void after(Stage stage) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java index 25d8c7a0baa..3dd715f82e8 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java @@ -57,6 +57,14 @@ static void requireExact(Path path, PathStateRootMetadata metadata) throws IOExc requireExact(path, Objects.requireNonNull(metadata, "metadata").encode()); } + static void deleteDurable(Path path) throws IOException { + Path target = Objects.requireNonNull(path, "path"); + Path directory = Objects.requireNonNull(target.getParent(), "metadata directory"); + if (Files.deleteIfExists(target)) { + syncDirectory(directory); + } + } + private static void requireExact(Path path, byte[] expected) throws IOException { if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { throw new IOException("path-state metadata is not a regular file: " + path); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index 3c10178e65e..f74e06da8dd 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -128,6 +128,21 @@ public synchronized PathStateRootMetadata getProgress() { return progress; } + static PathStateRootMetadata loadProgress(Path ownerDirectory, + PathStateStoreManifest manifest) throws IOException { + Path nodes = Objects.requireNonNull(ownerDirectory, "ownerDirectory").resolve(NODES_DIRECTORY); + if (!Files.exists(nodes, LinkOption.NOFOLLOW_LINKS)) { + return null; + } + if (!Files.isDirectory(nodes, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(nodes)) { + throw new IOException("path-state node database is not a direct directory: " + nodes); + } + try (PathStateNativeNodeStore store = PathStateNativeNodeStore.open(nodes, + Objects.requireNonNull(manifest, "manifest").getEngine())) { + return decodeProgress(store.get(PROGRESS_KEY)); + } + } + public Path getDirectory() { return directory; } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBasePublicationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBasePublicationTest.java new file mode 100644 index 00000000000..6f845f1674c --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBasePublicationTest.java @@ -0,0 +1,158 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.arch.Arch; +import org.tron.core.db2.stateroot.PathStateBasePublication.RecoveryAction; +import org.tron.core.db2.stateroot.PathStateBasePublication.Stage; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateBasePublicationTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void faultsBeforeNativeMarkerRollBackIntentIdempotently() throws Exception { + for (Engine engine : availableEngines()) { + Fixture fixture = fixture("before-marker-" + engine, engine, Stage.AFTER_INTENT); + assertThrows(IOException.class, fixture::publish); + fixture.close(); + + PathStateBasePublication recovery = new PathStateBasePublication(fixture.manifest); + org.junit.Assert.assertEquals(RecoveryAction.ROLLED_BACK_INTENT, recovery.recover()); + org.junit.Assert.assertEquals(RecoveryAction.NONE, recovery.recover()); + assertFalse(new PathStateCurrentStore(fixture.manifest).isInitialized()); + assertNull(PathStateNodeStoreSet.loadProgress(fixture.manifest.getBaseDirectory(), + fixture.manifest)); + } + } + + @Test + public void faultsAfterNativeMarkerCompletePublicationIdempotently() throws Exception { + for (Engine engine : availableEngines()) { + for (Stage stage : new Stage[]{Stage.AFTER_NODE_PROGRESS, Stage.AFTER_METADATA, + Stage.AFTER_CURRENT}) { + Fixture fixture = fixture("complete-" + engine + "-" + stage, engine, stage); + assertThrows(IOException.class, fixture::publish); + fixture.close(); + + PathStateBasePublication recovery = new PathStateBasePublication(fixture.manifest); + org.junit.Assert.assertEquals(RecoveryAction.COMPLETED_PUBLICATION, recovery.recover()); + org.junit.Assert.assertEquals(RecoveryAction.NONE, recovery.recover()); + assertSettled(fixture); + } + } + } + + @Test + public void faultAfterRetireIsAlreadySettled() throws Exception { + Fixture fixture = fixture("after-retire", Engine.ROCKSDB, Stage.AFTER_RETIRE); + assertThrows(IOException.class, fixture::publish); + fixture.close(); + + PathStateBasePublication recovery = new PathStateBasePublication(fixture.manifest); + org.junit.Assert.assertEquals(RecoveryAction.NONE, recovery.recover()); + assertSettled(fixture); + } + + @Test + public void recoveryRejectsNativeProgressWithoutIntentOrAuthority() throws Exception { + Fixture fixture = fixture("orphan-progress", Engine.ROCKSDB, Stage.AFTER_NODE_PROGRESS); + assertThrows(IOException.class, fixture::publish); + fixture.close(); + Files.delete(fixture.manifest.getBaseDirectory().resolve(PathStateBasePublication.INTENT_FILE)); + + assertThrows(IOException.class, new PathStateBasePublication(fixture.manifest)::recover); + } + + @Test + public void publicationRejectsNodeSetFromAnotherDirectoryBeforeIntent() throws Exception { + Fixture source = fixture("directory-source", Engine.ROCKSDB, null); + PathStateStoreManifest target = PathStateStoreManifest.createOrOpen( + temporaryFolder.newFolder("directory-target").toPath(), Engine.ROCKSDB); + PathStateRootMetadata targetMetadata = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, target.getIdentityDigest(), source.metadata.getStateRoot(), bytes(3)); + + assertThrows(IllegalArgumentException.class, + () -> new PathStateBasePublication(target).publish(source.stores, targetMetadata)); + assertFalse(Files.exists( + target.getBaseDirectory().resolve(PathStateBasePublication.INTENT_FILE))); + source.close(); + } + + private Fixture fixture(String name, Engine engine, Stage failure) throws Exception { + Path root = new File(temporaryFolder.getRoot(), name).toPath(); + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen(root, engine); + PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest); + PathStateRoot stateRoot = stores.createRoot(); + stateRoot.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}))); + PathStateRootMetadata metadata = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), stateRoot.rootHash(), bytes(3)); + PathStateBasePublication publication = new PathStateBasePublication(manifest, stage -> { + if (stage == failure) { + throw new IOException("injected after " + stage); + } + }); + return new Fixture(manifest, stores, metadata, publication); + } + + private static void assertSettled(Fixture fixture) throws Exception { + PathStateRootMetadata current = new PathStateCurrentStore(fixture.manifest).current(); + assertArrayEquals(fixture.metadata.encode(), current.encode()); + assertArrayEquals(fixture.metadata.encode(), PathStateNodeStoreSet.loadProgress( + fixture.manifest.getBaseDirectory(), fixture.manifest).encode()); + assertFalse(Files.exists( + fixture.manifest.getBaseDirectory().resolve(PathStateBasePublication.INTENT_FILE))); + } + + private static Engine[] availableEngines() { + return Arch.isArm64() ? new Engine[]{Engine.ROCKSDB} + : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + private static final class Fixture { + + private final PathStateStoreManifest manifest; + private final PathStateNodeStoreSet stores; + private final PathStateRootMetadata metadata; + private final PathStateBasePublication publication; + + private Fixture(PathStateStoreManifest manifest, PathStateNodeStoreSet stores, + PathStateRootMetadata metadata, PathStateBasePublication publication) { + this.manifest = manifest; + this.stores = stores; + this.metadata = metadata; + this.publication = publication; + } + + private void publish() throws IOException { + publication.publish(stores, metadata); + } + + private void close() throws IOException { + stores.close(); + } + } +} From 472d30233802a31f304d62bf453bf96aeb920e61 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 23:12:27 +0800 Subject: [PATCH 051/161] feat(chainbase): restore durable path leaves --- .../core/db2/stateroot/PathMerkleTrie.java | 52 +++++++++ .../stateroot/PathStateNativeNodeStore.java | 66 +++++++++++ .../db2/stateroot/PathStateNodeStoreSet.java | 110 +++++++++++++++--- .../core/db2/stateroot/PathStateRoot.java | 74 ++++++++++++ .../PathStateBasePublicationTest.java | 5 + .../PathStateNativeNodeStoreTest.java | 67 +++++++++++ 6 files changed, 360 insertions(+), 14 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index 0312571ba31..c4ecfc4f0ee 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; @@ -78,6 +79,36 @@ public synchronized int size() { return leaves.size(); } + synchronized List leafEntries() { + List entries = new ArrayList<>(leaves.size()); + for (Map.Entry entry : leaves.entrySet()) { + entries.add(new LeafEntry(entry.getKey().copy(), entry.getValue())); + } + return entries; + } + + /** Restores current leaves and verifies their complete path-node set without repairing it. */ + synchronized void restoreLeaves(Collection entries) { + if (!leaves.isEmpty() || !committedPaths.isEmpty() || dirty) { + throw new IllegalStateException("path trie is not empty before leaf restoration"); + } + for (LeafEntry entry : Objects.requireNonNull(entries, "entries")) { + LeafEntry present = Objects.requireNonNull(entry, "entry"); + if (leaves.put(secureKey(present.secureKey), + nonEmpty(present.encodedValue, "encodedValue")) != null) { + throw new IllegalArgumentException("duplicate restored path-state leaf"); + } + } + Map expectedNodes = buildCurrentNodes(); + for (Map.Entry entry : expectedNodes.entrySet()) { + if (!Arrays.equals(nodeStore.get(entry.getKey().bytes), entry.getValue())) { + throw new IllegalStateException("restored leaves do not match persisted path nodes"); + } + } + committedPaths.addAll(expectedNodes.keySet()); + rootHash = rootHash(expectedNodes); + } + /** Verifies every path owned by the current committed node set without repairing corruption. */ public synchronized void verifyNodeStore() { if (dirty) { @@ -302,6 +333,27 @@ private Leaf(byte[] nibbles, byte[] value) { } } + static final class LeafEntry { + + private final byte[] secureKey; + private final byte[] encodedValue; + + LeafEntry(byte[] secureKey, byte[] encodedValue) { + this.secureKey = Arrays.copyOf(Objects.requireNonNull(secureKey, "secureKey"), + secureKey.length); + this.encodedValue = Arrays.copyOf(Objects.requireNonNull(encodedValue, "encodedValue"), + encodedValue.length); + } + + byte[] getSecureKey() { + return Arrays.copyOf(secureKey, secureKey.length); + } + + byte[] getEncodedValue() { + return Arrays.copyOf(encodedValue, encodedValue.length); + } + } + private static final class BytesKey { private final byte[] bytes; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java index 5105c7094bb..d1f112a5d3b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java @@ -11,6 +11,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import org.iq80.leveldb.DB; import org.iq80.leveldb.WriteOptions; @@ -79,6 +80,11 @@ synchronized void writeBatch(List mutations) { delegate.writeBatch(owned); } + synchronized List scanPrefix(byte[] prefix) { + requireOpen(); + return delegate.scanPrefix(nonEmpty(prefix, "prefix")); + } + Path getDirectory() { return directory; } @@ -114,6 +120,8 @@ private interface Delegate extends Closeable { byte[] get(byte[] key); void writeBatch(List mutations); + + List scanPrefix(byte[] prefix); } private static final class LevelDelegate implements Delegate { @@ -147,6 +155,24 @@ public void writeBatch(List mutations) { } } + @Override + public List scanPrefix(byte[] prefix) { + List entries = new ArrayList<>(); + try (org.iq80.leveldb.DBIterator iterator = database.iterator()) { + iterator.seek(prefix); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (!startsWith(entry.getKey(), prefix)) { + break; + } + entries.add(new KeyValue(entry.getKey(), entry.getValue())); + } + } catch (IOException failure) { + throw new IllegalStateException("failed to scan path-state LevelDB nodes", failure); + } + return entries; + } + @Override public void close() throws IOException { database.close(); @@ -196,6 +222,22 @@ public void writeBatch(List mutations) { } } + @Override + public List scanPrefix(byte[] prefix) { + List entries = new ArrayList<>(); + try (org.rocksdb.RocksIterator iterator = database.newIterator()) { + iterator.seek(prefix); + while (iterator.isValid() && startsWith(iterator.key(), prefix)) { + entries.add(new KeyValue(iterator.key(), iterator.value())); + iterator.next(); + } + iterator.status(); + } catch (RocksDBException failure) { + throw new IllegalStateException("failed to scan path-state RocksDB nodes", failure); + } + return entries; + } + @Override public void close() { syncWrites.close(); @@ -226,4 +268,28 @@ static BatchMutation delete(byte[] key) { return new BatchMutation(key, null); } } + + static final class KeyValue { + + private final byte[] key; + private final byte[] value; + + private KeyValue(byte[] key, byte[] value) { + this.key = nonEmpty(key, "key"); + this.value = nonEmpty(value, "value"); + } + + byte[] getKey() { + return Arrays.copyOf(key, key.length); + } + + byte[] getValue() { + return Arrays.copyOf(value, value.length); + } + } + + private static boolean startsWith(byte[] value, byte[] prefix) { + return value.length >= prefix.length + && Arrays.equals(Arrays.copyOf(value, prefix.length), prefix); + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index f74e06da8dd..5041aaca0fd 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -6,8 +6,10 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; @@ -19,34 +21,45 @@ public final class PathStateNodeStoreSet implements Closeable { private static final byte[] PROGRESS_KEY = new byte[]{ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 'p', 'r', 'o', 'g', 'r', 'e', 's', 's'}; + private static final int LEAF_DOMAIN = -2; + private static final byte[] LEAF_PREFIX = ByteBuffer.allocate(Integer.BYTES) + .putInt(LEAF_DOMAIN).array(); + private static final int LEAF_KEY_LENGTH = Integer.BYTES * 2 + PathMerkleTrie.SECURE_KEY_LENGTH; private final Path directory; private final PathStateParticipantScope scope; private final Map participantStores = new LinkedHashMap<>(); private final Map pending = new LinkedHashMap<>(); + private final Map persistedLeaves = new LinkedHashMap<>(); private final PathStateNativeNodeStore nativeStore; private final PathNodeStore superStore; private final byte[] manifestDigest; private final Kind kind; - private final PathStateRootMetadata expectedLayer; + private final PathStateRootMetadata expectedMetadata; private PathStateRootMetadata progress; private PathStateRoot root; private boolean rootClaimed; private boolean closed; private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, Kind kind, - PathStateRootMetadata expectedLayer) + PathStateRootMetadata expectedMetadata) throws IOException { this.directory = directory.resolve(NODES_DIRECTORY); this.scope = new PathStateCanonicalizer().participantScope(); this.manifestDigest = manifest.getIdentityDigest(); this.kind = kind; - this.expectedLayer = expectedLayer; + this.expectedMetadata = expectedMetadata; nativeStore = PathStateNativeNodeStore.open(this.directory, manifest.getEngine()); try { progress = decodeProgress(nativeStore.get(PROGRESS_KEY)); if (progress != null) { requireProgressIdentity(progress); + } else if (kind == Kind.BASE && expectedMetadata != null) { + throw new IOException("path-state BASE metadata exists without native progress"); + } + loadPersistedLeaves(); + if (progress == null && !persistedLeaves.isEmpty()) { + throw new IOException("path-state leaf inventory exists without native progress"); } for (PathStateParticipant participant : scope.getParticipants()) { participantStores.put(participant.getDbName(), @@ -62,8 +75,10 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K public static PathStateNodeStoreSet openBase(PathStateStoreManifest manifest) throws IOException { PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); - requireUnsealed(admitted.getBaseDirectory()); - return new PathStateNodeStoreSet(admitted.getBaseDirectory(), admitted, Kind.BASE, null); + Path metadataPath = admitted.getBaseDirectory().resolve(PathStateCurrentStore.METADATA_FILE); + PathStateRootMetadata metadata = Files.exists(metadataPath, LinkOption.NOFOLLOW_LINKS) + ? PathStateMetadataFile.load(metadataPath) : null; + return new PathStateNodeStoreSet(admitted.getBaseDirectory(), admitted, Kind.BASE, metadata); } public static PathStateNodeStoreSet openLayer(PathStateStoreManifest manifest, @@ -81,19 +96,23 @@ public static PathStateNodeStoreSet openLayer(PathStateStoreManifest manifest, return new PathStateNodeStoreSet(layerDirectory, admitted, Kind.LAYER, layer); } - /** Claims this empty or in-process database for one trie owner. */ + /** Claims this database and restores its durable leaves when progress already exists. */ public synchronized PathStateRoot createRoot() { requireOpen(); if (rootClaimed) { throw new IllegalStateException("path-state node database set already has a trie owner"); } + PathStateRoot candidate = new PathStateRoot(scope, + participant -> participantStores.get(participant.getDbName()), + superStore); if (progress != null) { - throw new IllegalStateException("path-state persisted root requires leaf restoration"); + candidate.restoreLeaves(restoredLeafRecords(), progress.getStateRoot()); + if (!pending.isEmpty()) { + throw new IllegalStateException("path-state leaf restoration attempted to repair nodes"); + } } + root = candidate; rootClaimed = true; - root = new PathStateRoot(scope, - participant -> participantStores.get(participant.getDbName()), - superStore); return root; } @@ -109,17 +128,31 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti if (!Arrays.equals(currentRoot, next.getStateRoot())) { throw new IllegalArgumentException("path-state progress root does not match trie root"); } - java.util.List mutations = - new java.util.ArrayList<>(pending.size() + 1); + List mutations = + new ArrayList<>(pending.size() + persistedLeaves.size() + 1); for (Map.Entry entry : pending.entrySet()) { byte[] value = entry.getValue(); mutations.add(value == null ? PathStateNativeNodeStore.BatchMutation.delete(entry.getKey().copy()) : PathStateNativeNodeStore.BatchMutation.put(entry.getKey().copy(), value)); } + Map nextLeaves = leafMap(root.leafRecords()); + for (BytesKey persisted : persistedLeaves.keySet()) { + if (!nextLeaves.containsKey(persisted)) { + mutations.add(PathStateNativeNodeStore.BatchMutation.delete(persisted.copy())); + } + } + for (Map.Entry entry : nextLeaves.entrySet()) { + if (!Arrays.equals(persistedLeaves.get(entry.getKey()), entry.getValue())) { + mutations.add(PathStateNativeNodeStore.BatchMutation.put( + entry.getKey().copy(), entry.getValue())); + } + } mutations.add(PathStateNativeNodeStore.BatchMutation.put(PROGRESS_KEY, next.encode())); nativeStore.writeBatch(mutations); pending.clear(); + persistedLeaves.clear(); + persistedLeaves.putAll(nextLeaves); progress = next; } @@ -184,9 +217,58 @@ private void requireProgressIdentity(PathStateRootMetadata metadata) throws IOEx || !Arrays.equals(metadata.getFormatDigest(), manifestDigest)) { throw new IOException("path-state native progress identity mismatch"); } - if (expectedLayer != null && !Arrays.equals(metadata.encode(), expectedLayer.encode())) { - throw new IOException("path-state native layer progress mismatch"); + if (expectedMetadata != null + && !Arrays.equals(metadata.encode(), expectedMetadata.encode())) { + throw new IOException("path-state native progress differs from immutable metadata"); + } + } + + private void loadPersistedLeaves() throws IOException { + for (PathStateNativeNodeStore.KeyValue entry : nativeStore.scanPrefix(LEAF_PREFIX)) { + byte[] key = entry.getKey(); + if (key.length != LEAF_KEY_LENGTH || ByteBuffer.wrap(key).getInt() != LEAF_DOMAIN) { + throw new IOException("path-state durable leaf key is malformed"); + } + int storeId = ByteBuffer.wrap(key, Integer.BYTES, Integer.BYTES).getInt(); + requireParticipant(storeId); + persistedLeaves.put(new BytesKey(key), entry.getValue()); + } + } + + private List restoredLeafRecords() { + List records = new ArrayList<>(persistedLeaves.size()); + for (Map.Entry entry : persistedLeaves.entrySet()) { + byte[] key = entry.getKey().copy(); + int storeId = ByteBuffer.wrap(key, Integer.BYTES, Integer.BYTES).getInt(); + byte[] secureKey = Arrays.copyOfRange(key, Integer.BYTES * 2, key.length); + records.add(new PathStateRoot.LeafRecord(storeId, secureKey, entry.getValue())); + } + return records; + } + + private Map leafMap(List records) { + Map leaves = new LinkedHashMap<>(); + for (PathStateRoot.LeafRecord record : records) { + requireParticipant(record.getStoreId()); + BytesKey key = new BytesKey(ByteBuffer.allocate(LEAF_KEY_LENGTH) + .putInt(LEAF_DOMAIN) + .putInt(record.getStoreId()) + .put(record.getSecureKey()) + .array()); + if (leaves.put(key, record.getEncodedValue()) != null) { + throw new IllegalStateException("duplicate path-state durable leaf key"); + } + } + return leaves; + } + + private PathStateParticipant requireParticipant(int storeId) { + for (PathStateParticipant participant : scope.getParticipants()) { + if (participant.getStoreId() == storeId) { + return participant; + } } + throw new IllegalArgumentException("unknown path-state durable leaf Store ID: " + storeId); } private static PathStateRootMetadata decodeProgress(byte[] encoded) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index 52ac76d8caa..0f8e1ea7a0c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -105,6 +105,53 @@ public synchronized void verifyNodeStores() { superTrie.verifyNodeStore(); } + synchronized List leafRecords() { + List records = new ArrayList<>(); + for (PathStateParticipant participant : scope.getParticipants()) { + for (PathMerkleTrie.LeafEntry entry + : participantTries.get(participant.getDbName()).leafEntries()) { + records.add(new LeafRecord(participant.getStoreId(), entry.getSecureKey(), + entry.getEncodedValue())); + } + } + return records; + } + + synchronized void restoreLeaves(Collection records, byte[] expectedRoot) { + Map participants = new LinkedHashMap<>(); + Map> leaves = new LinkedHashMap<>(); + for (PathStateParticipant participant : scope.getParticipants()) { + participants.put(participant.getStoreId(), participant); + leaves.put(participant.getStoreId(), new ArrayList<>()); + } + for (LeafRecord record : Objects.requireNonNull(records, "records")) { + LeafRecord present = Objects.requireNonNull(record, "record"); + if (!participants.containsKey(present.storeId)) { + throw new IllegalArgumentException("restored leaf has unknown path-state Store ID"); + } + leaves.get(present.storeId).add(new PathMerkleTrie.LeafEntry( + present.secureKey, present.encodedValue)); + } + + List superLeaves = new ArrayList<>(); + for (PathStateParticipant participant : scope.getParticipants()) { + PathMerkleTrie trie = participantTries.get(participant.getDbName()); + trie.restoreLeaves(leaves.get(participant.getStoreId())); + byte[] storeRoot = trie.rootHash(); + superLeaves.add(new PathMerkleTrie.LeafEntry( + PathStateCommitmentCodec.superLeafKey(participant.getStoreId()), + PathStateCommitmentCodec.superLeafValue(participant.getStoreId(), + participant.getDbName(), participant.getStoreFormatVersion(), storeRoot))); + } + superTrie.restoreLeaves(superLeaves); + byte[] restoredRoot = superTrie.rootHash(); + if (!Arrays.equals(restoredRoot, Objects.requireNonNull(expectedRoot, "expectedRoot"))) { + throw new IllegalStateException("restored path-state root differs from durable progress"); + } + rootMaterialized = true; + verifyNodeStores(); + } + private List prepare(Collection mutations) { List supplied = new ArrayList<>( Objects.requireNonNull(mutations, "mutations")); @@ -145,6 +192,33 @@ public interface PathNodeStoreFactory { PathNodeStore open(PathStateParticipant participant); } + static final class LeafRecord { + + private final int storeId; + private final byte[] secureKey; + private final byte[] encodedValue; + + LeafRecord(int storeId, byte[] secureKey, byte[] encodedValue) { + this.storeId = storeId; + this.secureKey = Arrays.copyOf(Objects.requireNonNull(secureKey, "secureKey"), + secureKey.length); + this.encodedValue = Arrays.copyOf(Objects.requireNonNull(encodedValue, "encodedValue"), + encodedValue.length); + } + + int getStoreId() { + return storeId; + } + + byte[] getSecureKey() { + return Arrays.copyOf(secureKey, secureKey.length); + } + + byte[] getEncodedValue() { + return Arrays.copyOf(encodedValue, encodedValue.length); + } + } + private static final class PreparedMutation { private final PathStateParticipant participant; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBasePublicationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBasePublicationTest.java index 6f845f1674c..024380e3cc9 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBasePublicationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBasePublicationTest.java @@ -117,6 +117,11 @@ private static void assertSettled(Fixture fixture) throws Exception { fixture.manifest.getBaseDirectory(), fixture.manifest).encode()); assertFalse(Files.exists( fixture.manifest.getBaseDirectory().resolve(PathStateBasePublication.INTENT_FILE))); + try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openBase(fixture.manifest)) { + PathStateRoot restored = reopened.createRoot(); + assertArrayEquals(fixture.metadata.getStateRoot(), restored.rootHash()); + restored.verifyNodeStores(); + } } private static Engine[] availableEngines() { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index 9327e6d5465..627af275a92 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -87,10 +87,68 @@ public void baseStoreSetCreatesExact27PlusSuperAndPersistsRootNodes() throws Exc } try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openBase(manifest)) { assertArrayEquals(progress.encode(), reopened.getProgress().encode()); + PathStateRoot restored = reopened.createRoot(); + assertArrayEquals(rootHash, restored.rootHash()); + restored.verifyNodeStores(); + } + } + + @Test + public void missingDurableLeafFailsClosedDuringRootRestore() throws Exception { + PathStateStoreManifest manifest = manifest("missing-leaf", Engine.ROCKSDB); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}))); + byte[] stateRoot = root.rootHash(); + stores.commit(PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), stateRoot, bytes(3))); + } + try (PathStateNativeNodeStore nodes = PathStateNativeNodeStore.open( + manifest.getBaseDirectory().resolve("nodes"), Engine.ROCKSDB)) { + nodes.delete(durableLeafKey(21, + PathStateCommitmentCodec.storeLeafKey(21, new byte[]{1}))); + } + try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openBase(manifest)) { assertThrows(IllegalStateException.class, reopened::createRoot); } } + @Test + public void restoredBaseCommitsLeafUpdatesAndDeletesAcrossSecondReopen() throws Exception { + for (Engine engine : availableEngines()) { + PathStateStoreManifest manifest = manifest("leaf-delta-" + engine, engine); + byte[] firstRoot; + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4}))); + firstRoot = root.rootHash(); + stores.commit(PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), firstRoot, bytes(3))); + } + + byte[] secondRoot; + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + assertArrayEquals(firstRoot, root.rootHash()); + root.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5}), + PathStateMutation.delete("account", new byte[]{3}))); + secondRoot = root.rootHash(); + stores.commit(PathStateRootMetadata.base(101, bytes(4), bytes(1), 303, + P66Phase.P66_ON, manifest.getIdentityDigest(), secondRoot, bytes(6))); + } + + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + assertArrayEquals(secondRoot, root.rootHash()); + root.verifyNodeStores(); + } + } + } + @Test public void rejectedProgressDoesNotFlushPendingNodes() throws Exception { PathStateStoreManifest manifest = manifest("rejected-progress", Engine.ROCKSDB); @@ -167,6 +225,7 @@ private byte[] rootFor(PathStateStoreManifest manifest) throws Exception { } try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openBase(manifest)) { assertArrayEquals(progress.encode(), reopened.getProgress().encode()); + assertArrayEquals(stateRoot, reopened.createRoot().rootHash()); } return stateRoot; } @@ -190,6 +249,14 @@ private static byte[] namespaceRootKey(int storeId) { return java.nio.ByteBuffer.allocate(Integer.BYTES).putInt(storeId).array(); } + private static byte[] durableLeafKey(int storeId, byte[] secureKey) { + return java.nio.ByteBuffer.allocate(Integer.BYTES * 2 + secureKey.length) + .putInt(-2) + .putInt(storeId) + .put(secureKey) + .array(); + } + private static byte[] bytes(int seed) { byte[] value = new byte[32]; for (int index = 0; index < value.length; index++) { From 6d66e8a549e0728a0041123c0ecbfee501441708 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 23:24:20 +0800 Subject: [PATCH 052/161] feat(chainbase): inherit path state layers --- .../core/db2/stateroot/PathMerkleTrie.java | 31 ++-- .../core/db2/stateroot/PathStateLayer.java | 133 +++++++++++++++ .../db2/stateroot/PathStateNodeStoreSet.java | 66 +++++++ .../core/db2/stateroot/PathStateRoot.java | 21 ++- .../db2/stateroot/PathStateLayerTest.java | 161 ++++++++++++++++++ 5 files changed, 400 insertions(+), 12 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index c4ecfc4f0ee..57d2ec9bc85 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -87,18 +87,16 @@ synchronized List leafEntries() { return entries; } + /** Initializes an empty trie from canonical leaves and writes its complete path-node set. */ + synchronized void initializeLeaves(Collection entries) { + importLeaves(entries, "initialized"); + dirty = true; + rootHash(); + } + /** Restores current leaves and verifies their complete path-node set without repairing it. */ synchronized void restoreLeaves(Collection entries) { - if (!leaves.isEmpty() || !committedPaths.isEmpty() || dirty) { - throw new IllegalStateException("path trie is not empty before leaf restoration"); - } - for (LeafEntry entry : Objects.requireNonNull(entries, "entries")) { - LeafEntry present = Objects.requireNonNull(entry, "entry"); - if (leaves.put(secureKey(present.secureKey), - nonEmpty(present.encodedValue, "encodedValue")) != null) { - throw new IllegalArgumentException("duplicate restored path-state leaf"); - } - } + importLeaves(entries, "restored"); Map expectedNodes = buildCurrentNodes(); for (Map.Entry entry : expectedNodes.entrySet()) { if (!Arrays.equals(nodeStore.get(entry.getKey().bytes), entry.getValue())) { @@ -109,6 +107,19 @@ synchronized void restoreLeaves(Collection entries) { rootHash = rootHash(expectedNodes); } + private void importLeaves(Collection entries, String operation) { + if (!leaves.isEmpty() || !committedPaths.isEmpty() || dirty) { + throw new IllegalStateException("path trie is not empty before leaf " + operation); + } + for (LeafEntry entry : Objects.requireNonNull(entries, "entries")) { + LeafEntry present = Objects.requireNonNull(entry, "entry"); + if (leaves.put(secureKey(present.secureKey), + nonEmpty(present.encodedValue, "encodedValue")) != null) { + throw new IllegalArgumentException("duplicate " + operation + " path-state leaf"); + } + } + } + /** Verifies every path owned by the current committed node set without repairing corruption. */ public synchronized void verifyNodeStore() { if (dirty) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java new file mode 100644 index 00000000000..5356fd3d3e7 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java @@ -0,0 +1,133 @@ +package org.tron.core.db2.stateroot; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; + +/** One writable, current-only path-state layer derived from the published canonical parent. */ +public final class PathStateLayer implements Closeable { + + private final PathStateStoreManifest manifest; + private final PathStateCurrentStore currentStore; + private final PathStateNodeStoreSet stores; + private final PathStateRoot root; + private final PathStateRootMetadata parent; + private final long blockNumber; + private final byte[] blockHash; + private final byte[] parentHash; + private final long timestamp; + private final P66Phase phase; + private final byte[] transitionDigest; + private PathStateRootMetadata prepared; + private boolean persisted; + private PathStateRootMetadata committed; + + private PathStateLayer(PathStateStoreManifest manifest, PathStateCurrentStore currentStore, + PathStateNodeStoreSet stores, PathStateRoot root, PathStateRootMetadata parent, + long blockNumber, byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, + byte[] transitionDigest) { + this.manifest = manifest; + this.currentStore = currentStore; + this.stores = stores; + this.root = root; + this.parent = parent; + this.blockNumber = blockNumber; + this.blockHash = copy32(blockHash, "blockHash"); + this.parentHash = copy32(parentHash, "parentHash"); + this.timestamp = timestamp; + this.phase = Objects.requireNonNull(phase, "phase"); + this.transitionDigest = copy32(transitionDigest, "transitionDigest"); + } + + /** Begins a child layer only when the supplied parent is the exact verified CURRENT record. */ + public static PathStateLayer begin(PathStateStoreManifest manifest, + PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase, byte[] transitionDigest) throws IOException { + PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); + PathStateRootMetadata admittedParent = Objects.requireNonNull(parent, "parent"); + PathStateCurrentStore currentStore = new PathStateCurrentStore(admitted); + requireSame(admittedParent, currentStore.current(), + "path-state layer parent is not CURRENT"); + if (blockNumber != admittedParent.getBlockNumber() + 1 + || !Arrays.equals(parentHash, admittedParent.getBlockHash())) { + throw new IOException("path-state layer identity does not extend CURRENT"); + } + + PathStateRootMetadata identity = PathStateRootMetadata.layer(blockNumber, blockHash, + parentHash, timestamp, phase, admitted.getIdentityDigest(), + admittedParent.getStateRoot(), admittedParent.getStateRoot(), transitionDigest); + try (PathStateNodeStoreSet parentStores = + PathStateNodeStoreSet.openPublished(admitted, admittedParent)) { + PathStateRoot parentRoot = parentStores.createRoot(); + PathStateNodeStoreSet childStores = PathStateNodeStoreSet.beginLayer(admitted, identity); + try { + PathStateRoot childRoot = childStores.createRootFrom(parentStores.leafRecords(), + parentRoot.rootHash()); + return new PathStateLayer(admitted, currentStore, childStores, childRoot, admittedParent, + blockNumber, blockHash, parentHash, timestamp, phase, transitionDigest); + } catch (RuntimeException failure) { + try { + childStores.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } + } + } + + public synchronized void apply(Collection mutations) { + requireUncommitted(); + root.apply(mutations); + } + + /** Persists this layer's nodes/leaves/progress before publishing metadata and CURRENT. */ + public synchronized PathStateRootMetadata commit() throws IOException { + if (committed != null) { + return committed; + } + if (prepared == null) { + prepared = PathStateRootMetadata.layer(blockNumber, blockHash, parentHash, timestamp, phase, + manifest.getIdentityDigest(), parent.getStateRoot(), root.rootHash(), transitionDigest); + } + if (!persisted) { + stores.commit(prepared); + persisted = true; + } + committed = currentStore.appendLayer(prepared); + return committed; + } + + public synchronized byte[] rootHash() { + return root.rootHash(); + } + + @Override + public synchronized void close() throws IOException { + stores.close(); + } + + private void requireUncommitted() { + if (prepared != null) { + throw new IllegalStateException("path-state layer is already frozen for commit"); + } + } + + private static void requireSame(PathStateRootMetadata expected, + PathStateRootMetadata actual, String error) throws IOException { + if (!Arrays.equals(expected.encode(), actual.encode())) { + throw new IOException(error); + } + } + + private static byte[] copy32(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length != PathStateRootMetadata.DIGEST_LENGTH) { + throw new IllegalArgumentException(name + " must be exactly 32 bytes"); + } + return copy; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index 5041aaca0fd..f664bb59a98 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -36,6 +36,7 @@ public final class PathStateNodeStoreSet implements Closeable { private final byte[] manifestDigest; private final Kind kind; private final PathStateRootMetadata expectedMetadata; + private final boolean sealed; private PathStateRootMetadata progress; private PathStateRoot root; private boolean rootClaimed; @@ -49,6 +50,8 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K this.manifestDigest = manifest.getIdentityDigest(); this.kind = kind; this.expectedMetadata = expectedMetadata; + this.sealed = Files.exists(directory.resolve(PathStateCurrentStore.METADATA_FILE), + LinkOption.NOFOLLOW_LINKS); nativeStore = PathStateNativeNodeStore.open(this.directory, manifest.getEngine()); try { progress = decodeProgress(nativeStore.get(PROGRESS_KEY)); @@ -96,6 +99,41 @@ public static PathStateNodeStoreSet openLayer(PathStateStoreManifest manifest, return new PathStateNodeStoreSet(layerDirectory, admitted, Kind.LAYER, layer); } + /** Opens the node database referenced by the verified current authority. */ + public static PathStateNodeStoreSet openCurrent(PathStateStoreManifest manifest) + throws IOException { + PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); + PathStateRootMetadata current = new PathStateCurrentStore(admitted).current(); + return openPublished(admitted, current); + } + + static PathStateNodeStoreSet beginLayer(PathStateStoreManifest manifest, + PathStateRootMetadata identity) throws IOException { + PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); + PathStateRootMetadata layer = Objects.requireNonNull(identity, "identity"); + if (layer.getKind() != Kind.LAYER + || !Arrays.equals(layer.getFormatDigest(), admitted.getIdentityDigest())) { + throw new IOException("path-state layer node set identity mismatch"); + } + Path directory = admitted.getLayerDirectory(layer.getBlockNumber(), layer.getBlockHash()); + requireUnsealed(directory); + return new PathStateNodeStoreSet(directory, admitted, Kind.LAYER, null); + } + + static PathStateNodeStoreSet openPublished(PathStateStoreManifest manifest, + PathStateRootMetadata metadata) throws IOException { + PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); + PathStateRootMetadata published = Objects.requireNonNull(metadata, "metadata"); + Path owner = published.getKind() == Kind.BASE ? admitted.getBaseDirectory() + : admitted.getLayerDirectory(published.getBlockNumber(), published.getBlockHash()); + PathStateRootMetadata stored = PathStateMetadataFile.load( + owner.resolve(PathStateCurrentStore.METADATA_FILE)); + if (!Arrays.equals(stored.encode(), published.encode())) { + throw new IOException("path-state published metadata differs from authority"); + } + return new PathStateNodeStoreSet(owner, admitted, published.getKind(), published); + } + /** Claims this database and restores its durable leaves when progress already exists. */ public synchronized PathStateRoot createRoot() { requireOpen(); @@ -116,9 +154,37 @@ public synchronized PathStateRoot createRoot() { return root; } + synchronized PathStateRoot createRootFrom(List parentLeaves, + byte[] parentRoot) { + requireOpen(); + if (rootClaimed) { + throw new IllegalStateException("path-state node database set already has a trie owner"); + } + if (progress != null || !persistedLeaves.isEmpty()) { + throw new IllegalStateException("path-state layer already contains durable state"); + } + PathStateRoot candidate = new PathStateRoot(scope, + participant -> participantStores.get(participant.getDbName()), superStore); + candidate.initializeLeaves(parentLeaves, parentRoot); + root = candidate; + rootClaimed = true; + return root; + } + + synchronized List leafRecords() { + requireOpen(); + if (root == null) { + throw new IllegalStateException("path-state node database set has no trie owner"); + } + return root.leafRecords(); + } + /** Atomically persists all pending path nodes and their exact root progress. */ public synchronized void commit(PathStateRootMetadata metadata) throws IOException { requireOpen(); + if (sealed) { + throw new IOException("path-state node database set is sealed by immutable metadata"); + } if (root == null) { throw new IllegalStateException("path-state node database set has no trie owner"); } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index 0f8e1ea7a0c..7f5a0ef7b5e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -117,7 +117,16 @@ synchronized List leafRecords() { return records; } + synchronized void initializeLeaves(Collection records, byte[] expectedRoot) { + restoreLeaves(records, expectedRoot, true); + } + synchronized void restoreLeaves(Collection records, byte[] expectedRoot) { + restoreLeaves(records, expectedRoot, false); + } + + private void restoreLeaves(Collection records, byte[] expectedRoot, + boolean initialize) { Map participants = new LinkedHashMap<>(); Map> leaves = new LinkedHashMap<>(); for (PathStateParticipant participant : scope.getParticipants()) { @@ -136,14 +145,22 @@ synchronized void restoreLeaves(Collection records, byte[] expectedR List superLeaves = new ArrayList<>(); for (PathStateParticipant participant : scope.getParticipants()) { PathMerkleTrie trie = participantTries.get(participant.getDbName()); - trie.restoreLeaves(leaves.get(participant.getStoreId())); + if (initialize) { + trie.initializeLeaves(leaves.get(participant.getStoreId())); + } else { + trie.restoreLeaves(leaves.get(participant.getStoreId())); + } byte[] storeRoot = trie.rootHash(); superLeaves.add(new PathMerkleTrie.LeafEntry( PathStateCommitmentCodec.superLeafKey(participant.getStoreId()), PathStateCommitmentCodec.superLeafValue(participant.getStoreId(), participant.getDbName(), participant.getStoreFormatVersion(), storeRoot))); } - superTrie.restoreLeaves(superLeaves); + if (initialize) { + superTrie.initializeLeaves(superLeaves); + } else { + superTrie.restoreLeaves(superLeaves); + } byte[] restoredRoot = superTrie.rootHash(); if (!Arrays.equals(restoredRoot, Objects.requireNonNull(expectedRoot, "expectedRoot"))) { throw new IllegalStateException("restored path-state root differs from durable progress"); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java new file mode 100644 index 00000000000..0594ba9dca4 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java @@ -0,0 +1,161 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.arch.Arch; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateLayerTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void layersInheritPublishedParentAndRestoreCurrentAcrossReopen() throws Exception { + for (Engine engine : availableEngines()) { + Fixture fixture = publishedBase("inherit-" + engine, engine); + PathStateRootMetadata first; + byte[] firstRoot; + try (PathStateLayer layer = PathStateLayer.begin(fixture.manifest, fixture.base, 101, + bytes(11), fixture.base.getBlockHash(), 303, P66Phase.P66_ON, bytes(12))) { + layer.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5}), + PathStateMutation.delete("account", new byte[]{3}))); + firstRoot = layer.rootHash(); + first = layer.commit(); + assertArrayEquals(first.encode(), layer.commit().encode()); + assertThrows(IllegalStateException.class, () -> layer.apply(Collections.singletonList( + PathStateMutation.delete("proposal", new byte[]{1})))); + } + + assertCurrentRoot(fixture.manifest, first, firstRoot); + + PathStateRootMetadata second; + byte[] secondRoot; + try (PathStateLayer layer = PathStateLayer.begin(fixture.manifest, first, 102, + bytes(13), first.getBlockHash(), 306, P66Phase.P66_ON, bytes(14))) { + layer.apply(Collections.singletonList( + PathStateMutation.put("account", new byte[]{7}, new byte[]{8}))); + secondRoot = layer.rootHash(); + second = layer.commit(); + } + + assertCurrentRoot(fixture.manifest, second, secondRoot); + try (Stream layers = Files.list(fixture.manifest.getLayersDirectory())) { + assertEquals(2, layers.count()); + } + } + } + + @Test + public void staleParentFailsBeforeCreatingLayerDirectory() throws Exception { + Fixture fixture = publishedBase("stale-parent", Engine.ROCKSDB); + PathStateRootMetadata first; + try (PathStateLayer layer = PathStateLayer.begin(fixture.manifest, fixture.base, 101, + bytes(11), fixture.base.getBlockHash(), 303, P66Phase.P66_ON, bytes(12))) { + layer.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5}))); + first = layer.commit(); + } + + Path rejected = fixture.manifest.getLayerDirectory(102, bytes(13)); + assertThrows(IOException.class, () -> PathStateLayer.begin(fixture.manifest, fixture.base, 102, + bytes(13), first.getBlockHash(), 306, P66Phase.P66_ON, bytes(14))); + assertFalse(Files.exists(rejected)); + } + + @Test + public void currentLayerRestoreFailsClosedWhenDurableLeafIsMissing() throws Exception { + Fixture fixture = publishedBase("corrupt-layer", Engine.ROCKSDB); + PathStateRootMetadata layer; + try (PathStateLayer child = PathStateLayer.begin(fixture.manifest, fixture.base, 101, + bytes(11), fixture.base.getBlockHash(), 303, P66Phase.P66_ON, bytes(12))) { + child.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5}))); + layer = child.commit(); + } + + Path nodes = fixture.manifest.getLayerDirectory(101, layer.getBlockHash()) + .resolve(PathStateNodeStoreSet.NODES_DIRECTORY); + try (PathStateNativeNodeStore nativeStore = + PathStateNativeNodeStore.open(nodes, Engine.ROCKSDB)) { + nativeStore.delete(durableLeafKey(21, + PathStateCommitmentCodec.storeLeafKey(21, new byte[]{1}))); + } + try (PathStateNodeStoreSet current = PathStateNodeStoreSet.openCurrent(fixture.manifest)) { + assertThrows(IllegalStateException.class, current::createRoot); + } + } + + private Fixture publishedBase(String name, Engine engine) throws Exception { + Path rootDirectory = new File(temporaryFolder.getRoot(), name).toPath(); + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen(rootDirectory, engine); + PathStateRootMetadata base; + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4}))); + base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, P66Phase.P66_ON, + manifest.getIdentityDigest(), root.rootHash(), bytes(3)); + new PathStateBasePublication(manifest).publish(stores, base); + } + return new Fixture(manifest, base); + } + + private static void assertCurrentRoot(PathStateStoreManifest manifest, + PathStateRootMetadata expected, byte[] expectedRoot) throws Exception { + assertArrayEquals(expected.encode(), new PathStateCurrentStore(manifest).current().encode()); + try (PathStateNodeStoreSet current = PathStateNodeStoreSet.openCurrent(manifest)) { + PathStateRoot restored = current.createRoot(); + assertArrayEquals(expectedRoot, restored.rootHash()); + restored.verifyNodeStores(); + } + } + + private static Engine[] availableEngines() { + return Arch.isArm64() ? new Engine[]{Engine.ROCKSDB} + : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; + } + + private static byte[] durableLeafKey(int storeId, byte[] secureKey) { + return java.nio.ByteBuffer.allocate(Integer.BYTES * 2 + secureKey.length) + .putInt(-2) + .putInt(storeId) + .put(secureKey) + .array(); + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + private static final class Fixture { + + private final PathStateStoreManifest manifest; + private final PathStateRootMetadata base; + + private Fixture(PathStateStoreManifest manifest, PathStateRootMetadata base) { + this.manifest = manifest; + this.base = base; + } + } +} From 23cf063f548e0563ebb12a6caadef39f6f303f81 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 21 Aug 2026 23:41:02 +0800 Subject: [PATCH 053/161] feat(chainbase): recover path state layers --- .../core/db2/stateroot/PathStateLayer.java | 27 +- .../stateroot/PathStateLayerPublication.java | 234 ++++++++++++++++++ .../PathStateLayerPublicationTest.java | 187 ++++++++++++++ 3 files changed, 437 insertions(+), 11 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerPublicationTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java index 5356fd3d3e7..c72c63231be 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java @@ -11,7 +11,7 @@ public final class PathStateLayer implements Closeable { private final PathStateStoreManifest manifest; - private final PathStateCurrentStore currentStore; + private final PathStateLayerPublication publication; private final PathStateNodeStoreSet stores; private final PathStateRoot root; private final PathStateRootMetadata parent; @@ -22,15 +22,14 @@ public final class PathStateLayer implements Closeable { private final P66Phase phase; private final byte[] transitionDigest; private PathStateRootMetadata prepared; - private boolean persisted; private PathStateRootMetadata committed; - private PathStateLayer(PathStateStoreManifest manifest, PathStateCurrentStore currentStore, + private PathStateLayer(PathStateStoreManifest manifest, PathStateLayerPublication publication, PathStateNodeStoreSet stores, PathStateRoot root, PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, byte[] transitionDigest) { this.manifest = manifest; - this.currentStore = currentStore; + this.publication = publication; this.stores = stores; this.root = root; this.parent = parent; @@ -46,6 +45,14 @@ private PathStateLayer(PathStateStoreManifest manifest, PathStateCurrentStore cu public static PathStateLayer begin(PathStateStoreManifest manifest, PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, byte[] transitionDigest) throws IOException { + return begin(manifest, parent, blockNumber, blockHash, parentHash, timestamp, phase, + transitionDigest, stage -> { }); + } + + static PathStateLayer begin(PathStateStoreManifest manifest, + PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase, byte[] transitionDigest, + PathStateLayerPublication.FaultHook faultHook) throws IOException { PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); PathStateRootMetadata admittedParent = Objects.requireNonNull(parent, "parent"); PathStateCurrentStore currentStore = new PathStateCurrentStore(admitted); @@ -66,8 +73,10 @@ public static PathStateLayer begin(PathStateStoreManifest manifest, try { PathStateRoot childRoot = childStores.createRootFrom(parentStores.leafRecords(), parentRoot.rootHash()); - return new PathStateLayer(admitted, currentStore, childStores, childRoot, admittedParent, - blockNumber, blockHash, parentHash, timestamp, phase, transitionDigest); + return new PathStateLayer(admitted, + new PathStateLayerPublication(admitted, faultHook), childStores, childRoot, + admittedParent, blockNumber, blockHash, parentHash, timestamp, phase, + transitionDigest); } catch (RuntimeException failure) { try { childStores.close(); @@ -93,11 +102,7 @@ public synchronized PathStateRootMetadata commit() throws IOException { prepared = PathStateRootMetadata.layer(blockNumber, blockHash, parentHash, timestamp, phase, manifest.getIdentityDigest(), parent.getStateRoot(), root.rootHash(), transitionDigest); } - if (!persisted) { - stores.commit(prepared); - persisted = true; - } - committed = currentStore.appendLayer(prepared); + committed = publication.publish(stores, prepared); return committed; } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java new file mode 100644 index 00000000000..af897757101 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java @@ -0,0 +1,234 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; +import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; + +/** Crash-recoverable publication boundary for one immutable reversible path-state layer. */ +public final class PathStateLayerPublication { + + public static final String INTENT_FILE = "INTENT"; + + private final PathStateStoreManifest manifest; + private final PathStateCurrentStore currentStore; + private final FaultHook faultHook; + + public PathStateLayerPublication(PathStateStoreManifest manifest) { + this(manifest, stage -> { }); + } + + PathStateLayerPublication(PathStateStoreManifest manifest, FaultHook faultHook) { + this.manifest = Objects.requireNonNull(manifest, "manifest"); + this.currentStore = new PathStateCurrentStore(manifest); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + /** Publishes one layer through intent, native progress, metadata, CURRENT, and retire stages. */ + public synchronized PathStateRootMetadata publish(PathStateNodeStoreSet stores, + PathStateRootMetadata metadata) throws IOException { + PathStateRootMetadata layer = requireLayer(metadata); + Path directory = layerDirectory(layer); + Path expectedNodes = directory.resolve(PathStateNodeStoreSet.NODES_DIRECTORY) + .toAbsolutePath().normalize(); + PathStateNodeStoreSet nodeStores = Objects.requireNonNull(stores, "stores"); + if (!expectedNodes.equals(nodeStores.getDirectory().toAbsolutePath().normalize())) { + throw new IllegalArgumentException("path-state LAYER node database directory mismatch"); + } + requireCurrentParentOrChild(layer); + + Path intent = directory.resolve(INTENT_FILE); + PathStateMetadataFile.publishImmutable(intent, layer); + faultHook.after(Stage.AFTER_INTENT); + nodeStores.commit(layer); + faultHook.after(Stage.AFTER_NODE_PROGRESS); + PathStateMetadataFile.publishImmutable( + directory.resolve(PathStateCurrentStore.METADATA_FILE), layer); + faultHook.after(Stage.AFTER_METADATA); + PathStateRootMetadata current = currentStore.current(); + if (!same(current, layer)) { + current = currentStore.appendLayer(layer); + } + faultHook.after(Stage.AFTER_CURRENT); + PathStateMetadataFile.deleteDurable(intent); + faultHook.after(Stage.AFTER_RETIRE); + return current; + } + + /** Reconciles the sole unfinished layer intent and verifies all settled layer authorities. */ + public synchronized RecoveryAction recover() throws IOException { + List layers = scanLayers(); + LayerState pending = null; + for (LayerState layer : layers) { + layer.verifySettledOrIntent(); + if (layer.intent != null) { + if (pending != null) { + throw new IOException("multiple unfinished path-state layer intents"); + } + pending = layer; + } + } + if (pending == null) { + verifyCurrentProgress(); + return RecoveryAction.NONE; + } + + PathStateRootMetadata intent = pending.intent; + PathStateRootMetadata current = currentStore.current(); + if (!same(current, intent) && !isParent(current, intent)) { + throw new IOException("path-state layer intent no longer extends CURRENT"); + } + if (pending.progress == null) { + if (same(current, intent)) { + throw new IOException("path-state CURRENT layer exists without native progress"); + } + PathStateMetadataFile.deleteDurable(pending.intentPath); + verifyCurrentProgress(); + return RecoveryAction.ROLLED_BACK_INTENT; + } + + requireSame(intent, pending.progress, + "path-state layer intent and native progress differ"); + if (!same(current, intent)) { + currentStore.appendLayer(intent); + } + PathStateMetadataFile.deleteDurable(pending.intentPath); + verifyCurrentProgress(); + return RecoveryAction.COMPLETED_PUBLICATION; + } + + private List scanLayers() throws IOException { + List entries = new ArrayList<>(); + try (Stream paths = Files.list(manifest.getLayersDirectory())) { + paths.sorted(Comparator.comparing(path -> path.getFileName().toString())) + .forEach(entries::add); + } + List layers = new ArrayList<>(entries.size()); + for (Path entry : entries) { + if (!Files.isDirectory(entry, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(entry)) { + throw new IOException("path-state layer entry is not a direct directory: " + entry); + } + layers.add(new LayerState(entry)); + } + return layers; + } + + private void verifyCurrentProgress() throws IOException { + PathStateRootMetadata current = currentStore.current(); + Path owner = current.getKind() == Kind.BASE ? manifest.getBaseDirectory() + : layerDirectory(current); + PathStateRootMetadata progress = PathStateNodeStoreSet.loadProgress(owner, manifest); + if (progress == null) { + throw new IOException("path-state CURRENT has no native progress"); + } + requireSame(current, progress, "path-state CURRENT and native progress differ"); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openPublished(manifest, current)) { + PathStateRoot root = stores.createRoot(); + root.verifyNodeStores(); + } + } + + private void requireCurrentParentOrChild(PathStateRootMetadata layer) throws IOException { + PathStateRootMetadata current = currentStore.current(); + if (!same(current, layer) && !isParent(current, layer)) { + throw new IOException("path-state LAYER publication does not extend CURRENT"); + } + } + + private PathStateRootMetadata requireLayer(PathStateRootMetadata metadata) throws IOException { + PathStateRootMetadata layer = Objects.requireNonNull(metadata, "metadata"); + if (layer.getKind() != Kind.LAYER + || !Arrays.equals(layer.getFormatDigest(), manifest.getIdentityDigest())) { + throw new IOException("path-state LAYER publication identity mismatch"); + } + return layer; + } + + private Path layerDirectory(PathStateRootMetadata metadata) { + return manifest.getLayerDirectory(metadata.getBlockNumber(), metadata.getBlockHash()); + } + + private static boolean isParent(PathStateRootMetadata parent, PathStateRootMetadata child) { + return child.getBlockNumber() == parent.getBlockNumber() + 1 + && Arrays.equals(child.getParentHash(), parent.getBlockHash()) + && Arrays.equals(child.getParentStateRoot(), parent.getStateRoot()); + } + + private static boolean same(PathStateRootMetadata left, PathStateRootMetadata right) { + return Arrays.equals(left.encode(), right.encode()); + } + + private static void requireSame(PathStateRootMetadata expected, + PathStateRootMetadata actual, String error) throws IOException { + if (!same(expected, actual)) { + throw new IOException(error); + } + } + + public enum RecoveryAction { + NONE, + ROLLED_BACK_INTENT, + COMPLETED_PUBLICATION + } + + enum Stage { + AFTER_INTENT, + AFTER_NODE_PROGRESS, + AFTER_METADATA, + AFTER_CURRENT, + AFTER_RETIRE + } + + @FunctionalInterface + interface FaultHook { + + void after(Stage stage) throws IOException; + } + + private final class LayerState { + + private final Path directory; + private final Path intentPath; + private final PathStateRootMetadata intent; + private final PathStateRootMetadata metadata; + private final PathStateRootMetadata progress; + + private LayerState(Path directory) throws IOException { + this.directory = directory; + this.intentPath = directory.resolve(INTENT_FILE); + Path metadataPath = directory.resolve(PathStateCurrentStore.METADATA_FILE); + this.intent = Files.exists(intentPath, LinkOption.NOFOLLOW_LINKS) + ? requireLayer(PathStateMetadataFile.load(intentPath)) : null; + this.metadata = Files.exists(metadataPath, LinkOption.NOFOLLOW_LINKS) + ? requireLayer(PathStateMetadataFile.load(metadataPath)) : null; + this.progress = PathStateNodeStoreSet.loadProgress(directory, manifest); + } + + private void verifySettledOrIntent() throws IOException { + PathStateRootMetadata identity = intent != null ? intent : metadata; + if (identity != null && !directory.equals(layerDirectory(identity))) { + throw new IOException("path-state layer record is in a noncanonical directory"); + } + if (metadata != null) { + if (progress == null) { + throw new IOException("path-state layer metadata exists without native progress"); + } + requireSame(metadata, progress, + "path-state layer metadata and native progress differ"); + } else if (progress != null && intent == null) { + throw new IOException("path-state layer has orphaned native progress"); + } + if (intent != null && metadata != null) { + requireSame(intent, metadata, + "path-state layer intent and metadata differ"); + } + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerPublicationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerPublicationTest.java new file mode 100644 index 00000000000..afb42bc7381 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerPublicationTest.java @@ -0,0 +1,187 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.arch.Arch; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateLayerPublication.RecoveryAction; +import org.tron.core.db2.stateroot.PathStateLayerPublication.Stage; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateLayerPublicationTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void faultsBeforeNativeProgressRollBackIntentIdempotently() throws Exception { + for (Engine engine : availableEngines()) { + Fixture fixture = fixture("before-progress-" + engine, engine, Stage.AFTER_INTENT); + assertThrows(IOException.class, fixture::publish); + fixture.close(); + + PathStateLayerPublication recovery = new PathStateLayerPublication(fixture.manifest); + assertEquals(RecoveryAction.ROLLED_BACK_INTENT, recovery.recover()); + assertEquals(RecoveryAction.NONE, recovery.recover()); + assertArrayEquals(fixture.base.encode(), + new PathStateCurrentStore(fixture.manifest).current().encode()); + assertFalse(Files.exists(fixture.intentPath())); + } + } + + @Test + public void faultsAfterNativeProgressCompletePublicationIdempotently() throws Exception { + for (Engine engine : availableEngines()) { + for (Stage stage : new Stage[]{Stage.AFTER_NODE_PROGRESS, Stage.AFTER_METADATA, + Stage.AFTER_CURRENT}) { + Fixture fixture = fixture("complete-" + engine + "-" + stage, engine, stage); + assertThrows(IOException.class, fixture::publish); + fixture.close(); + + PathStateLayerPublication recovery = new PathStateLayerPublication(fixture.manifest); + assertEquals(RecoveryAction.COMPLETED_PUBLICATION, recovery.recover()); + assertEquals(RecoveryAction.NONE, recovery.recover()); + assertSettled(fixture); + } + } + } + + @Test + public void faultAfterRetireIsAlreadySettled() throws Exception { + Fixture fixture = fixture("after-retire", Engine.ROCKSDB, Stage.AFTER_RETIRE); + assertThrows(IOException.class, fixture::publish); + fixture.close(); + + PathStateLayerPublication recovery = new PathStateLayerPublication(fixture.manifest); + assertEquals(RecoveryAction.NONE, recovery.recover()); + assertSettled(fixture); + } + + @Test + public void recoveryRejectsNativeProgressWithoutIntentOrMetadata() throws Exception { + Fixture fixture = fixture("orphan-progress", Engine.ROCKSDB, Stage.AFTER_NODE_PROGRESS); + assertThrows(IOException.class, fixture::publish); + fixture.close(); + Files.delete(fixture.intentPath()); + + assertThrows(IOException.class, + new PathStateLayerPublication(fixture.manifest)::recover); + } + + @Test + public void staleForkIntentCannotAdvanceCurrent() throws Exception { + Fixture stale = fixture("stale-intent", Engine.ROCKSDB, Stage.AFTER_INTENT); + assertThrows(IOException.class, stale::publish); + stale.close(); + + try (PathStateLayer canonical = PathStateLayer.begin(stale.manifest, stale.base, 101, + bytes(21), stale.base.getBlockHash(), 303, P66Phase.P66_ON, bytes(22))) { + canonical.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{7}))); + canonical.commit(); + } + + assertThrows(IOException.class, new PathStateLayerPublication(stale.manifest)::recover); + assertArrayEquals(bytes(21), + new PathStateCurrentStore(stale.manifest).current().getBlockHash()); + } + + private Fixture fixture(String name, Engine engine, Stage failure) throws Exception { + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen( + new File(temporaryFolder.getRoot(), name).toPath(), engine); + PathStateRootMetadata base = publishBase(manifest); + PathStateLayer layer = PathStateLayer.begin(manifest, base, 101, bytes(11), + base.getBlockHash(), 303, P66Phase.P66_ON, bytes(12), stage -> { + if (stage == failure) { + throw new IOException("injected after " + stage); + } + }); + layer.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5}), + PathStateMutation.delete("account", new byte[]{3}))); + return new Fixture(manifest, base, layer, layer.rootHash()); + } + + private static PathStateRootMetadata publishBase(PathStateStoreManifest manifest) + throws Exception { + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4}))); + PathStateRootMetadata base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), root.rootHash(), bytes(3)); + new PathStateBasePublication(manifest).publish(stores, base); + return base; + } + } + + private static void assertSettled(Fixture fixture) throws Exception { + PathStateRootMetadata current = new PathStateCurrentStore(fixture.manifest).current(); + assertEquals(101, current.getBlockNumber()); + assertArrayEquals(fixture.expectedRoot, current.getStateRoot()); + Path layerDirectory = fixture.manifest.getLayerDirectory(101, current.getBlockHash()); + assertArrayEquals(current.encode(), + PathStateNodeStoreSet.loadProgress(layerDirectory, fixture.manifest).encode()); + assertFalse(Files.exists(layerDirectory.resolve(PathStateLayerPublication.INTENT_FILE))); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openCurrent(fixture.manifest)) { + PathStateRoot restored = stores.createRoot(); + assertArrayEquals(fixture.expectedRoot, restored.rootHash()); + restored.verifyNodeStores(); + } + } + + private static Engine[] availableEngines() { + return Arch.isArm64() ? new Engine[]{Engine.ROCKSDB} + : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + private static final class Fixture { + + private final PathStateStoreManifest manifest; + private final PathStateRootMetadata base; + private final PathStateLayer layer; + private final byte[] expectedRoot; + + private Fixture(PathStateStoreManifest manifest, PathStateRootMetadata base, + PathStateLayer layer, byte[] expectedRoot) { + this.manifest = manifest; + this.base = base; + this.layer = layer; + this.expectedRoot = Arrays.copyOf(expectedRoot, expectedRoot.length); + } + + private void publish() throws IOException { + layer.commit(); + } + + private void close() throws IOException { + layer.close(); + } + + private Path intentPath() { + return manifest.getLayerDirectory(101, bytes(11)) + .resolve(PathStateLayerPublication.INTENT_FILE); + } + } +} From 006ab2f39fae45517c961b1340bf436b34777e2f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 22 Aug 2026 00:07:47 +0800 Subject: [PATCH 054/161] feat(chainbase): bound path state layers --- .../core/db2/stateroot/PathStateLayer.java | 25 ++- .../db2/stateroot/PathStateLayerLimits.java | 154 ++++++++++++++++++ .../stateroot/PathStateLayerPublication.java | 19 ++- .../stateroot/PathStateNativeNodeStore.java | 38 +++++ .../db2/stateroot/PathStateNodeStoreSet.java | 108 +++++++++++- .../stateroot/PathStateLayerLimitsTest.java | 153 +++++++++++++++++ 6 files changed, 489 insertions(+), 8 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerLimits.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerLimitsTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java index c72c63231be..390978b2a42 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java @@ -2,6 +2,7 @@ import java.io.Closeable; import java.io.IOException; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; import java.util.Objects; @@ -46,14 +47,31 @@ public static PathStateLayer begin(PathStateStoreManifest manifest, PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, byte[] transitionDigest) throws IOException { return begin(manifest, parent, blockNumber, blockHash, parentHash, timestamp, phase, - transitionDigest, stage -> { }); + transitionDigest, PathStateLayerLimits.defaults()); + } + + public static PathStateLayer begin(PathStateStoreManifest manifest, + PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase, byte[] transitionDigest, PathStateLayerLimits limits) + throws IOException { + return begin(manifest, parent, blockNumber, blockHash, parentHash, timestamp, phase, + transitionDigest, limits, stage -> { }); } static PathStateLayer begin(PathStateStoreManifest manifest, PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, byte[] transitionDigest, PathStateLayerPublication.FaultHook faultHook) throws IOException { + return begin(manifest, parent, blockNumber, blockHash, parentHash, timestamp, phase, + transitionDigest, PathStateLayerLimits.defaults(), faultHook); + } + + static PathStateLayer begin(PathStateStoreManifest manifest, + PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase, byte[] transitionDigest, PathStateLayerLimits limits, + PathStateLayerPublication.FaultHook faultHook) throws IOException { PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); + PathStateLayerLimits admittedLimits = Objects.requireNonNull(limits, "limits"); PathStateRootMetadata admittedParent = Objects.requireNonNull(parent, "parent"); PathStateCurrentStore currentStore = new PathStateCurrentStore(admitted); requireSame(admittedParent, currentStore.current(), @@ -66,6 +84,8 @@ static PathStateLayer begin(PathStateStoreManifest manifest, PathStateRootMetadata identity = PathStateRootMetadata.layer(blockNumber, blockHash, parentHash, timestamp, phase, admitted.getIdentityDigest(), admittedParent.getStateRoot(), admittedParent.getStateRoot(), transitionDigest); + Path layerDirectory = admitted.getLayerDirectory(blockNumber, blockHash); + admittedLimits.verifyCanBegin(admitted, layerDirectory); try (PathStateNodeStoreSet parentStores = PathStateNodeStoreSet.openPublished(admitted, admittedParent)) { PathStateRoot parentRoot = parentStores.createRoot(); @@ -74,7 +94,8 @@ static PathStateLayer begin(PathStateStoreManifest manifest, PathStateRoot childRoot = childStores.createRootFrom(parentStores.leafRecords(), parentRoot.rootHash()); return new PathStateLayer(admitted, - new PathStateLayerPublication(admitted, faultHook), childStores, childRoot, + new PathStateLayerPublication(admitted, admittedLimits, faultHook), + childStores, childRoot, admittedParent, blockNumber, blockHash, parentHash, timestamp, phase, transitionDigest); } catch (RuntimeException failure) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerLimits.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerLimits.java new file mode 100644 index 00000000000..7ca64d37daf --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerLimits.java @@ -0,0 +1,154 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Stream; +import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; + +/** Explicit count and logical-byte admission limits for current-only reversible layers. */ +public final class PathStateLayerLimits { + + public static final int DEFAULT_MAX_LAYERS = 128; + public static final long DEFAULT_MAX_LOGICAL_BYTES = 1L << 40; + + private final int maxLayers; + private final long maxLogicalBytes; + + public PathStateLayerLimits(int maxLayers, long maxLogicalBytes) { + if (maxLayers <= 0 || maxLogicalBytes <= 0) { + throw new IllegalArgumentException("path-state layer limits must be positive"); + } + this.maxLayers = maxLayers; + this.maxLogicalBytes = maxLogicalBytes; + } + + public static PathStateLayerLimits defaults() { + return new PathStateLayerLimits(DEFAULT_MAX_LAYERS, DEFAULT_MAX_LOGICAL_BYTES); + } + + void verifyCanBegin(PathStateStoreManifest manifest, Path candidate) throws IOException { + Usage usage = usageExcluding(manifest, candidate); + try { + requireWithin(Math.addExact(usage.layers, 1), usage.logicalBytes); + } catch (ArithmeticException overflow) { + throw new IOException("path-state layer count overflow", overflow); + } + } + + void verifyAdmission(PathStateStoreManifest manifest, Path candidate, + PathStateRootMetadata metadata, long nativeLogicalBytes) throws IOException { + Usage usage = usageExcluding(manifest, candidate); + long candidateBytes; + try { + candidateBytes = Math.addExact(nativeLogicalBytes, metadata.encode().length); + requireWithin(Math.addExact(usage.layers, 1), + Math.addExact(usage.logicalBytes, candidateBytes)); + } catch (ArithmeticException overflow) { + throw new IOException("path-state layer limit accounting overflow", overflow); + } + } + + void verifyExisting(PathStateStoreManifest manifest) throws IOException { + Usage usage = usageExcluding(manifest, null); + requireWithin(usage.layers, usage.logicalBytes); + } + + public int getMaxLayers() { + return maxLayers; + } + + public long getMaxLogicalBytes() { + return maxLogicalBytes; + } + + private Usage usageExcluding(PathStateStoreManifest manifest, Path excluded) + throws IOException { + PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); + Path omitted = excluded == null ? null : excluded.toAbsolutePath().normalize(); + int layers = 0; + long logicalBytes = 0; + try (Stream paths = Files.list(admitted.getLayersDirectory())) { + for (Path entry : (Iterable) paths::iterator) { + if (!Files.isDirectory(entry, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(entry)) { + throw new IOException("path-state layer entry is not a direct directory: " + entry); + } + if (omitted != null && omitted.equals(entry.toAbsolutePath().normalize())) { + continue; + } + Path metadataPath = entry.resolve(PathStateCurrentStore.METADATA_FILE); + Path intentPath = entry.resolve(PathStateLayerPublication.INTENT_FILE); + PathStateRootMetadata metadata = Files.exists(metadataPath, LinkOption.NOFOLLOW_LINKS) + ? requireLayer(admitted, entry, PathStateMetadataFile.load(metadataPath)) : null; + PathStateRootMetadata intent = Files.exists(intentPath, LinkOption.NOFOLLOW_LINKS) + ? requireLayer(admitted, entry, PathStateMetadataFile.load(intentPath)) : null; + PathStateRootMetadata progress = PathStateNodeStoreSet.loadProgress(entry, admitted); + Long nativeBytes = PathStateNodeStoreSet.loadLogicalBytes(entry, admitted); + if ((progress == null) != (nativeBytes == null)) { + throw new IOException("path-state layer progress and logical bytes marker differ"); + } + if (metadata != null) { + requireSame(metadata, progress, + "path-state layer metadata and native progress differ"); + layers = Math.addExact(layers, 1); + logicalBytes = Math.addExact(logicalBytes, + Math.addExact(nativeBytes, metadata.encode().length)); + } else if (intent != null && progress != null) { + requireSame(intent, progress, + "path-state layer intent and native progress differ"); + layers = Math.addExact(layers, 1); + logicalBytes = Math.addExact(logicalBytes, + Math.addExact(nativeBytes, intent.encode().length)); + } else if (progress != null) { + throw new IOException("path-state layer has orphaned native progress"); + } + } + } catch (ArithmeticException overflow) { + throw new IOException("path-state layer limit accounting overflow", overflow); + } + return new Usage(layers, logicalBytes); + } + + private void requireWithin(int layers, long logicalBytes) throws IOException { + if (layers > maxLayers) { + throw new IOException("path-state layer count limit exceeded: " + layers + " > " + + maxLayers); + } + if (logicalBytes > maxLogicalBytes) { + throw new IOException("path-state layer logical bytes limit exceeded: " + logicalBytes + + " > " + maxLogicalBytes); + } + } + + private static PathStateRootMetadata requireLayer(PathStateStoreManifest manifest, + Path directory, PathStateRootMetadata metadata) throws IOException { + if (metadata.getKind() != Kind.LAYER + || !Arrays.equals(metadata.getFormatDigest(), manifest.getIdentityDigest()) + || !directory.equals(manifest.getLayerDirectory( + metadata.getBlockNumber(), metadata.getBlockHash()))) { + throw new IOException("path-state layer limit record identity mismatch"); + } + return metadata; + } + + private static void requireSame(PathStateRootMetadata expected, + PathStateRootMetadata actual, String error) throws IOException { + if (actual == null || !Arrays.equals(expected.encode(), actual.encode())) { + throw new IOException(error); + } + } + + private static final class Usage { + + private final int layers; + private final long logicalBytes; + + private Usage(int layers, long logicalBytes) { + this.layers = layers; + this.logicalBytes = logicalBytes; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java index af897757101..d20fd714ab0 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java @@ -19,15 +19,27 @@ public final class PathStateLayerPublication { private final PathStateStoreManifest manifest; private final PathStateCurrentStore currentStore; + private final PathStateLayerLimits limits; private final FaultHook faultHook; public PathStateLayerPublication(PathStateStoreManifest manifest) { - this(manifest, stage -> { }); + this(manifest, PathStateLayerLimits.defaults()); + } + + public PathStateLayerPublication(PathStateStoreManifest manifest, + PathStateLayerLimits limits) { + this(manifest, limits, stage -> { }); } PathStateLayerPublication(PathStateStoreManifest manifest, FaultHook faultHook) { + this(manifest, PathStateLayerLimits.defaults(), faultHook); + } + + PathStateLayerPublication(PathStateStoreManifest manifest, PathStateLayerLimits limits, + FaultHook faultHook) { this.manifest = Objects.requireNonNull(manifest, "manifest"); this.currentStore = new PathStateCurrentStore(manifest); + this.limits = Objects.requireNonNull(limits, "limits"); this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); } @@ -43,6 +55,8 @@ public synchronized PathStateRootMetadata publish(PathStateNodeStoreSet stores, throw new IllegalArgumentException("path-state LAYER node database directory mismatch"); } requireCurrentParentOrChild(layer); + limits.verifyAdmission(manifest, directory, layer, + nodeStores.projectedLogicalBytes(layer)); Path intent = directory.resolve(INTENT_FILE); PathStateMetadataFile.publishImmutable(intent, layer); @@ -64,6 +78,7 @@ public synchronized PathStateRootMetadata publish(PathStateNodeStoreSet stores, /** Reconciles the sole unfinished layer intent and verifies all settled layer authorities. */ public synchronized RecoveryAction recover() throws IOException { + limits.verifyExisting(manifest); List layers = scanLayers(); LayerState pending = null; for (LayerState layer : layers) { @@ -91,6 +106,7 @@ public synchronized RecoveryAction recover() throws IOException { } PathStateMetadataFile.deleteDurable(pending.intentPath); verifyCurrentProgress(); + limits.verifyExisting(manifest); return RecoveryAction.ROLLED_BACK_INTENT; } @@ -101,6 +117,7 @@ public synchronized RecoveryAction recover() throws IOException { } PathStateMetadataFile.deleteDurable(pending.intentPath); verifyCurrentProgress(); + limits.verifyExisting(manifest); return RecoveryAction.COMPLETED_PUBLICATION; } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java index d1f112a5d3b..7cfd7a46e87 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java @@ -85,6 +85,11 @@ synchronized List scanPrefix(byte[] prefix) { return delegate.scanPrefix(nonEmpty(prefix, "prefix")); } + synchronized List scanAll() { + requireOpen(); + return delegate.scanAll(); + } + Path getDirectory() { return directory; } @@ -122,6 +127,8 @@ private interface Delegate extends Closeable { void writeBatch(List mutations); List scanPrefix(byte[] prefix); + + List scanAll(); } private static final class LevelDelegate implements Delegate { @@ -173,6 +180,21 @@ public List scanPrefix(byte[] prefix) { return entries; } + @Override + public List scanAll() { + List entries = new ArrayList<>(); + try (org.iq80.leveldb.DBIterator iterator = database.iterator()) { + iterator.seekToFirst(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + entries.add(new KeyValue(entry.getKey(), entry.getValue())); + } + } catch (IOException failure) { + throw new IllegalStateException("failed to scan all path-state LevelDB nodes", failure); + } + return entries; + } + @Override public void close() throws IOException { database.close(); @@ -238,6 +260,22 @@ public List scanPrefix(byte[] prefix) { return entries; } + @Override + public List scanAll() { + List entries = new ArrayList<>(); + try (org.rocksdb.RocksIterator iterator = database.newIterator()) { + iterator.seekToFirst(); + while (iterator.isValid()) { + entries.add(new KeyValue(iterator.key(), iterator.value())); + iterator.next(); + } + iterator.status(); + } catch (RocksDBException failure) { + throw new IllegalStateException("failed to scan all path-state RocksDB nodes", failure); + } + return entries; + } + @Override public void close() { syncWrites.close(); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index f664bb59a98..730f456d2c2 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -21,6 +21,9 @@ public final class PathStateNodeStoreSet implements Closeable { private static final byte[] PROGRESS_KEY = new byte[]{ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 'p', 'r', 'o', 'g', 'r', 'e', 's', 's'}; + private static final byte[] LOGICAL_BYTES_KEY = new byte[]{ + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + 'l', 'o', 'g', 'i', 'c', 'a', 'l', '-', 'b', 'y', 't', 'e', 's'}; private static final int LEAF_DOMAIN = -2; private static final byte[] LEAF_PREFIX = ByteBuffer.allocate(Integer.BYTES) .putInt(LEAF_DOMAIN).array(); @@ -38,6 +41,7 @@ public final class PathStateNodeStoreSet implements Closeable { private final PathStateRootMetadata expectedMetadata; private final boolean sealed; private PathStateRootMetadata progress; + private Long logicalBytes; private PathStateRoot root; private boolean rootClaimed; private boolean closed; @@ -55,6 +59,10 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K nativeStore = PathStateNativeNodeStore.open(this.directory, manifest.getEngine()); try { progress = decodeProgress(nativeStore.get(PROGRESS_KEY)); + logicalBytes = decodeLogicalBytes(nativeStore.get(LOGICAL_BYTES_KEY)); + if ((progress == null) != (logicalBytes == null)) { + throw new IOException("path-state native progress and logical bytes marker differ"); + } if (progress != null) { requireProgressIdentity(progress); } else if (kind == Kind.BASE && expectedMetadata != null) { @@ -189,11 +197,7 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti throw new IllegalStateException("path-state node database set has no trie owner"); } PathStateRootMetadata next = Objects.requireNonNull(metadata, "metadata"); - requireProgressIdentity(next); - byte[] currentRoot = root.rootHash(); - if (!Arrays.equals(currentRoot, next.getStateRoot())) { - throw new IllegalArgumentException("path-state progress root does not match trie root"); - } + long nextLogicalBytes = projectedLogicalBytes(next); List mutations = new ArrayList<>(pending.size() + persistedLeaves.size() + 1); for (Map.Entry entry : pending.entrySet()) { @@ -215,11 +219,45 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti } } mutations.add(PathStateNativeNodeStore.BatchMutation.put(PROGRESS_KEY, next.encode())); + mutations.add(PathStateNativeNodeStore.BatchMutation.put(LOGICAL_BYTES_KEY, + ByteBuffer.allocate(Long.BYTES).putLong(nextLogicalBytes).array())); nativeStore.writeBatch(mutations); pending.clear(); persistedLeaves.clear(); persistedLeaves.putAll(nextLeaves); progress = next; + logicalBytes = nextLogicalBytes; + } + + synchronized long projectedLogicalBytes(PathStateRootMetadata metadata) throws IOException { + requireOpen(); + if (root == null) { + throw new IllegalStateException("path-state node database set has no trie owner"); + } + PathStateRootMetadata next = Objects.requireNonNull(metadata, "metadata"); + requireProgressIdentity(next); + if (!Arrays.equals(root.rootHash(), next.getStateRoot())) { + throw new IllegalArgumentException("path-state progress root does not match trie root"); + } + long total = logicalBytes == null ? 0 : logicalBytes; + for (Map.Entry entry : pending.entrySet()) { + byte[] key = entry.getKey().copy(); + total = replaceLogicalEntry(total, key, nativeStore.get(key), entry.getValue()); + } + Map nextLeaves = leafMap(root.leafRecords()); + for (Map.Entry entry : persistedLeaves.entrySet()) { + if (!nextLeaves.containsKey(entry.getKey())) { + total = replaceLogicalEntry(total, entry.getKey().copy(), entry.getValue(), null); + } + } + for (Map.Entry entry : nextLeaves.entrySet()) { + byte[] previous = persistedLeaves.get(entry.getKey()); + if (!Arrays.equals(previous, entry.getValue())) { + total = replaceLogicalEntry(total, entry.getKey().copy(), previous, entry.getValue()); + } + } + return replaceLogicalEntry(total, PROGRESS_KEY, + progress == null ? null : progress.encode(), next.encode()); } public synchronized PathStateRootMetadata getProgress() { @@ -242,6 +280,40 @@ static PathStateRootMetadata loadProgress(Path ownerDirectory, } } + static Long loadLogicalBytes(Path ownerDirectory, PathStateStoreManifest manifest) + throws IOException { + Path nodes = Objects.requireNonNull(ownerDirectory, "ownerDirectory").resolve(NODES_DIRECTORY); + if (!Files.exists(nodes, LinkOption.NOFOLLOW_LINKS)) { + return null; + } + if (!Files.isDirectory(nodes, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(nodes)) { + throw new IOException("path-state node database is not a direct directory: " + nodes); + } + try (PathStateNativeNodeStore store = PathStateNativeNodeStore.open(nodes, + Objects.requireNonNull(manifest, "manifest").getEngine())) { + Long expected = decodeLogicalBytes(store.get(LOGICAL_BYTES_KEY)); + if (expected == null) { + return null; + } + long actual = 0; + try { + for (PathStateNativeNodeStore.KeyValue entry : store.scanAll()) { + byte[] key = entry.getKey(); + if (!Arrays.equals(key, LOGICAL_BYTES_KEY)) { + actual = Math.addExact(actual, + Math.addExact(key.length, entry.getValue().length)); + } + } + } catch (ArithmeticException overflow) { + throw new IOException("path-state logical bytes verification overflow", overflow); + } + if (actual != expected) { + throw new IOException("path-state logical bytes marker does not match native entries"); + } + return expected; + } + } + public Path getDirectory() { return directory; } @@ -348,6 +420,32 @@ private static PathStateRootMetadata decodeProgress(byte[] encoded) throws IOExc } } + private static Long decodeLogicalBytes(byte[] encoded) throws IOException { + if (encoded == null) { + return null; + } + if (encoded.length != Long.BYTES) { + throw new IOException("path-state logical bytes marker is corrupt"); + } + long value = ByteBuffer.wrap(encoded).getLong(); + if (value < 0) { + throw new IOException("path-state logical bytes marker is negative"); + } + return value; + } + + private static long replaceLogicalEntry(long total, byte[] key, byte[] previous, byte[] next) + throws IOException { + try { + long adjusted = previous == null ? total + : Math.subtractExact(total, Math.addExact(key.length, previous.length)); + return next == null ? adjusted + : Math.addExact(adjusted, Math.addExact(key.length, next.length)); + } catch (ArithmeticException overflow) { + throw new IOException("path-state logical bytes overflow", overflow); + } + } + private static void requireUnsealed(Path directory) throws IOException { Path metadata = directory.resolve(PathStateCurrentStore.METADATA_FILE); if (Files.exists(metadata, LinkOption.NOFOLLOW_LINKS)) { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerLimitsTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerLimitsTest.java new file mode 100644 index 00000000000..d43bbe9b43c --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerLimitsTest.java @@ -0,0 +1,153 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.arch.Arch; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateLayerLimitsTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void countLimitRejectsSecondLayerBeforeCreatingItsDirectory() throws Exception { + for (Engine engine : availableEngines()) { + Fixture fixture = fixture("count-" + engine, engine); + PathStateLayerLimits limits = new PathStateLayerLimits(1, Long.MAX_VALUE); + PathStateRootMetadata first = append(fixture.manifest, fixture.base, 101, 11, limits); + Path rejected = fixture.manifest.getLayerDirectory(102, bytes(13)); + + assertThrows(IOException.class, () -> PathStateLayer.begin(fixture.manifest, first, 102, + bytes(13), first.getBlockHash(), 306, P66Phase.P66_ON, bytes(14), limits)); + assertFalse(Files.exists(rejected)); + assertArrayEquals(first.encode(), + new PathStateCurrentStore(fixture.manifest).current().encode()); + Long logicalBytes = PathStateNodeStoreSet.loadLogicalBytes( + fixture.manifest.getLayerDirectory(101, first.getBlockHash()), fixture.manifest); + assertNotNull(logicalBytes); + assertTrue(logicalBytes > 0); + } + } + + @Test + public void byteLimitRejectsCommitBeforeIntentOrNativeProgress() throws Exception { + Fixture fixture = fixture("bytes", Engine.ROCKSDB); + PathStateLayerLimits limits = new PathStateLayerLimits(10, 1); + Path directory = fixture.manifest.getLayerDirectory(101, bytes(11)); + try (PathStateLayer layer = PathStateLayer.begin(fixture.manifest, fixture.base, 101, + bytes(11), fixture.base.getBlockHash(), 303, P66Phase.P66_ON, bytes(12), limits)) { + layer.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5}))); + assertThrows(IOException.class, layer::commit); + } + + assertFalse(Files.exists(directory.resolve(PathStateLayerPublication.INTENT_FILE))); + assertFalse(Files.exists(directory.resolve(PathStateCurrentStore.METADATA_FILE))); + assertNull(PathStateNodeStoreSet.loadProgress(directory, fixture.manifest)); + assertNull(PathStateNodeStoreSet.loadLogicalBytes(directory, fixture.manifest)); + assertArrayEquals(fixture.base.encode(), + new PathStateCurrentStore(fixture.manifest).current().encode()); + } + + @Test + public void restartRejectsStoredUsageAboveConfiguredLimits() throws Exception { + Fixture fixture = fixture("restart-limit", Engine.ROCKSDB); + PathStateLayerLimits roomy = new PathStateLayerLimits(10, Long.MAX_VALUE); + PathStateRootMetadata first = append(fixture.manifest, fixture.base, 101, 11, roomy); + append(fixture.manifest, first, 102, 13, roomy); + + assertThrows(IOException.class, () -> new PathStateLayerPublication(fixture.manifest, + new PathStateLayerLimits(1, Long.MAX_VALUE)).recover()); + assertThrows(IOException.class, () -> new PathStateLayerPublication(fixture.manifest, + new PathStateLayerLimits(10, 1)).recover()); + } + + @Test + public void corruptLogicalBytesMarkerFailsClosed() throws Exception { + Fixture fixture = fixture("corrupt-marker", Engine.ROCKSDB); + PathStateRootMetadata layer = append(fixture.manifest, fixture.base, 101, 11, + PathStateLayerLimits.defaults()); + Path nodes = fixture.manifest.getLayerDirectory(101, layer.getBlockHash()) + .resolve(PathStateNodeStoreSet.NODES_DIRECTORY); + try (PathStateNativeNodeStore store = PathStateNativeNodeStore.open(nodes, Engine.ROCKSDB)) { + store.put(logicalBytesKey(), ByteBuffer.allocate(Long.BYTES).putLong(1).array()); + } + + assertThrows(IOException.class, + new PathStateLayerPublication(fixture.manifest)::recover); + } + + private Fixture fixture(String name, Engine engine) throws Exception { + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen( + new File(temporaryFolder.getRoot(), name).toPath(), engine); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4}))); + PathStateRootMetadata base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), root.rootHash(), bytes(3)); + new PathStateBasePublication(manifest).publish(stores, base); + return new Fixture(manifest, base); + } + } + + private static PathStateRootMetadata append(PathStateStoreManifest manifest, + PathStateRootMetadata parent, long blockNumber, int seed, PathStateLayerLimits limits) + throws Exception { + try (PathStateLayer layer = PathStateLayer.begin(manifest, parent, blockNumber, bytes(seed), + parent.getBlockHash(), blockNumber * 3, P66Phase.P66_ON, bytes(seed + 1), limits)) { + layer.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{(byte) seed}))); + return layer.commit(); + } + } + + private static Engine[] availableEngines() { + return Arch.isArm64() ? new Engine[]{Engine.ROCKSDB} + : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; + } + + private static byte[] logicalBytesKey() { + byte[] suffix = "logical-bytes".getBytes(StandardCharsets.US_ASCII); + return ByteBuffer.allocate(Integer.BYTES + suffix.length) + .putInt(-1).put(suffix).array(); + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + private static final class Fixture { + + private final PathStateStoreManifest manifest; + private final PathStateRootMetadata base; + + private Fixture(PathStateStoreManifest manifest, PathStateRootMetadata base) { + this.manifest = manifest; + this.base = base; + } + } +} From 19cf998fdebb14a951050d2e40bf893c76c46f3d Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 22 Aug 2026 00:48:26 +0800 Subject: [PATCH 055/161] feat(chainbase): switch path state ancestry --- .../db2/stateroot/PathStateCurrentStore.java | 70 ++++++++ .../PathStateCanonicalSwitchTest.java | 163 ++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalSwitchTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java index 42cd45a6878..eb5cea68af7 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java @@ -65,6 +65,44 @@ public synchronized PathStateRootMetadata appendLayer(PathStateRootMetadata laye return current(); } + /** Atomically switches CURRENT to an exact durable ancestor inside the reversible window. */ + public synchronized PathStateRootMetadata switchToAncestor(PathStateRootMetadata target, + PathStateLayerLimits limits) throws IOException { + return switchToAncestor(target, limits, temporary -> { }); + } + + synchronized PathStateRootMetadata switchToAncestor(PathStateRootMetadata target, + PathStateLayerLimits limits, PathStateMetadataFile.FaultHook faultHook) throws IOException { + PathStateRootMetadata admittedTarget = Objects.requireNonNull(target, "target"); + PathStateLayerLimits admittedLimits = Objects.requireNonNull(limits, "limits"); + requireFormat(admittedTarget); + PathStateRootMetadata head = current(); + if (same(head, admittedTarget)) { + verifyTargetState(admittedTarget); + return head; + } + if (admittedTarget.getBlockNumber() >= head.getBlockNumber()) { + throw new IOException("path-state canonical switch target is not an ancestor"); + } + + PathStateRootMetadata base = requireKind(PathStateMetadataFile.load(basePath()), Kind.BASE); + requireFormat(base); + PathStateRootMetadata cursor = head; + for (int depth = 1; depth <= admittedLimits.getMaxLayers(); depth++) { + cursor = parentOf(cursor, base); + if (same(cursor, admittedTarget)) { + verifyTargetState(admittedTarget); + PathStateMetadataFile.replaceCurrent(currentPath, admittedTarget, + Objects.requireNonNull(faultHook, "faultHook")); + return current(); + } + if (cursor.getKind() == Kind.BASE) { + break; + } + } + throw new IOException("path-state canonical switch exceeds the reversible window"); + } + /** Loads CURRENT and verifies that every referenced layer reaches the single durable base. */ public synchronized PathStateRootMetadata current() throws IOException { PathStateRootMetadata base = requireKind(PathStateMetadataFile.load(basePath()), Kind.BASE); @@ -115,6 +153,38 @@ private Path layerPath(long blockNumber, byte[] blockHash) { return manifest.getLayerDirectory(blockNumber, blockHash).resolve(METADATA_FILE); } + private PathStateRootMetadata parentOf(PathStateRootMetadata child, + PathStateRootMetadata base) throws IOException { + if (child.getKind() != Kind.LAYER || child.getBlockNumber() == 0) { + throw new IOException("path-state canonical switch reached an invalid parent boundary"); + } + PathStateRootMetadata parent; + if (base.getBlockNumber() == child.getBlockNumber() - 1) { + parent = base; + } else { + parent = requireKind(PathStateMetadataFile.load( + layerPath(child.getBlockNumber() - 1, child.getParentHash())), Kind.LAYER); + requireFormat(parent); + } + requireChild(parent, child); + return parent; + } + + private void verifyTargetState(PathStateRootMetadata target) throws IOException { + Path owner = target.getKind() == Kind.BASE ? manifest.getBaseDirectory() + : manifest.getLayerDirectory(target.getBlockNumber(), target.getBlockHash()); + PathStateRootMetadata progress = PathStateNodeStoreSet.loadProgress(owner, manifest); + if (progress == null || !same(target, progress)) { + throw new IOException("path-state canonical switch target has invalid native progress"); + } + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openPublished(manifest, target)) { + PathStateRoot root = stores.createRoot(); + root.verifyNodeStores(); + } catch (IllegalArgumentException | IllegalStateException e) { + throw new IOException("path-state canonical switch target is corrupt", e); + } + } + private static PathStateRootMetadata requireKind(PathStateRootMetadata metadata, Kind kind) { PathStateRootMetadata present = Objects.requireNonNull(metadata, "metadata"); if (present.getKind() != kind) { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalSwitchTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalSwitchTest.java new file mode 100644 index 00000000000..eb5e66f4cd4 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalSwitchTest.java @@ -0,0 +1,163 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.arch.Arch; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateCanonicalSwitchTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void switchesToAncestorThenBuildsAndRestoresCanonicalSiblingFork() throws Exception { + for (Engine engine : availableEngines()) { + Fixture fixture = fixture("fork-" + engine, engine); + PathStateLayerLimits limits = new PathStateLayerLimits(10, Long.MAX_VALUE); + PathStateRootMetadata firstA = append(fixture.manifest, fixture.base, 101, 11, limits); + PathStateRootMetadata secondA = append(fixture.manifest, firstA, 102, 13, limits); + PathStateCurrentStore currentStore = new PathStateCurrentStore(fixture.manifest); + + assertArrayEquals(fixture.base.encode(), + currentStore.switchToAncestor(fixture.base, limits).encode()); + PathStateRootMetadata firstB = append(fixture.manifest, fixture.base, 101, 21, limits); + PathStateRootMetadata secondB = append(fixture.manifest, firstB, 102, 23, limits); + + assertArrayEquals(secondB.encode(), currentStore.current().encode()); + assertThrows(IOException.class, () -> currentStore.switchToAncestor(secondA, limits)); + assertArrayEquals(secondB.encode(), currentStore.current().encode()); + assertTrue(Files.exists(fixture.manifest.getLayerDirectory(102, secondA.getBlockHash()) + .resolve(PathStateCurrentStore.METADATA_FILE))); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openCurrent(fixture.manifest)) { + PathStateRoot restored = stores.createRoot(); + assertArrayEquals(secondB.getStateRoot(), restored.rootHash()); + restored.verifyNodeStores(); + } + assertArrayEquals(secondB.encode(), new PathStateCurrentStore( + PathStateStoreManifest.validateExisting(fixture.manifest.getDirectory(), engine)) + .current().encode()); + } + } + + @Test + public void deepReorgBeyondConfiguredWindowFailsClosed() throws Exception { + Fixture fixture = fixture("deep-reorg", Engine.ROCKSDB); + PathStateLayerLimits roomy = new PathStateLayerLimits(10, Long.MAX_VALUE); + PathStateRootMetadata first = append(fixture.manifest, fixture.base, 101, 11, roomy); + PathStateRootMetadata second = append(fixture.manifest, first, 102, 13, roomy); + PathStateCurrentStore currentStore = new PathStateCurrentStore(fixture.manifest); + + assertThrows(IOException.class, () -> currentStore.switchToAncestor(fixture.base, + new PathStateLayerLimits(1, Long.MAX_VALUE))); + assertArrayEquals(second.encode(), currentStore.current().encode()); + } + + @Test + public void corruptAncestorNodesRejectSwitchWithoutMovingCurrent() throws Exception { + Fixture fixture = fixture("corrupt-target", Engine.ROCKSDB); + PathStateLayerLimits limits = new PathStateLayerLimits(10, Long.MAX_VALUE); + PathStateRootMetadata current = append(fixture.manifest, fixture.base, 101, 11, limits); + Path nodes = fixture.manifest.getBaseDirectory().resolve(PathStateNodeStoreSet.NODES_DIRECTORY); + try (PathStateNativeNodeStore store = PathStateNativeNodeStore.open(nodes, Engine.ROCKSDB)) { + store.delete(durableLeafKey(21, + PathStateCommitmentCodec.storeLeafKey(21, new byte[]{1}))); + } + + PathStateCurrentStore currentStore = new PathStateCurrentStore(fixture.manifest); + assertThrows(IOException.class, () -> currentStore.switchToAncestor(fixture.base, limits)); + assertArrayEquals(current.encode(), currentStore.current().encode()); + } + + @Test + public void failedCurrentReplacementPreservesOldCanonicalHead() throws Exception { + Fixture fixture = fixture("switch-fault", Engine.ROCKSDB); + PathStateLayerLimits limits = new PathStateLayerLimits(10, Long.MAX_VALUE); + PathStateRootMetadata current = append(fixture.manifest, fixture.base, 101, 11, limits); + PathStateCurrentStore currentStore = new PathStateCurrentStore(fixture.manifest); + + assertThrows(IOException.class, () -> currentStore.switchToAncestor(fixture.base, limits, + temporary -> { + assertTrue(Files.exists(temporary)); + throw new IOException("injected after temporary force"); + })); + assertArrayEquals(current.encode(), currentStore.current().encode()); + try (Stream paths = Files.list(fixture.manifest.getDirectory())) { + assertTrue(paths.noneMatch(path -> path.getFileName().toString().startsWith(".CURRENT-"))); + } + assertArrayEquals(fixture.base.encode(), + currentStore.switchToAncestor(fixture.base, limits).encode()); + } + + private Fixture fixture(String name, Engine engine) throws Exception { + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen( + new File(temporaryFolder.getRoot(), name).toPath(), engine); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4}))); + PathStateRootMetadata base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), root.rootHash(), bytes(3)); + new PathStateBasePublication(manifest).publish(stores, base); + return new Fixture(manifest, base); + } + } + + private static PathStateRootMetadata append(PathStateStoreManifest manifest, + PathStateRootMetadata parent, long blockNumber, int seed, PathStateLayerLimits limits) + throws Exception { + try (PathStateLayer layer = PathStateLayer.begin(manifest, parent, blockNumber, bytes(seed), + parent.getBlockHash(), blockNumber * 3, P66Phase.P66_ON, bytes(seed + 1), limits)) { + layer.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{(byte) seed}))); + return layer.commit(); + } + } + + private static byte[] durableLeafKey(int storeId, byte[] secureKey) { + return java.nio.ByteBuffer.allocate(Integer.BYTES * 2 + secureKey.length) + .putInt(-2) + .putInt(storeId) + .put(secureKey) + .array(); + } + + private static Engine[] availableEngines() { + return Arch.isArm64() ? new Engine[]{Engine.ROCKSDB} + : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + private static final class Fixture { + + private final PathStateStoreManifest manifest; + private final PathStateRootMetadata base; + + private Fixture(PathStateStoreManifest manifest, PathStateRootMetadata base) { + this.manifest = manifest; + this.base = base; + } + } +} From fa6819fea112654175b7616c776db9b944a1274a Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 22 Aug 2026 18:12:00 +0800 Subject: [PATCH 056/161] feat(chainbase): retire path state forks --- .../db2/stateroot/PathStateCurrentStore.java | 27 +- .../stateroot/PathStateLayerRetirement.java | 281 ++++++++++++++++++ .../db2/stateroot/PathStateMetadataFile.java | 35 ++- .../PathStateLayerRetirementTest.java | 193 ++++++++++++ 4 files changed, 529 insertions(+), 7 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerRetirement.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerRetirementTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java index eb5cea68af7..c09958f5cbb 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java @@ -4,7 +4,10 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Objects; import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; @@ -19,7 +22,7 @@ public final class PathStateCurrentStore { public static final String METADATA_FILE = "METADATA"; public static final String CURRENT_FILE = "CURRENT"; - private static final int MAX_VALIDATION_LAYERS = 65_536; + static final int MAX_VALIDATION_LAYERS = 65_536; private final PathStateStoreManifest manifest; private final Path currentPath; @@ -66,20 +69,32 @@ public synchronized PathStateRootMetadata appendLayer(PathStateRootMetadata laye } /** Atomically switches CURRENT to an exact durable ancestor inside the reversible window. */ - public synchronized PathStateRootMetadata switchToAncestor(PathStateRootMetadata target, + synchronized PathStateRootMetadata switchToAncestor(PathStateRootMetadata target, PathStateLayerLimits limits) throws IOException { return switchToAncestor(target, limits, temporary -> { }); } synchronized PathStateRootMetadata switchToAncestor(PathStateRootMetadata target, PathStateLayerLimits limits, PathStateMetadataFile.FaultHook faultHook) throws IOException { + List suffix = layersAboveAncestor(target, limits); + PathStateRootMetadata admittedTarget = Objects.requireNonNull(target, "target"); + if (suffix.isEmpty()) { + return current(); + } + PathStateMetadataFile.replaceCurrent(currentPath, admittedTarget, + Objects.requireNonNull(faultHook, "faultHook")); + return current(); + } + + synchronized List layersAboveAncestor(PathStateRootMetadata target, + PathStateLayerLimits limits) throws IOException { PathStateRootMetadata admittedTarget = Objects.requireNonNull(target, "target"); PathStateLayerLimits admittedLimits = Objects.requireNonNull(limits, "limits"); requireFormat(admittedTarget); PathStateRootMetadata head = current(); if (same(head, admittedTarget)) { verifyTargetState(admittedTarget); - return head; + return Collections.emptyList(); } if (admittedTarget.getBlockNumber() >= head.getBlockNumber()) { throw new IOException("path-state canonical switch target is not an ancestor"); @@ -87,14 +102,14 @@ synchronized PathStateRootMetadata switchToAncestor(PathStateRootMetadata target PathStateRootMetadata base = requireKind(PathStateMetadataFile.load(basePath()), Kind.BASE); requireFormat(base); + List suffix = new ArrayList<>(); PathStateRootMetadata cursor = head; for (int depth = 1; depth <= admittedLimits.getMaxLayers(); depth++) { + suffix.add(cursor); cursor = parentOf(cursor, base); if (same(cursor, admittedTarget)) { verifyTargetState(admittedTarget); - PathStateMetadataFile.replaceCurrent(currentPath, admittedTarget, - Objects.requireNonNull(faultHook, "faultHook")); - return current(); + return Collections.unmodifiableList(suffix); } if (cursor.getKind() == Kind.BASE) { break; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerRetirement.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerRetirement.java new file mode 100644 index 00000000000..3b7822c24c5 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerRetirement.java @@ -0,0 +1,281 @@ +package org.tron.core.db2.stateroot; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; +import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; + +/** Crash-recoverable removal of the old canonical suffix after an ancestor switch. */ +public final class PathStateLayerRetirement { + + public static final String INTENT_FILE = "RETIRE_INTENT"; + + private static final int MAGIC = 0x50535254; // PSRT + private static final short VERSION = 1; + private static final int MAX_PLAN_LENGTH = 32 * 1024 * 1024; + + private final PathStateStoreManifest manifest; + private final PathStateCurrentStore currentStore; + private final PathStateLayerLimits limits; + private final FaultHook faultHook; + private final Path intentPath; + + public PathStateLayerRetirement(PathStateStoreManifest manifest, + PathStateLayerLimits limits) { + this(manifest, limits, stage -> { }); + } + + PathStateLayerRetirement(PathStateStoreManifest manifest, PathStateLayerLimits limits, + FaultHook faultHook) { + this.manifest = Objects.requireNonNull(manifest, "manifest"); + this.currentStore = new PathStateCurrentStore(manifest); + this.limits = Objects.requireNonNull(limits, "limits"); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + this.intentPath = manifest.getDirectory().resolve(INTENT_FILE); + } + + /** Switches to an exact ancestor and durably removes only the old canonical suffix. */ + public synchronized PathStateRootMetadata switchToAncestor(PathStateRootMetadata target) + throws IOException { + if (Files.exists(intentPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("unfinished path-state layer retirement requires recovery"); + } + List victims = currentStore.layersAboveAncestor(target, limits); + if (victims.isEmpty()) { + return currentStore.current(); + } + RetirementPlan plan = new RetirementPlan(target, victims); + plan.verify(manifest); + PathStateMetadataFile.publishImmutableBytes(intentPath, plan.encode()); + faultHook.after(Stage.AFTER_INTENT); + PathStateRootMetadata current = currentStore.switchToAncestor(target, limits); + faultHook.after(Stage.AFTER_CURRENT); + retire(plan); + PathStateMetadataFile.deleteDurable(intentPath); + faultHook.after(Stage.AFTER_RETIRE); + limits.verifyExisting(manifest); + return current; + } + + /** Completes one durable switch/retire plan and becomes a zero-action retry. */ + public synchronized RecoveryAction recover() throws IOException { + if (!Files.exists(intentPath, LinkOption.NOFOLLOW_LINKS)) { + limits.verifyExisting(manifest); + return RecoveryAction.NONE; + } + RetirementPlan plan = RetirementPlan.decode( + PathStateMetadataFile.loadImmutableBytes(intentPath, MAX_PLAN_LENGTH)); + plan.verify(manifest); + PathStateRootMetadata current = currentStore.current(); + if (same(current, plan.victims.get(0))) { + currentStore.switchToAncestor(plan.target, limits); + } else if (!same(current, plan.target)) { + throw new IOException("path-state retirement plan does not own CURRENT"); + } + retire(plan); + PathStateMetadataFile.deleteDurable(intentPath); + limits.verifyExisting(manifest); + return RecoveryAction.COMPLETED_RETIREMENT; + } + + private void retire(RetirementPlan plan) throws IOException { + for (PathStateRootMetadata victim : plan.victims) { + deleteLayer(victim); + faultHook.after(Stage.AFTER_LAYER_RETIRE); + } + } + + private void deleteLayer(PathStateRootMetadata victim) throws IOException { + Path directory = manifest.getLayerDirectory(victim.getBlockNumber(), victim.getBlockHash()); + if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + return; + } + if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS) + || Files.isSymbolicLink(directory)) { + throw new IOException("path-state retirement target is not a direct directory"); + } + Path metadata = directory.resolve(PathStateCurrentStore.METADATA_FILE); + if (Files.exists(metadata, LinkOption.NOFOLLOW_LINKS)) { + PathStateMetadataFile.requireExact(metadata, victim); + } + List entries = new ArrayList<>(); + try (Stream paths = Files.walk(directory)) { + paths.forEach(entries::add); + } + for (Path entry : entries) { + if (Files.isSymbolicLink(entry)) { + throw new IOException("path-state retirement refuses symbolic links: " + entry); + } + } + entries.sort(Comparator.reverseOrder()); + for (Path entry : entries) { + Files.deleteIfExists(entry); + faultHook.after(Stage.AFTER_LAYER_ENTRY_DELETE); + } + PathStateMetadataFile.syncDirectory(manifest.getLayersDirectory()); + } + + public enum RecoveryAction { + NONE, + COMPLETED_RETIREMENT + } + + enum Stage { + AFTER_INTENT, + AFTER_CURRENT, + AFTER_LAYER_ENTRY_DELETE, + AFTER_LAYER_RETIRE, + AFTER_RETIRE + } + + @FunctionalInterface + interface FaultHook { + + void after(Stage stage) throws IOException; + } + + private static final class RetirementPlan { + + private final PathStateRootMetadata target; + private final List victims; + + private RetirementPlan(PathStateRootMetadata target, + List victims) { + this.target = Objects.requireNonNull(target, "target"); + this.victims = new ArrayList<>(Objects.requireNonNull(victims, "victims")); + if (this.victims.isEmpty()) { + throw new IllegalArgumentException("path-state retirement plan requires victims"); + } + } + + private void verify(PathStateStoreManifest manifest) throws IOException { + requireFormat(target, manifest); + PathStateRootMetadata child = null; + for (PathStateRootMetadata victim : victims) { + requireFormat(victim, manifest); + if (victim.getKind() != Kind.LAYER) { + throw new IOException("path-state retirement victim is not a layer"); + } + if (child != null && !isParent(victim, child)) { + throw new IOException("path-state retirement suffix is not contiguous"); + } + child = victim; + } + if (!isParent(target, victims.get(victims.size() - 1))) { + throw new IOException("path-state retirement target does not precede its suffix"); + } + } + + private byte[] encode() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(0); + output.writeInt(victims.size()); + writeMetadata(output, target); + for (PathStateRootMetadata victim : victims) { + writeMetadata(output, victim); + } + output.flush(); + byte[] payload = bytes.toByteArray(); + ByteBuffer.wrap(payload).putInt(8, payload.length + Integer.BYTES); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(payload); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + byte[] encoded = bytes.toByteArray(); + if (encoded.length > MAX_PLAN_LENGTH) { + throw new IllegalArgumentException("path-state retirement plan is too large"); + } + return encoded; + } catch (IOException impossible) { + throw new IllegalStateException("in-memory retirement plan encoding failed", impossible); + } + } + + private static RetirementPlan decode(byte[] encoded) throws IOException { + byte[] value = Arrays.copyOf(Objects.requireNonNull(encoded, "encoded"), encoded.length); + if (value.length <= Integer.BYTES || value.length > MAX_PLAN_LENGTH) { + throw new IOException("path-state retirement plan length is invalid"); + } + byte[] payload = Arrays.copyOf(value, value.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(value, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new IOException("path-state retirement plan checksum mismatch"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(value))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != value.length) { + throw new IOException("unsupported path-state retirement plan header"); + } + int count = input.readInt(); + if (count <= 0 || count > PathStateCurrentStore.MAX_VALIDATION_LAYERS) { + throw new IOException("path-state retirement plan count is invalid"); + } + PathStateRootMetadata target = readMetadata(input); + List victims = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + victims.add(readMetadata(input)); + } + if (input.available() != Integer.BYTES) { + throw new IOException("path-state retirement plan payload mismatch"); + } + return new RetirementPlan(target, victims); + } catch (IllegalArgumentException invalid) { + throw new IOException("path-state retirement plan metadata is corrupt", invalid); + } + } + + private static void writeMetadata(DataOutputStream output, PathStateRootMetadata metadata) + throws IOException { + byte[] encoded = metadata.encode(); + output.writeInt(encoded.length); + output.write(encoded); + } + + private static PathStateRootMetadata readMetadata(DataInputStream input) throws IOException { + int length = input.readInt(); + if (length <= 0 || length > input.available() - Integer.BYTES) { + throw new IOException("path-state retirement metadata length is invalid"); + } + byte[] encoded = new byte[length]; + input.readFully(encoded); + return PathStateRootMetadata.decode(encoded); + } + + private static void requireFormat(PathStateRootMetadata metadata, + PathStateStoreManifest manifest) throws IOException { + if (!Arrays.equals(metadata.getFormatDigest(), manifest.getIdentityDigest())) { + throw new IOException("path-state retirement metadata identity mismatch"); + } + } + + private static boolean isParent(PathStateRootMetadata parent, + PathStateRootMetadata child) { + return child.getBlockNumber() == parent.getBlockNumber() + 1 + && Arrays.equals(child.getParentHash(), parent.getBlockHash()) + && Arrays.equals(child.getParentStateRoot(), parent.getStateRoot()); + } + } + + private static boolean same(PathStateRootMetadata left, PathStateRootMetadata right) { + return Arrays.equals(left.encode(), right.encode()); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java index 3dd715f82e8..c13ad9d0284 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java @@ -31,6 +31,19 @@ static PathStateRootMetadata load(Path path) throws IOException { } } + static byte[] loadImmutableBytes(Path path, int maxLength) throws IOException { + Path target = Objects.requireNonNull(path, "path"); + if (!Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state immutable record is missing or not a regular file: " + + target); + } + long length = Files.size(target); + if (length <= 0 || length > maxLength) { + throw new IOException("path-state immutable record length is invalid: " + target); + } + return Files.readAllBytes(target); + } + /** Publishes once; an exact existing record is an idempotent retry, not a rewrite. */ static void publishImmutable(Path path, PathStateRootMetadata metadata) throws IOException { Path target = Objects.requireNonNull(path, "path"); @@ -42,6 +55,19 @@ static void publishImmutable(Path path, PathStateRootMetadata metadata) throws I publish(target, encoded, false, temporary -> { }); } + static void publishImmutableBytes(Path path, byte[] encoded) throws IOException { + Path target = Objects.requireNonNull(path, "path"); + byte[] value = Arrays.copyOf(Objects.requireNonNull(encoded, "encoded"), encoded.length); + if (value.length == 0) { + throw new IllegalArgumentException("path-state immutable record must not be empty"); + } + if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) { + requireExactBytes(target, value); + return; + } + publish(target, value, false, temporary -> { }); + } + static void replaceCurrent(Path path, PathStateRootMetadata metadata) throws IOException { replaceCurrent(path, metadata, temporary -> { }); } @@ -80,6 +106,13 @@ private static void requireExact(Path path, byte[] expected) throws IOException } } + private static void requireExactBytes(Path path, byte[] expected) throws IOException { + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) + || !Arrays.equals(expected, Files.readAllBytes(path))) { + throw new IOException("immutable path-state record identity mismatch: " + path); + } + } + private static void publish(Path target, byte[] encoded, boolean replace, FaultHook faultHook) throws IOException { Path directory = Objects.requireNonNull(target.getParent(), "metadata directory"); @@ -118,7 +151,7 @@ private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IO } } - private static void syncDirectory(Path directory) throws IOException { + static void syncDirectory(Path directory) throws IOException { try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { channel.force(true); } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerRetirementTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerRetirementTest.java new file mode 100644 index 00000000000..b9c4c31bee0 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerRetirementTest.java @@ -0,0 +1,193 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.arch.Arch; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateLayerRetirement.RecoveryAction; +import org.tron.core.db2.stateroot.PathStateLayerRetirement.Stage; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateLayerRetirementTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void retiredForkReleasesLayerBudgetForCanonicalSibling() throws Exception { + for (Engine engine : availableEngines()) { + Fixture fixture = fixture("bounded-fork-" + engine, engine); + PathStateLayerLimits limits = new PathStateLayerLimits(2, Long.MAX_VALUE); + PathStateRootMetadata firstA = append(fixture.manifest, fixture.base, 101, 11, limits); + PathStateRootMetadata secondA = append(fixture.manifest, firstA, 102, 13, limits); + + PathStateLayerRetirement retirement = new PathStateLayerRetirement( + fixture.manifest, limits); + assertArrayEquals(fixture.base.encode(), + retirement.switchToAncestor(fixture.base).encode()); + assertFalse(Files.exists(layerDirectory(fixture.manifest, firstA))); + assertFalse(Files.exists(layerDirectory(fixture.manifest, secondA))); + + PathStateRootMetadata firstB = append(fixture.manifest, fixture.base, 101, 21, limits); + PathStateRootMetadata secondB = append(fixture.manifest, firstB, 102, 23, limits); + assertArrayEquals(secondB.encode(), new PathStateCurrentStore(fixture.manifest) + .current().encode()); + assertEquals(RecoveryAction.NONE, retirement.recover()); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openCurrent(fixture.manifest)) { + PathStateRoot restored = stores.createRoot(); + assertArrayEquals(secondB.getStateRoot(), restored.rootHash()); + restored.verifyNodeStores(); + } + } + } + + @Test + public void everyRetirementFaultRecoversIdempotently() throws Exception { + for (Engine engine : availableEngines()) { + for (Stage failure : Stage.values()) { + Fixture fixture = fixture("recover-" + engine + "-" + failure, engine); + PathStateLayerLimits limits = new PathStateLayerLimits(10, Long.MAX_VALUE); + PathStateRootMetadata first = append(fixture.manifest, fixture.base, 101, 11, limits); + PathStateRootMetadata second = append(fixture.manifest, first, 102, 13, limits); + PathStateLayerRetirement retirement = new PathStateLayerRetirement( + fixture.manifest, limits, stage -> { + if (stage == failure) { + throw new IOException("injected after " + stage); + } + }); + + assertThrows(IOException.class, () -> retirement.switchToAncestor(fixture.base)); + PathStateLayerRetirement recovery = new PathStateLayerRetirement( + fixture.manifest, limits); + RecoveryAction expected = failure == Stage.AFTER_RETIRE + ? RecoveryAction.NONE : RecoveryAction.COMPLETED_RETIREMENT; + assertEquals(expected, recovery.recover()); + assertEquals(RecoveryAction.NONE, recovery.recover()); + assertArrayEquals(fixture.base.encode(), new PathStateCurrentStore(fixture.manifest) + .current().encode()); + assertFalse(Files.exists(layerDirectory(fixture.manifest, first))); + assertFalse(Files.exists(layerDirectory(fixture.manifest, second))); + assertFalse(Files.exists(fixture.manifest.getDirectory() + .resolve(PathStateLayerRetirement.INTENT_FILE))); + } + } + } + + @Test + public void corruptRetirementIntentFailsClosedBeforeCurrentMoves() throws Exception { + Fixture fixture = fixture("corrupt-intent", Engine.ROCKSDB); + PathStateLayerLimits limits = new PathStateLayerLimits(10, Long.MAX_VALUE); + PathStateRootMetadata first = append(fixture.manifest, fixture.base, 101, 11, limits); + PathStateRootMetadata second = append(fixture.manifest, first, 102, 13, limits); + PathStateLayerRetirement retirement = new PathStateLayerRetirement(fixture.manifest, limits, + stage -> { + if (stage == Stage.AFTER_INTENT) { + throw new IOException("injected after intent"); + } + }); + assertThrows(IOException.class, () -> retirement.switchToAncestor(fixture.base)); + Path intent = fixture.manifest.getDirectory().resolve(PathStateLayerRetirement.INTENT_FILE); + byte[] corrupt = Files.readAllBytes(intent); + corrupt[corrupt.length - 1] ^= 1; + Files.write(intent, corrupt); + + assertThrows(IOException.class, + new PathStateLayerRetirement(fixture.manifest, limits)::recover); + assertArrayEquals(second.encode(), new PathStateCurrentStore(fixture.manifest) + .current().encode()); + assertTrue(Files.exists(layerDirectory(fixture.manifest, first))); + assertTrue(Files.exists(layerDirectory(fixture.manifest, second))); + } + + @Test + public void retirementCannotDeleteAfterAnotherCanonicalBranchAdvances() throws Exception { + Fixture fixture = fixture("stale-retirement", Engine.ROCKSDB); + PathStateLayerLimits limits = new PathStateLayerLimits(10, Long.MAX_VALUE); + PathStateRootMetadata firstA = append(fixture.manifest, fixture.base, 101, 11, limits); + PathStateRootMetadata secondA = append(fixture.manifest, firstA, 102, 13, limits); + PathStateLayerRetirement retirement = new PathStateLayerRetirement(fixture.manifest, limits, + stage -> { + if (stage == Stage.AFTER_INTENT) { + throw new IOException("injected after intent"); + } + }); + assertThrows(IOException.class, () -> retirement.switchToAncestor(fixture.base)); + + new PathStateCurrentStore(fixture.manifest).switchToAncestor(fixture.base, limits); + PathStateRootMetadata firstB = append(fixture.manifest, fixture.base, 101, 21, limits); + assertThrows(IOException.class, + new PathStateLayerRetirement(fixture.manifest, limits)::recover); + assertArrayEquals(firstB.encode(), new PathStateCurrentStore(fixture.manifest) + .current().encode()); + assertTrue(Files.exists(layerDirectory(fixture.manifest, firstA))); + assertTrue(Files.exists(layerDirectory(fixture.manifest, secondA))); + } + + private Fixture fixture(String name, Engine engine) throws Exception { + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen( + new File(temporaryFolder.getRoot(), name).toPath(), engine); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4}))); + PathStateRootMetadata base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), root.rootHash(), bytes(3)); + new PathStateBasePublication(manifest).publish(stores, base); + return new Fixture(manifest, base); + } + } + + private static PathStateRootMetadata append(PathStateStoreManifest manifest, + PathStateRootMetadata parent, long blockNumber, int seed, PathStateLayerLimits limits) + throws Exception { + try (PathStateLayer layer = PathStateLayer.begin(manifest, parent, blockNumber, bytes(seed), + parent.getBlockHash(), blockNumber * 3, P66Phase.P66_ON, bytes(seed + 1), limits)) { + layer.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{(byte) seed}))); + return layer.commit(); + } + } + + private static Path layerDirectory(PathStateStoreManifest manifest, + PathStateRootMetadata metadata) { + return manifest.getLayerDirectory(metadata.getBlockNumber(), metadata.getBlockHash()); + } + + private static Engine[] availableEngines() { + return Arch.isArm64() ? new Engine[]{Engine.ROCKSDB} + : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + private static final class Fixture { + + private final PathStateStoreManifest manifest; + private final PathStateRootMetadata base; + + private Fixture(PathStateStoreManifest manifest, PathStateRootMetadata base) { + this.manifest = manifest; + this.base = base; + } + } +} From 7fcb36b39b4c4a2c4df99e0186ed8458eae48d91 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 22 Aug 2026 18:17:45 +0800 Subject: [PATCH 057/161] test(chainbase): guard current-only path state --- .../PathStateCurrentOnlyContractTest.java | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentOnlyContractTest.java diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentOnlyContractTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentOnlyContractTest.java new file mode 100644 index 00000000000..015ed0016bc --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentOnlyContractTest.java @@ -0,0 +1,168 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.io.File; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.arch.Arch; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateCurrentOnlyContractTest { + + private static final Class[] DURABLE_API = new Class[]{ + PathStateBasePublication.class, + PathStateCurrentStore.class, + PathStateLayer.class, + PathStateLayerLimits.class, + PathStateLayerPublication.class, + PathStateLayerRetirement.class, + PathStateNodeStoreSet.class, + PathStateRootMetadata.class, + PathStateStoreManifest.class + }; + + private static final String[] FORBIDDEN_API_TOKENS = new String[]{ + "getrootat", "histor", "proof", "segment" + }; + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void durablePublicApiExposesCurrentAuthorityButNoHistoricalService() throws Exception { + assertEquals(setOf("appendLayer", "current", "isInitialized", "publishBase"), + publicMethodNames(PathStateCurrentStore.class)); + assertEquals(setOf("close", "commit", "createRoot", "getDirectory", "getProgress", + "openBase", "openCurrent", "openLayer"), + publicMethodNames(PathStateNodeStoreSet.class)); + assertEquals(setOf("recover", "switchToAncestor"), + publicMethodNames(PathStateLayerRetirement.class)); + for (Class type : DURABLE_API) { + for (Method method : type.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + String name = method.getName().toLowerCase(java.util.Locale.ROOT); + for (String forbidden : FORBIDDEN_API_TOKENS) { + assertFalse(type.getSimpleName() + "." + method.getName(), + name.contains(forbidden)); + } + } + } + } + + assertFalse(Modifier.isPublic(PathStateNodeStoreSet.class.getDeclaredMethod( + "openPublished", PathStateStoreManifest.class, PathStateRootMetadata.class) + .getModifiers())); + assertFalse(Modifier.isPublic(PathStateCurrentStore.class.getDeclaredMethod( + "layersAboveAncestor", PathStateRootMetadata.class, PathStateLayerLimits.class) + .getModifiers())); + assertFalse(Modifier.isPublic(PathStateCurrentStore.class.getDeclaredMethod( + "switchToAncestor", PathStateRootMetadata.class, PathStateLayerLimits.class) + .getModifiers())); + } + + @Test + public void completedForkLifecycleKeepsExactCurrentOnlyRootLayout() throws Exception { + for (Engine engine : availableEngines()) { + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen( + new File(temporaryFolder.getRoot(), "layout-" + engine).toPath(), engine); + PathStateRootMetadata base = publishBase(manifest); + PathStateLayerLimits limits = new PathStateLayerLimits(2, Long.MAX_VALUE); + PathStateRootMetadata firstA = append(manifest, base, 101, 11, limits); + PathStateRootMetadata secondA = append(manifest, firstA, 102, 13, limits); + new PathStateLayerRetirement(manifest, limits).switchToAncestor(base); + PathStateRootMetadata current = append(manifest, base, 101, 21, limits); + + assertEquals(setOf("CURRENT", "MANIFEST", "base", "layers"), + children(manifest.getDirectory())); + assertEquals(setOf("METADATA", "nodes"), children(manifest.getBaseDirectory())); + assertEquals(Collections.singleton(layerDirectory(manifest, current) + .getFileName().toString()), + children(manifest.getLayersDirectory())); + assertEquals(setOf("METADATA", "nodes"), children( + manifest.getLayerDirectory(current.getBlockNumber(), current.getBlockHash()))); + assertFalse(Files.exists(manifest.getDirectory().resolve("history"))); + assertFalse(Files.exists(manifest.getDirectory().resolve("segments"))); + assertFalse(Files.exists(manifest.getDirectory().resolve("index"))); + assertFalse(Files.exists(manifest.getDirectory().resolve("proofs"))); + assertFalse(Files.exists(layerDirectory(manifest, firstA))); + assertFalse(Files.exists(layerDirectory(manifest, secondA))); + assertArrayEquals(current.encode(), new PathStateCurrentStore(manifest).current().encode()); + } + } + + private static PathStateRootMetadata publishBase(PathStateStoreManifest manifest) + throws Exception { + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4}))); + PathStateRootMetadata base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), root.rootHash(), bytes(3)); + new PathStateBasePublication(manifest).publish(stores, base); + return base; + } + } + + private static PathStateRootMetadata append(PathStateStoreManifest manifest, + PathStateRootMetadata parent, long blockNumber, int seed, PathStateLayerLimits limits) + throws Exception { + try (PathStateLayer layer = PathStateLayer.begin(manifest, parent, blockNumber, bytes(seed), + parent.getBlockHash(), blockNumber * 3, P66Phase.P66_ON, bytes(seed + 1), limits)) { + layer.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{(byte) seed}))); + return layer.commit(); + } + } + + private static Set children(Path directory) throws Exception { + try (Stream paths = Files.list(directory)) { + return paths.map(path -> path.getFileName().toString()).collect( + Collectors.toCollection(TreeSet::new)); + } + } + + private static Set setOf(String... values) { + return new TreeSet<>(Arrays.asList(values)); + } + + private static Set publicMethodNames(Class type) { + return Arrays.stream(type.getDeclaredMethods()) + .filter(method -> Modifier.isPublic(method.getModifiers())) + .map(Method::getName) + .collect(Collectors.toCollection(TreeSet::new)); + } + + private static Path layerDirectory(PathStateStoreManifest manifest, + PathStateRootMetadata metadata) { + return manifest.getLayerDirectory(metadata.getBlockNumber(), metadata.getBlockHash()); + } + + private static Engine[] availableEngines() { + return Arch.isArm64() ? new Engine[]{Engine.ROCKSDB} + : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } +} From bba3e312de35dd321bf9ea7e6fc6d42fc11b90cf Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 25 Aug 2026 10:04:29 +0800 Subject: [PATCH 058/161] feat(chainbase): integrate native archive lifecycle Replace participant-owned recovery and forward projection with Chainbase checkpoint WAL bindings. Publish committed history, all-Store serving generations, and request-owned historical reads through the Manager lifecycle. --- .../archive/AccountAssetArchiveProjector.java | 17 +- .../AccountAssetBlockProjectionBridge.java | 244 -------- .../AccountAssetForwardMutationManifest.java | 193 ------ .../AccountAssetForwardMutationRecorder.java | 246 -------- .../archive/AccountAssetForwardProjector.java | 67 --- ...AccountAssetPreparedBlockPayloadOwner.java | 191 ------ .../AccountAssetTargetActivationResolver.java | 89 --- .../core/db2/archive/ArchiveBaseManifest.java | 2 +- .../ArchiveBlockForwardMutationCapture.java | 106 ---- .../ArchiveBlockForwardMutationLimits.java | 44 -- .../archive/ArchiveBlockForwardPayload.java | 38 -- .../ArchiveBlockProjectionPreparer.java | 10 - .../db2/archive/ArchiveBootstrapAnchor.java | 168 +++++- .../ArchiveCommittedPrefixPublisher.java | 10 + .../db2/archive/ArchiveGenerationCapsule.java | 22 +- .../db2/archive/ArchiveHistoryScanAnchor.java | 197 +++++++ .../db2/archive/ArchiveHistoryWriter.java | 80 +-- .../core/db2/archive/ArchiveParticipant.java | 11 - .../archive/ArchiveParticipantBatchFile.java | 186 ------ .../ArchiveParticipantMutationBatch.java | 117 ---- ...hiveParticipantMutationBatchCollector.java | 136 ----- .../ArchiveParticipantRecoveryStorage.java | 278 --------- .../archive/ArchiveReaderHeadPublisher.java | 66 --- .../archive/ArchiveReaderPublicationGate.java | 197 ------- .../ArchiveRecoveryAuthorityScanner.java | 165 ------ .../db2/archive/ArchiveRecoveryExecutor.java | 121 ---- .../db2/archive/ArchiveRecoveryPlanner.java | 181 ------ .../db2/archive/ArchiveRecoveryScanner.java | 127 ---- .../db2/archive/ArchiveRuntimeAttachment.java | 51 +- .../ArchiveTargetApplyCoordinator.java | 196 ------ .../ArchiveTargetMutationPlanBuilder.java | 71 --- .../db2/archive/ArchiveTruncationIntent.java | 4 +- .../archive/ArchiveTruncationRecovery.java | 2 +- .../core/db2/archive/ArchiveWalBinding.java | 178 ++++++ .../db2/archive/ArchiveWalBindingCodec.java | 99 ++++ .../archive/ArchiveWalStartupValidator.java | 97 +++ .../DurableHistoryMarkerRangeEvidence.java | 6 - .../core/db2/archive/HistoryCommitStore.java | 10 +- .../core/db2/archive/HistoryIndexStore.java | 6 +- .../core/db2/archive/HistorySegmentStore.java | 6 +- .../archive/LatestStateGenerationAdapter.java | 102 ++++ ...testStateGenerationCoordinatorFactory.java | 45 +- .../archive/LevelDbArchiveParticipant.java | 158 ----- .../PersistentCommittedHistoryReader.java | 4 +- .../PersistentServingKeyIndexCatalog.java | 59 +- .../archive/RocksDbArchiveParticipant.java | 164 ------ .../archive/SnapshotOldValueCollector.java | 57 +- .../db2/archive/StateArchiveRuntimeOwner.java | 556 +++++++++++------- .../tron/core/db2/core/SnapshotManager.java | 401 +++---------- .../main/java/org/tron/core/db/Manager.java | 119 ++-- 50 files changed, 1606 insertions(+), 4094 deletions(-) delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardProjector.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetPreparedBlockPayloadOwner.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationLimits.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardPayload.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockProjectionPreparer.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveCommittedPrefixPublisher.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryScanAnchor.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipant.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantBatchFile.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryPlanner.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalBinding.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalBindingCodec.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalStartupValidator.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java index ba2423b5e86..add27c69f5f 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java @@ -9,7 +9,6 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; -import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; import org.tron.core.db2.archive.P66AccountAssetCodec.AssetRow; import org.tron.core.db2.archive.P66AccountAssetCodec.DecodedAssetRow; import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; @@ -58,14 +57,14 @@ Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue r assetKeys.addAll(oldAssets.keySet()); assetKeys.addAll(postAssets.keySet()); List reverseAssets = new ArrayList<>(); - List forwardAssets = new ArrayList<>(); + List changedAssetRows = new ArrayList<>(); for (WrappedByteArray assetKey : assetKeys) { byte[] oldValue = oldAssets.get(assetKey); byte[] postValue = postAssets.get(assetKey); if (!Arrays.equals(oldValue, postValue)) { reverseAssets.add(new BlockReverseDiff.Entry(assetKey.getBytes(), OldValue.fromNullable(oldValue))); - forwardAssets.add(new AssetMutation(assetKey.getBytes(), postValue == null + changedAssetRows.add(new AssetRow(assetKey.getBytes(), postValue == null ? BlockChangeView.PostValue.absent() : BlockChangeView.PostValue.present(postValue))); } @@ -78,14 +77,10 @@ Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue r ? BlockChangeView.PostValue.absent() : BlockChangeView.PostValue.present(canonicalAccount(accountKey, postAccount, projectPost)); if (canonicalPost.isPresent()) { - List rows = new ArrayList<>(forwardAssets.size()); - for (AssetMutation mutation : forwardAssets) { - rows.add(new AssetRow(mutation.getPhysicalRawKey(), mutation.getPostValue())); - } codec.requireCanonicalLayout(phase(postAccount, projectPost), - accountKey, canonicalPost.getValue(), rows); + accountKey, canonicalPost.getValue(), changedAssetRows); } - return new Projection(canonicalOld, canonicalPost, reverseAssets, forwardAssets); + return new Projection(canonicalOld, canonicalPost, reverseAssets); } boolean requiresOldPhysicalAssets(byte[] rawOld, BlockChangeView.PostValue rawPost) { @@ -168,14 +163,12 @@ static final class Projection { final OldValue oldAccount; final BlockChangeView.PostValue postAccount; final List reverseAssets; - final List forwardAssets; private Projection(OldValue oldAccount, BlockChangeView.PostValue postAccount, - List reverseAssets, List forwardAssets) { + List reverseAssets) { this.oldAccount = oldAccount; this.postAccount = postAccount; this.reverseAssets = Collections.unmodifiableList(new ArrayList<>(reverseAssets)); - this.forwardAssets = Collections.unmodifiableList(new ArrayList<>(forwardAssets)); } } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java deleted file mode 100644 index 04ca5941dd3..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridge.java +++ /dev/null @@ -1,244 +0,0 @@ -package org.tron.core.db2.archive; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import org.tron.core.db2.archive.AccountAssetForwardMutationManifest.Entry; -import org.tron.core.db2.archive.BlockChangeView.Change; -import org.tron.core.db2.archive.BlockChangeView.DatabaseChanges; -import org.tron.core.db2.archive.BlockChangeView.PostValue; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; -import org.tron.core.db2.common.WrappedByteArray; - -/** - * Standalone bridge which prepares reverse and forward projections before a durable history marker - * exists, then seals the forward projection against that marker exactly once. - */ -public final class AccountAssetBlockProjectionBridge { - - private final AccountAssetArchiveProjector projector; - private final AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource; - private final List participants; - - public AccountAssetBlockProjectionBridge(AccountAssetArchiveProjector projector, - AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource) { - this.projector = Objects.requireNonNull(projector, "projector"); - this.oldPhysicalAssetsSource = Objects.requireNonNull(oldPhysicalAssetsSource, - "oldPhysicalAssetsSource"); - participants = ArchiveParticipantDescriptor.current().getParticipants(); - } - - public PreparedBlockProjection prepare(BlockChangeView view, - TargetAssetOptimization activation) { - BlockChangeView input = Objects.requireNonNull(view, "view"); - TargetAssetOptimization targetActivation = Objects.requireNonNull(activation, "activation"); - validateBeforeProjection(input, targetActivation); - - List groups = new ArrayList<>(); - List accountAssetEntries = new ArrayList<>(); - List forwardEntries = new ArrayList<>(); - for (DatabaseChanges database : input.getDatabases()) { - List entries = new ArrayList<>(); - for (Change change : database.getChanges()) { - byte[] key = change.getKey(); - OldValue oldValue = OldValue.fromNullable(database.getPrevious(key)); - PostValue postValue = change.getPostValue(); - if (AccountAssetArchiveProjector.ACCOUNT_DB.equals(database.getDbName())) { - Map oldPhysicalAssets = Collections.emptyMap(); - if (projector.requiresOldPhysicalAssets( - oldValue.isPresent() ? oldValue.getValue() : null, postValue)) { - oldPhysicalAssets = AccountAssetOldPhysicalAssetsSource.captureRequired( - oldPhysicalAssetsSource, key); - } - AccountAssetArchiveProjector.Projection projection = projector.project(key, - oldValue.isPresent() ? oldValue.getValue() : null, postValue, - targetActivation.isEnabled(), oldPhysicalAssets); - oldValue = projection.oldAccount; - postValue = projection.postAccount; - accountAssetEntries.addAll(projection.reverseAssets); - forwardEntries.add(new Entry(key, change.getPostValue(), projection.postAccount, - projection.forwardAssets)); - } - if (!sameLogicalValue(oldValue, postValue)) { - entries.add(new BlockReverseDiff.Entry(key, oldValue)); - } - } - if (!entries.isEmpty()) { - groups.add(new BlockReverseDiff.DbGroup(database.getDbName(), entries)); - } - } - if (!accountAssetEntries.isEmpty()) { - groups.add(new BlockReverseDiff.DbGroup(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, - accountAssetEntries)); - } - - BlockReverseDiff reverse = new BlockReverseDiff(input.getMeta(), groups); - return new PreparedBlockProjection(input, participants, reverse, forwardEntries, - targetActivation.getPhase()); - } - - private void validateBeforeProjection(BlockChangeView view, - TargetAssetOptimization activation) { - if (!view.getMeta().equals(activation.getMeta())) { - throw new ArchivePersistenceException("Block projection activation identity mismatch"); - } - List actual = new ArrayList<>(); - for (DatabaseChanges database : view.getDatabases()) { - actual.add(database.getDbName()); - if (AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(database.getDbName()) - && !database.getChanges().isEmpty()) { - throw new ArchivePersistenceException( - "AccountAsset block projection requires one derived physical mutation source"); - } - } - Collections.sort(actual); - List captured = new ArrayList<>(participants); - captured.remove(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB); - if (!actual.equals(captured)) { - throw new ArchivePersistenceException( - "Block projection source set mismatch: expected=" + captured + ", actual=" + actual); - } - } - - private static boolean sameLogicalValue(OldValue oldValue, PostValue postValue) { - return oldValue.isPresent() == postValue.isPresent() - && (!oldValue.isPresent() || Arrays.equals(oldValue.getValue(), postValue.getValue())); - } - - /** Target-bound proposal-66 state; mismatched identity is rejected before any Store read. */ - public static final class TargetAssetOptimization { - private final BlockSnapshotMeta meta; - private final Phase phase; - - private TargetAssetOptimization(BlockSnapshotMeta meta, Phase phase) { - this.meta = Objects.requireNonNull(meta, "meta"); - this.phase = Objects.requireNonNull(phase, "phase"); - } - - public static TargetAssetOptimization forTarget(BlockSnapshotMeta meta, boolean enabled) { - return forTarget(meta, enabled ? Phase.P66_ON : Phase.P66_OFF); - } - - static TargetAssetOptimization forTarget(BlockSnapshotMeta meta, Phase phase) { - return new TargetAssetOptimization(meta, phase); - } - - BlockSnapshotMeta getMeta() { - return meta; - } - - boolean isEnabled() { - return phase != Phase.P66_OFF; - } - - Phase getPhase() { - return phase; - } - } - - /** Meta-bound projection owner which can be sealed or aborted exactly once. */ - public static final class PreparedBlockProjection { - private final BlockSnapshotMeta meta; - private final List participants; - private BlockChangeView view; - private BlockReverseDiff reverseDiff; - private List forwardEntries; - private final Phase targetPhase; - private State state = State.PREPARED; - - private PreparedBlockProjection(BlockChangeView view, List participants, - BlockReverseDiff reverseDiff, List forwardEntries, Phase targetPhase) { - this.view = Objects.requireNonNull(view, "view"); - this.meta = view.getMeta(); - this.participants = Collections.unmodifiableList(new ArrayList<>(participants)); - this.reverseDiff = Objects.requireNonNull(reverseDiff, "reverseDiff"); - this.forwardEntries = Collections.unmodifiableList(new ArrayList<>(forwardEntries)); - this.targetPhase = Objects.requireNonNull(targetPhase, "targetPhase"); - } - - public synchronized BlockReverseDiff getReverseDiff() { - if (state == State.ABORTED) { - throw new ArchivePersistenceException("Prepared block projection was aborted"); - } - return reverseDiff; - } - - synchronized BlockSnapshotMeta getMeta() { - return meta; - } - - synchronized void requirePreparedOwnership() { - requirePrepared(); - } - - public synchronized AccountAssetForwardMutationManifest seal(HistoryCommitMarker marker) { - AccountAssetForwardMutationManifest manifest = previewSeal(marker); - completeSeal(); - return manifest; - } - - public synchronized ArchiveBlockForwardPayload sealPayload(HistoryCommitMarker marker) { - ArchiveBlockForwardPayload payload = previewSealPayload(marker); - completeSeal(); - return payload; - } - - synchronized AccountAssetForwardMutationManifest previewSeal(HistoryCommitMarker marker) { - HistoryCommitMarker target = validateMarker(marker); - return new AccountAssetForwardMutationManifest(target, targetPhase, forwardEntries); - } - - synchronized ArchiveBlockForwardPayload previewSealPayload(HistoryCommitMarker marker) { - HistoryCommitMarker target = validateMarker(marker); - return new ArchiveBlockForwardPayload(target, view, - new AccountAssetForwardMutationManifest(target, targetPhase, forwardEntries)); - } - - synchronized HistoryCommitMarker validateMarker(HistoryCommitMarker marker) { - requirePrepared(); - HistoryCommitMarker target = Objects.requireNonNull(marker, "marker"); - if (!meta.equals(target.getMeta())) { - throw new ArchivePersistenceException("Prepared block projection target mismatch"); - } - if (!participants.equals(target.getDatabases())) { - throw new ArchivePersistenceException( - "Prepared block projection participant set mismatch"); - } - return target; - } - - synchronized void completeSeal() { - requirePrepared(); - view = null; - forwardEntries = Collections.emptyList(); - state = State.SEALED; - } - - synchronized boolean retainsCapturedView() { - return view != null; - } - - public synchronized void abort() { - requirePrepared(); - view = null; - reverseDiff = null; - forwardEntries = Collections.emptyList(); - state = State.ABORTED; - } - - private void requirePrepared() { - if (state != State.PREPARED) { - throw new ArchivePersistenceException("Prepared block projection is terminal"); - } - } - - private enum State { - PREPARED, - SEALED, - ABORTED - } - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java deleted file mode 100644 index 8ec8ff10dc2..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationManifest.java +++ /dev/null @@ -1,193 +0,0 @@ -package org.tron.core.db2.archive; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.TreeMap; -import java.util.TreeSet; -import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; -import org.tron.core.db2.archive.AccountAssetForwardProjector.Projection; -import org.tron.core.db2.archive.BlockChangeView.PostValue; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; - -/** Immutable one-shot account projection input bound to one exact committed target. */ -public final class AccountAssetForwardMutationManifest implements AccountAssetForwardProjector { - - private final byte[] encodedTarget; - private final String formatId; - private final Phase targetPhase; - private final TreeMap entries = new TreeMap<>(); - private final TreeSet consumed = new TreeSet<>(); - private boolean begun; - private boolean completed; - - public AccountAssetForwardMutationManifest(HistoryCommitMarker target, Phase targetPhase, - List entries) { - HistoryCommitMarker expectedTarget = Objects.requireNonNull(target, "target"); - if (!expectedTarget.getDatabases().equals(sortedParticipants())) { - throw new IllegalArgumentException("Manifest target must cover exact VERSIONED_STATE set"); - } - encodedTarget = new HistoryCommitMarkerCodec().encode(expectedTarget); - formatId = P66AccountAssetCodec.FORMAT_ID; - this.targetPhase = Objects.requireNonNull(targetPhase, "targetPhase"); - for (Entry entry : Objects.requireNonNull(entries, "entries")) { - if (entry == null) { - throw new IllegalArgumentException("Manifest contains null entry"); - } - Key key = new Key(entry.accountPhysicalKey); - if (this.entries.put(key, entry) != null) { - throw new IllegalArgumentException("Duplicate manifest account physical key"); - } - } - } - - String getFormatId() { - return formatId; - } - - Phase getTargetPhase() { - return targetPhase; - } - - @Override - public synchronized void begin(HistoryCommitMarker target, - List changedAccountPhysicalKeys) { - if (begun || completed) { - throw new ArchivePersistenceException("AccountAsset manifest is one-shot"); - } - if (!Arrays.equals(encodedTarget, - new HistoryCommitMarkerCodec().encode(Objects.requireNonNull(target, "target")))) { - throw new ArchivePersistenceException("AccountAsset manifest target identity mismatch"); - } - TreeMap changed = new TreeMap<>(); - for (byte[] key : Objects.requireNonNull(changedAccountPhysicalKeys, - "changedAccountPhysicalKeys")) { - if (changed.put(new Key(key), Boolean.TRUE) != null) { - throw new ArchivePersistenceException("Duplicate changed account physical key"); - } - } - if (!changed.keySet().equals(entries.keySet())) { - throw new ArchivePersistenceException( - "AccountAsset manifest does not exactly cover changed account keys"); - } - begun = true; - } - - @Override - public synchronized Projection project(byte[] accountPhysicalKey, - PostValue rawAccountPostValue) { - if (!begun || completed) { - throw new ArchivePersistenceException("AccountAsset manifest is not active"); - } - Entry entry = entries.get(new Key(accountPhysicalKey)); - if (entry == null) { - throw new ArchivePersistenceException("AccountAsset manifest entry is missing"); - } - Key key = new Key(accountPhysicalKey); - if (consumed.contains(key)) { - throw new ArchivePersistenceException("AccountAsset manifest entry was already consumed"); - } - if (!samePostValue(entry.rawAccountPostValue, - Objects.requireNonNull(rawAccountPostValue, "rawAccountPostValue"))) { - throw new ArchivePersistenceException("AccountAsset manifest raw account value mismatch"); - } - consumed.add(key); - return entry.projection; - } - - @Override - public synchronized void complete() { - if (!begun || completed) { - throw new ArchivePersistenceException("AccountAsset manifest is not active"); - } - if (consumed.size() != entries.size()) { - throw new ArchivePersistenceException("AccountAsset manifest contains unused entry"); - } - completed = true; - } - - private static boolean samePostValue(PostValue left, PostValue right) { - return left.isPresent() == right.isPresent() - && (!left.isPresent() || Arrays.equals(left.getValue(), right.getValue())); - } - - private static List sortedParticipants() { - return ArchiveParticipantDescriptor.current().getParticipants(); - } - - /** One changed account's exact raw input and canonical physical outputs. */ - public static final class Entry { - private final byte[] accountPhysicalKey; - private final PostValue rawAccountPostValue; - private final Projection projection; - - public Entry(byte[] accountPhysicalKey, PostValue rawAccountPostValue, - PostValue canonicalAccountPostValue, List assetMutations) { - this.accountPhysicalKey = Arrays.copyOf( - Objects.requireNonNull(accountPhysicalKey, "accountPhysicalKey"), - accountPhysicalKey.length); - this.rawAccountPostValue = Objects.requireNonNull(rawAccountPostValue, - "rawAccountPostValue"); - PostValue canonical = Objects.requireNonNull(canonicalAccountPostValue, - "canonicalAccountPostValue"); - if (rawAccountPostValue.isPresent() != canonical.isPresent()) { - throw new IllegalArgumentException( - "Raw and canonical account presence must match"); - } - List mutations = new ArrayList<>(Objects.requireNonNull(assetMutations, - "assetMutations")); - TreeMap assetKeys = new TreeMap<>(); - for (AssetMutation mutation : mutations) { - if (mutation == null) { - throw new IllegalArgumentException("Manifest entry contains null asset mutation"); - } - byte[] assetKey = mutation.getPhysicalRawKey(); - if (!strictlyExtends(this.accountPhysicalKey, assetKey)) { - throw new IllegalArgumentException( - "AccountAsset physical key does not belong to account"); - } - if (assetKeys.put(new Key(assetKey), Boolean.TRUE) != null) { - throw new IllegalArgumentException("Duplicate account-asset physical key"); - } - } - projection = new Projection(canonical, mutations); - } - - private static boolean strictlyExtends(byte[] prefix, byte[] value) { - if (value.length <= prefix.length) { - return false; - } - for (int i = 0; i < prefix.length; i++) { - if (prefix[i] != value[i]) { - return false; - } - } - return true; - } - } - - private static final class Key implements Comparable { - private final byte[] value; - - private Key(byte[] value) { - this.value = Arrays.copyOf(Objects.requireNonNull(value, "key"), value.length); - } - - @Override - public int compareTo(Key other) { - return BlockReverseDiff.compareUnsigned(value, other.value); - } - - @Override - public boolean equals(Object object) { - return object instanceof Key && Arrays.equals(value, ((Key) object).value); - } - - @Override - public int hashCode() { - return Arrays.hashCode(value); - } - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java deleted file mode 100644 index 8df6dfeea36..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardMutationRecorder.java +++ /dev/null @@ -1,246 +0,0 @@ -package org.tron.core.db2.archive; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.TreeMap; -import org.tron.core.db2.archive.AccountAssetForwardMutationManifest.Entry; -import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; -import org.tron.core.db2.archive.BlockChangeView.PostValue; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; - -/** Collects explicit execution-time AccountAsset events for one target without Store reads. */ -public final class AccountAssetForwardMutationRecorder { - - private final BlockSnapshotMeta targetMeta; - private final ArchiveBlockForwardMutationLimits limits; - private final Phase targetPhase; - private final TreeMap accounts = new TreeMap<>(); - private int accountCount; - private int assetMutationCount; - private long totalPayloadBytes; - private boolean sealed; - - public AccountAssetForwardMutationRecorder(BlockSnapshotMeta targetMeta, - Phase targetPhase, ArchiveBlockForwardMutationLimits limits) { - this.targetMeta = Objects.requireNonNull(targetMeta, "targetMeta"); - this.targetPhase = Objects.requireNonNull(targetPhase, "targetPhase"); - this.limits = Objects.requireNonNull(limits, "limits"); - } - - public synchronized void recordAccount(BlockSnapshotMeta eventMeta, - byte[] accountPhysicalKey, PostValue rawAccountPostValue, - PostValue canonicalAccountPostValue) { - requireOpenMeta(eventMeta); - PostValue raw = Objects.requireNonNull(rawAccountPostValue, "rawAccountPostValue"); - PostValue canonical = Objects.requireNonNull(canonicalAccountPostValue, - "canonicalAccountPostValue"); - if (raw.isPresent() != canonical.isPresent()) { - throw new ArchivePersistenceException( - "Raw and canonical account presence must match"); - } - Key key = new Key(accountPhysicalKey); - requireKeyLength(key.value.length); - AccountEvents current = accounts.get(key); - if (current != null && current.rawAccountPostValue != null) { - throw new ArchivePersistenceException("Duplicate AccountAsset account event"); - } - long rawBytes = valueLength(raw); - long canonicalBytes = valueLength(canonical); - AccountEvents account = current == null ? new AccountEvents(key.value) : current; - reserve(current == null, false, - (current == null ? key.value.length : 0L) + rawBytes + canonicalBytes); - if (current == null) { - accounts.put(key, account); - } - account.rawAccountPostValue = raw; - account.canonicalAccountPostValue = canonical; - } - - public synchronized void recordAssetPut(BlockSnapshotMeta eventMeta, - byte[] accountPhysicalKey, byte[] assetPhysicalKey, byte[] value) { - recordAsset(eventMeta, accountPhysicalKey, assetPhysicalKey, - PostValue.present(Objects.requireNonNull(value, "value"))); - } - - public synchronized void recordAssetDelete(BlockSnapshotMeta eventMeta, - byte[] accountPhysicalKey, byte[] assetPhysicalKey) { - recordAsset(eventMeta, accountPhysicalKey, assetPhysicalKey, PostValue.absent()); - } - - public synchronized AccountAssetForwardMutationManifest seal(HistoryCommitMarker sealTarget) { - requireOpen(); - HistoryCommitMarker committedTarget = Objects.requireNonNull(sealTarget, "sealTarget"); - if (!targetMeta.equals(committedTarget.getMeta())) { - throw new ArchivePersistenceException( - "AccountAsset recorder seal target meta mismatch"); - } - List entries = new ArrayList<>(); - for (AccountEvents account : accounts.values()) { - if (account.rawAccountPostValue == null) { - throw new ArchivePersistenceException( - "AccountAsset recorder contains incomplete account"); - } - entries.add(new Entry(account.accountPhysicalKey, account.rawAccountPostValue, - account.canonicalAccountPostValue, new ArrayList<>(account.assets.values()))); - } - AccountAssetForwardMutationManifest manifest = - new AccountAssetForwardMutationManifest(committedTarget, targetPhase, entries); - sealed = true; - clearPayload(); - return manifest; - } - - synchronized void discard() { - requireOpen(); - sealed = true; - clearPayload(); - } - - synchronized boolean isPayloadReleased() { - return accounts.isEmpty() - && accountCount == 0 - && assetMutationCount == 0 - && totalPayloadBytes == 0; - } - - synchronized void reserveView(BlockChangeView view) { - requireOpen(); - long additionalBytes = 0; - long remaining = limits.getMaxTotalPayloadBytes() - totalPayloadBytes; - for (BlockChangeView.DatabaseChanges database : view.getDatabases()) { - for (BlockChangeView.Change change : database.getChanges()) { - byte[] key = change.getKey(); - requireKeyLength(key.length); - long entryBytes = key.length + valueLength(change.getPostValue()); - if (entryBytes > remaining - additionalBytes) { - throw new ArchivePersistenceException( - "Block forward mutation payload exceeds total limit"); - } - additionalBytes += entryBytes; - } - } - reserve(false, false, additionalBytes); - } - - private void recordAsset(BlockSnapshotMeta eventMeta, byte[] accountPhysicalKey, - byte[] assetPhysicalKey, PostValue postValue) { - requireOpenMeta(eventMeta); - Key accountKey = new Key(accountPhysicalKey); - byte[] assetKey = Arrays.copyOf(Objects.requireNonNull(assetPhysicalKey, - "assetPhysicalKey"), assetPhysicalKey.length); - requireKeyLength(accountKey.value.length); - requireKeyLength(assetKey.length); - if (!strictlyExtends(accountKey.value, assetKey)) { - throw new ArchivePersistenceException( - "AccountAsset physical key does not belong to account"); - } - AccountEvents current = accounts.get(accountKey); - Key mutationKey = new Key(assetKey); - if (current != null && current.assets.containsKey(mutationKey)) { - throw new ArchivePersistenceException("Duplicate AccountAsset asset event"); - } - long valueBytes = valueLength(postValue); - AccountEvents account = current == null ? new AccountEvents(accountKey.value) : current; - AssetMutation mutation = new AssetMutation(assetKey, postValue); - reserve(current == null, true, - (current == null ? accountKey.value.length : 0L) + assetKey.length + valueBytes); - if (current == null) { - accounts.put(accountKey, account); - } - account.assets.put(mutationKey, mutation); - } - - private long valueLength(PostValue value) { - long length = value.isPresent() ? value.getValue().length : 0L; - if (length > limits.getMaxValueBytes()) { - throw new ArchivePersistenceException("Block forward mutation value exceeds limit"); - } - return length; - } - - private void requireKeyLength(int length) { - if (length > limits.getMaxKeyBytes()) { - throw new ArchivePersistenceException("Block forward mutation key exceeds limit"); - } - } - - private void reserve(boolean newAccount, boolean newAsset, long additionalBytes) { - if (newAccount && accountCount >= limits.getMaxAccounts()) { - throw new ArchivePersistenceException("Block forward mutation account count exceeds limit"); - } - if (newAsset && assetMutationCount >= limits.getMaxAssetMutations()) { - throw new ArchivePersistenceException("Block forward mutation asset count exceeds limit"); - } - long remaining = limits.getMaxTotalPayloadBytes() - totalPayloadBytes; - if (additionalBytes < 0 || additionalBytes > remaining) { - throw new ArchivePersistenceException("Block forward mutation payload exceeds total limit"); - } - if (newAccount) { - accountCount++; - } - if (newAsset) { - assetMutationCount++; - } - totalPayloadBytes += additionalBytes; - } - - private void requireOpenMeta(BlockSnapshotMeta eventMeta) { - requireOpen(); - if (!targetMeta.equals(Objects.requireNonNull(eventMeta, "eventMeta"))) { - throw new ArchivePersistenceException("AccountAsset recorder event meta mismatch"); - } - } - - private void requireOpen() { - if (sealed) { - throw new ArchivePersistenceException("AccountAsset recorder is already sealed"); - } - } - - private void clearPayload() { - accounts.clear(); - accountCount = 0; - assetMutationCount = 0; - totalPayloadBytes = 0; - } - - private static boolean strictlyExtends(byte[] prefix, byte[] value) { - if (value.length <= prefix.length) { - return false; - } - for (int i = 0; i < prefix.length; i++) { - if (prefix[i] != value[i]) { - return false; - } - } - return true; - } - - private static final class AccountEvents { - private final byte[] accountPhysicalKey; - private final TreeMap assets = new TreeMap<>(); - private PostValue rawAccountPostValue; - private PostValue canonicalAccountPostValue; - - private AccountEvents(byte[] accountPhysicalKey) { - this.accountPhysicalKey = Arrays.copyOf( - Objects.requireNonNull(accountPhysicalKey, "accountPhysicalKey"), - accountPhysicalKey.length); - } - } - - private static final class Key implements Comparable { - private final byte[] value; - - private Key(byte[] value) { - this.value = Arrays.copyOf(Objects.requireNonNull(value, "key"), value.length); - } - - @Override - public int compareTo(Key other) { - return BlockReverseDiff.compareUnsigned(value, other.value); - } - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardProjector.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardProjector.java deleted file mode 100644 index 096b37ea10c..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetForwardProjector.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.tron.core.db2.archive; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import org.tron.core.db2.archive.BlockChangeView.PostValue; - -/** Explicit no-scan contract for canonical account and physical account-asset post mutations. */ -@FunctionalInterface -public interface AccountAssetForwardProjector { - - /** Opens one target-bound projection pass and declares its exact changed-account keys. */ - default void begin(HistoryCommitMarker target, List changedAccountPhysicalKeys) { - } - - Projection project(byte[] accountPhysicalKey, PostValue rawAccountPostValue); - - /** Finalizes one projection pass after every declared account has been consumed. */ - default void complete() { - } - - /** One canonical account post value plus exact physical account-asset post mutations. */ - final class Projection { - private final PostValue accountPostValue; - private final List assetMutations; - - public Projection(PostValue accountPostValue, List assetMutations) { - this.accountPostValue = Objects.requireNonNull(accountPostValue, "accountPostValue"); - List copy = new ArrayList<>( - Objects.requireNonNull(assetMutations, "assetMutations")); - if (copy.contains(null)) { - throw new IllegalArgumentException("AccountAsset projection contains null mutation"); - } - this.assetMutations = Collections.unmodifiableList(copy); - } - - PostValue getAccountPostValue() { - return accountPostValue; - } - - List getAssetMutations() { - return assetMutations; - } - } - - /** Exact physical account-asset key and its present/absent post state. */ - final class AssetMutation { - private final byte[] physicalRawKey; - private final PostValue postValue; - - public AssetMutation(byte[] physicalRawKey, PostValue postValue) { - this.physicalRawKey = Arrays.copyOf( - Objects.requireNonNull(physicalRawKey, "physicalRawKey"), physicalRawKey.length); - this.postValue = Objects.requireNonNull(postValue, "postValue"); - } - - byte[] getPhysicalRawKey() { - return Arrays.copyOf(physicalRawKey, physicalRawKey.length); - } - - PostValue getPostValue() { - return postValue; - } - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetPreparedBlockPayloadOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetPreparedBlockPayloadOwner.java deleted file mode 100644 index ad165b28948..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetPreparedBlockPayloadOwner.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.tron.core.db2.archive; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.PreparedBlockProjection; - -/** Standalone ownership seam from one block layer to one immutable contiguous flush batch. */ -public final class AccountAssetPreparedBlockPayloadOwner { - - private static final Object OWNERSHIP_LOCK = new Object(); - - private final BlockSnapshotMeta meta; - private PreparedBlockProjection projection; - private State state = State.EMPTY; - - public AccountAssetPreparedBlockPayloadOwner(BlockSnapshotMeta meta) { - this.meta = Objects.requireNonNull(meta, "meta"); - } - - public void attach(PreparedBlockProjection prepared) { - synchronized (OWNERSHIP_LOCK) { - if (state != State.EMPTY) { - throw new ArchivePersistenceException("Block payload owner already left empty state"); - } - PreparedBlockProjection candidate = Objects.requireNonNull(prepared, "prepared"); - if (!meta.equals(candidate.getMeta())) { - throw new ArchivePersistenceException("Block payload owner target mismatch"); - } - candidate.requirePreparedOwnership(); - projection = candidate; - state = State.ATTACHED; - } - } - - public BlockReverseDiff getReverseDiff() { - synchronized (OWNERSHIP_LOCK) { - requireAttached(); - return projection.getReverseDiff(); - } - } - - public void discard() { - synchronized (OWNERSHIP_LOCK) { - requireAttached(); - projection.abort(); - projection = null; - state = State.DISCARDED; - } - } - - public boolean isAttachedTo(BlockSnapshotMeta expectedMeta) { - synchronized (OWNERSHIP_LOCK) { - return state == State.ATTACHED && meta.equals(expectedMeta); - } - } - - public static FrozenBatch freezeContiguous( - List owners) { - synchronized (OWNERSHIP_LOCK) { - List candidates = new ArrayList<>( - Objects.requireNonNull(owners, "owners")); - if (candidates.isEmpty()) { - throw new ArchivePersistenceException("Flush batch must contain at least one payload"); - } - Set unique = new HashSet<>(); - BlockSnapshotMeta previous = null; - for (AccountAssetPreparedBlockPayloadOwner owner : candidates) { - AccountAssetPreparedBlockPayloadOwner candidate = Objects.requireNonNull(owner, "owner"); - if (!unique.add(candidate)) { - throw new ArchivePersistenceException("Flush batch contains duplicate payload owner"); - } - candidate.requireAttached(); - if (previous != null && !isNext(previous, candidate.meta)) { - throw new ArchivePersistenceException("Flush batch payloads are not contiguous"); - } - previous = candidate.meta; - } - - List payloads = new ArrayList<>(); - for (AccountAssetPreparedBlockPayloadOwner owner : candidates) { - payloads.add(owner.transfer()); - } - return new FrozenBatch(payloads); - } - } - - private static boolean isNext(BlockSnapshotMeta previous, BlockSnapshotMeta current) { - return current.getEpoch() == previous.getEpoch() + 1 - && current.getBlockNumber() == previous.getBlockNumber() + 1 - && Arrays.equals(current.getParentHash(), previous.getBlockHash()); - } - - private PreparedBlockProjection transfer() { - requireAttached(); - PreparedBlockProjection transferred = projection; - projection = null; - state = State.TRANSFERRED; - return transferred; - } - - private void requireAttached() { - if (state != State.ATTACHED) { - throw new ArchivePersistenceException("Block payload owner is not attached"); - } - } - - private enum State { - EMPTY, - ATTACHED, - TRANSFERRED, - DISCARDED - } - - /** Immutable flush-range owner; a marker mismatch does not consume any block payload. */ - public static final class FrozenBatch { - private final List expectedMetas; - private List payloads; - private BatchState state = BatchState.FROZEN; - - private FrozenBatch(List payloads) { - this.payloads = Collections.unmodifiableList(new ArrayList<>(payloads)); - List metas = new ArrayList<>(payloads.size()); - for (PreparedBlockProjection payload : payloads) { - metas.add(payload.getMeta()); - } - expectedMetas = Collections.unmodifiableList(metas); - } - - public synchronized List getExpectedMetas() { - requireFrozen(); - return expectedMetas; - } - - public synchronized boolean contains(BlockSnapshotMeta meta) { - return expectedMetas.contains(Objects.requireNonNull(meta, "meta")); - } - - public synchronized List seal( - List markers) { - requireFrozen(); - List targets = new ArrayList<>( - Objects.requireNonNull(markers, "markers")); - if (targets.size() != payloads.size()) { - throw new ArchivePersistenceException("Flush batch marker count mismatch"); - } - - List sealed = new ArrayList<>(); - for (int i = 0; i < payloads.size(); i++) { - sealed.add(payloads.get(i).previewSealPayload(targets.get(i))); - } - for (PreparedBlockProjection payload : payloads) { - payload.completeSeal(); - } - payloads = Collections.emptyList(); - state = BatchState.SEALED; - return Collections.unmodifiableList(sealed); - } - - public synchronized void abort() { - requireFrozen(); - for (PreparedBlockProjection payload : payloads) { - payload.abort(); - } - payloads = Collections.emptyList(); - state = BatchState.ABORTED; - } - - public synchronized void abortIfFrozen() { - if (state == BatchState.FROZEN) { - abort(); - } - } - - private void requireFrozen() { - if (state != BatchState.FROZEN) { - throw new ArchivePersistenceException("Flush batch payload owner is terminal"); - } - } - - private enum BatchState { - FROZEN, - SEALED, - ABORTED - } - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java deleted file mode 100644 index a310e00603c..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetTargetActivationResolver.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.tron.core.db2.archive; - -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Objects; -import org.tron.common.utils.ByteArray; -import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.TargetAssetOptimization; -import org.tron.core.db2.archive.BlockChangeView.Change; -import org.tron.core.db2.archive.BlockChangeView.DatabaseChanges; -import org.tron.core.db2.archive.BlockChangeView.PostValue; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; - -/** Resolves proposal-66 activation from the target block's exact properties post view. */ -public final class AccountAssetTargetActivationResolver { - - static final String PROPERTIES_DB = "properties"; - static final String PROPOSAL_66_KEY = "ALLOW_ASSET_OPTIMIZATION"; - static final String PROPOSAL_53_KEY = "ALLOW_ACCOUNT_ASSET_OPTIMIZATION"; - - private static final byte[] PROPOSAL_66_PHYSICAL_KEY = - PROPOSAL_66_KEY.getBytes(StandardCharsets.UTF_8); - - public TargetAssetOptimization resolve(BlockSnapshotMeta target, BlockChangeView view) { - BlockSnapshotMeta expectedTarget = Objects.requireNonNull(target, "target"); - BlockChangeView input = Objects.requireNonNull(view, "view"); - if (!expectedTarget.equals(input.getMeta())) { - throw new ArchivePersistenceException("Asset optimization target identity mismatch"); - } - - DatabaseChanges properties = null; - for (DatabaseChanges database : input.getDatabases()) { - if (PROPERTIES_DB.equals(database.getDbName())) { - if (properties != null) { - throw new ArchivePersistenceException("Duplicate properties block view"); - } - properties = database; - } - } - if (properties == null) { - throw new ArchivePersistenceException("Missing properties block view"); - } - - byte[] previous = properties.getPrevious(PROPOSAL_66_PHYSICAL_KEY); - byte[] value = null; - boolean changed = false; - for (Change change : properties.getChanges()) { - if (Arrays.equals(PROPOSAL_66_PHYSICAL_KEY, change.getKey())) { - if (changed) { - throw new ArchivePersistenceException("Duplicate proposal-66 property mutation"); - } - changed = true; - PostValue postValue = change.getPostValue(); - if (!postValue.isPresent()) { - throw new ArchivePersistenceException("Proposal-66 property must not be deleted"); - } - value = postValue.getValue(); - } - } - if (!changed) { - value = previous; - } - boolean previousEnabled = decode(previous); - boolean targetEnabled = decode(value); - if (previousEnabled && !targetEnabled) { - throw new ArchivePersistenceException("Proposal-66 property must not regress"); - } - Phase phase = !targetEnabled ? Phase.P66_OFF - : previousEnabled ? Phase.P66_ON : Phase.P66_ACTIVATION; - return TargetAssetOptimization.forTarget(expectedTarget, phase); - } - - static byte[] proposal66PhysicalKey() { - return Arrays.copyOf(PROPOSAL_66_PHYSICAL_KEY, PROPOSAL_66_PHYSICAL_KEY.length); - } - - private static boolean decode(byte[] value) { - if (value == null) { - throw new ArchivePersistenceException("Missing proposal-66 property value"); - } - if (value.length != Long.BYTES) { - throw new ArchivePersistenceException("Proposal-66 property value must be exactly 8 bytes"); - } - long decoded = ByteArray.toLong(value); - if (decoded != 0L && decoded != 1L) { - throw new ArchivePersistenceException("Proposal-66 property value must be 0 or 1"); - } - return decoded == 1L; - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java index 58a7435af06..e74bc60f337 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBaseManifest.java @@ -22,7 +22,7 @@ final class ArchiveBaseManifest { private static final int MAGIC = 0x54414d46; // TAMF - private static final short VERSION = 2; + private static final short VERSION = 3; private static final int MAX_LENGTH = 1024 * 1024; private final Path directory; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java deleted file mode 100644 index 9aef9739a12..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationCapture.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.tron.core.db2.archive; - -import java.util.Objects; -import org.tron.core.db2.archive.BlockChangeView.PostValue; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; - -/** One-shot owner of a block's explicit AccountAsset events, post-state view, and output batch. */ -public final class ArchiveBlockForwardMutationCapture { - - private final BlockSnapshotMeta targetMeta; - private final AccountAssetForwardMutationRecorder accountAssetRecorder; - private BlockChangeView view; - private State state = State.OPEN; - - public ArchiveBlockForwardMutationCapture(BlockSnapshotMeta targetMeta, - Phase targetPhase, ArchiveBlockForwardMutationLimits limits) { - this.targetMeta = Objects.requireNonNull(targetMeta, "targetMeta"); - accountAssetRecorder = new AccountAssetForwardMutationRecorder(targetMeta, targetPhase, - Objects.requireNonNull(limits, "limits")); - } - - public synchronized void recordAccount(BlockSnapshotMeta eventMeta, - byte[] accountPhysicalKey, PostValue rawAccountPostValue, - PostValue canonicalAccountPostValue) { - requireOpen(); - accountAssetRecorder.recordAccount(eventMeta, accountPhysicalKey, rawAccountPostValue, - canonicalAccountPostValue); - } - - public synchronized void recordAssetPut(BlockSnapshotMeta eventMeta, - byte[] accountPhysicalKey, byte[] assetPhysicalKey, byte[] value) { - requireOpen(); - accountAssetRecorder.recordAssetPut(eventMeta, accountPhysicalKey, assetPhysicalKey, value); - } - - public synchronized void recordAssetDelete(BlockSnapshotMeta eventMeta, - byte[] accountPhysicalKey, byte[] assetPhysicalKey) { - requireOpen(); - accountAssetRecorder.recordAssetDelete(eventMeta, accountPhysicalKey, assetPhysicalKey); - } - - public synchronized void attach(BlockChangeView blockChangeView) { - requireOpen(); - BlockChangeView attached = Objects.requireNonNull(blockChangeView, "blockChangeView"); - if (!targetMeta.equals(attached.getMeta())) { - throw new ArchivePersistenceException("Block forward capture view meta mismatch"); - } - if (view != null) { - throw new ArchivePersistenceException("Block forward capture view is already attached"); - } - accountAssetRecorder.reserveView(attached); - view = attached; - } - - public synchronized ArchiveParticipantMutationBatch seal(HistoryCommitMarker committedTarget) { - requireOpen(); - if (view == null) { - throw new ArchivePersistenceException("Block forward capture view is missing"); - } - HistoryCommitMarker target = Objects.requireNonNull(committedTarget, "committedTarget"); - if (!targetMeta.equals(target.getMeta())) { - throw new ArchivePersistenceException("Block forward capture marker meta mismatch"); - } - AccountAssetForwardMutationManifest manifest = accountAssetRecorder.seal(target); - try { - ArchiveParticipantMutationBatch batch = - new ArchiveParticipantMutationBatchCollector(manifest).collect(target, view); - state = State.SEALED; - return batch; - } catch (RuntimeException e) { - state = State.FAILED; - throw e; - } finally { - view = null; - } - } - - public synchronized void abort() { - requireOpen(); - accountAssetRecorder.discard(); - view = null; - state = State.ABORTED; - } - - synchronized boolean hasAttachedView() { - return view != null; - } - - synchronized boolean isPayloadReleased() { - return accountAssetRecorder.isPayloadReleased(); - } - - private void requireOpen() { - if (state != State.OPEN) { - throw new ArchivePersistenceException( - "Block forward capture is terminal: " + state.name()); - } - } - - private enum State { - OPEN, - SEALED, - FAILED, - ABORTED - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationLimits.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationLimits.java deleted file mode 100644 index d1e12fde931..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationLimits.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.tron.core.db2.archive; - -/** Explicit standalone bounds for one block's complete forward mutation capture. */ -public final class ArchiveBlockForwardMutationLimits { - - private final int maxAccounts; - private final int maxAssetMutations; - private final int maxKeyBytes; - private final int maxValueBytes; - private final long maxTotalPayloadBytes; - - public ArchiveBlockForwardMutationLimits(int maxAccounts, int maxAssetMutations, - int maxKeyBytes, int maxValueBytes, long maxTotalPayloadBytes) { - if (maxAccounts < 0 || maxAssetMutations < 0 || maxKeyBytes < 0 - || maxValueBytes < 0 || maxTotalPayloadBytes < 0) { - throw new IllegalArgumentException("Block forward mutation limits must not be negative"); - } - this.maxAccounts = maxAccounts; - this.maxAssetMutations = maxAssetMutations; - this.maxKeyBytes = maxKeyBytes; - this.maxValueBytes = maxValueBytes; - this.maxTotalPayloadBytes = maxTotalPayloadBytes; - } - - int getMaxAccounts() { - return maxAccounts; - } - - int getMaxAssetMutations() { - return maxAssetMutations; - } - - int getMaxKeyBytes() { - return maxKeyBytes; - } - - int getMaxValueBytes() { - return maxValueBytes; - } - - long getMaxTotalPayloadBytes() { - return maxTotalPayloadBytes; - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardPayload.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardPayload.java deleted file mode 100644 index 0879e76846a..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockForwardPayload.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.tron.core.db2.archive; - -import java.util.Objects; - -/** Immutable handoff of one committed target's exact view and AccountAsset projection. */ -public final class ArchiveBlockForwardPayload { - - private final HistoryCommitMarker marker; - private final BlockChangeView view; - private final AccountAssetForwardMutationManifest accountAssetManifest; - - ArchiveBlockForwardPayload(HistoryCommitMarker marker, BlockChangeView view, - AccountAssetForwardMutationManifest accountAssetManifest) { - this.marker = Objects.requireNonNull(marker, "marker"); - this.view = Objects.requireNonNull(view, "view"); - this.accountAssetManifest = Objects.requireNonNull(accountAssetManifest, - "accountAssetManifest"); - if (!marker.getMeta().equals(view.getMeta())) { - throw new ArchivePersistenceException("Forward payload view target mismatch"); - } - } - - public BlockSnapshotMeta getMeta() { - return marker.getMeta(); - } - - public HistoryCommitMarker getMarker() { - return marker; - } - - public BlockChangeView getView() { - return view; - } - - public AccountAssetForwardMutationManifest getAccountAssetManifest() { - return accountAssetManifest; - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockProjectionPreparer.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockProjectionPreparer.java deleted file mode 100644 index 1e0633c5518..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBlockProjectionPreparer.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.tron.core.db2.archive; - -import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.PreparedBlockProjection; - -/** Prepares one identity-bound reverse and forward projection from one immutable block view. */ -@FunctionalInterface -public interface ArchiveBlockProjectionPreparer { - - PreparedBlockProjection prepare(BlockChangeView view); -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBootstrapAnchor.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBootstrapAnchor.java index 7ff51cdda3b..f529ac19d32 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBootstrapAnchor.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveBootstrapAnchor.java @@ -1,55 +1,169 @@ package org.tron.core.db2.archive; +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import java.util.UUID; -/** Explicit identity which makes a synthetic empty first H a non-queryable bootstrap anchor. */ +/** Atomic identity which makes a synthetic empty first H a non-queryable bootstrap anchor. */ final class ArchiveBootstrapAnchor { - private static final String PATH = "progress/bootstrap.progress"; + private static final int MAGIC = 0x54414241; // TABA + private static final short VERSION = 1; + private static final int HEADER_LENGTH = 16; + private static final int MAX_LENGTH = 1024 * 1024; + private static final String FILE_NAME = "bootstrap.anchor"; private ArchiveBootstrapAnchor() { } - static void store(Path archiveDirectory, HistoryCommitMarker marker, byte[] planDigest, - List participants) throws IOException { - ArchiveProgressEnvelope anchor = new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, - marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), planDigest, participants); - new ArchiveProgressFile(archiveDirectory.resolve(PATH), new ArchiveProgressEnvelopeCodec()) - .store(anchor); + static void store(Path archiveDirectory, HistoryCommitMarker marker, List stores) + throws IOException { + HistoryCommitMarkerCodec markerCodec = new HistoryCommitMarkerCodec(); + byte[] markerBytes = markerCodec.encode(marker); + requireStoreScope(marker, stores); + byte[] encoded = encode(markerBytes); + Files.createDirectories(archiveDirectory); + Path temporary = archiveDirectory.resolve("." + FILE_NAME + "-" + UUID.randomUUID()); + try { + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + writeFully(channel, ByteBuffer.wrap(encoded)); + channel.force(true); + } + try { + Files.move(temporary, archiveDirectory.resolve(FILE_NAME), + StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new ArchivePersistenceException( + "Archive filesystem does not support atomic bootstrap anchor replacement", + unsupported); + } + HistorySegmentStore.syncDirectory(archiveDirectory); + } finally { + Files.deleteIfExists(temporary); + } } static HistoryCommitMarker loadAndValidateIfPresent(Path archiveDirectory, - CommittedHistoryAuthority history, List participants) throws IOException { - Path path = archiveDirectory.resolve(PATH); - if (!Files.exists(path)) { + CommittedHistoryAuthority history, List stores) throws IOException { + Path path = archiveDirectory.resolve(FILE_NAME); + if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { return null; } + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new ArchivePersistenceException("Archive bootstrap anchor is not a regular file"); + } + HistoryCommitMarkerCodec markerCodec = new HistoryCommitMarkerCodec(); + HistoryCommitMarker anchor; + try { + anchor = markerCodec.decode(decode(Files.readAllBytes(path))); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Archive bootstrap anchor is corrupt", invalid); + } HistoryCommitMarker first = history.get(history.firstEpoch()); if (first == null) { throw new ArchivePersistenceException("Archive bootstrap anchor has no history marker"); } - ArchiveProgressEnvelope anchor = new ArchiveProgressFile(path, - new ArchiveProgressEnvelopeCodec()).load(); - if (anchor.getMutationPlanDigest() == null) { - throw new ArchivePersistenceException("Archive bootstrap anchor plan digest is missing"); + requireStoreScope(anchor, stores); + if (!Arrays.equals(markerCodec.encode(anchor), markerCodec.encode(first))) { + throw new ArchivePersistenceException( + "Archive bootstrap anchor does not match the first history marker"); } - anchor.requireIdentity(Kind.READER_VISIBLE, null, first.getMeta().getEpoch(), - first.getMeta().getBlockHash(), first.getBatchId(), - first.getHistoryLocation().getBodyDigest(), anchor.getMutationPlanDigest(), participants); - BlockReverseDiff diff; if (history instanceof ArchiveHistoryWriter) { - diff = ((ArchiveHistoryWriter) history).readCommitted(first.getMeta().getEpoch()); - } else { - return first; - } - if (!diff.getGroups().isEmpty()) { - throw new ArchivePersistenceException("Archive bootstrap anchor history is not empty"); + BlockReverseDiff diff = ((ArchiveHistoryWriter) history) + .readCommitted(first.getMeta().getEpoch()); + if (!diff.getGroups().isEmpty()) { + throw new ArchivePersistenceException("Archive bootstrap anchor history is not empty"); + } } return first; } + + private static byte[] encode(byte[] markerBytes) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(0); + output.writeInt(markerBytes.length); + output.write(markerBytes); + output.flush(); + byte[] payload = bytes.toByteArray(); + int length = payload.length + Integer.BYTES; + if (length > MAX_LENGTH) { + throw new IllegalArgumentException("Archive bootstrap anchor is too large"); + } + ByteBuffer.wrap(payload).putInt(8, length); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(payload); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected bootstrap anchor encoding failure", impossible); + } + } + + private static byte[] decode(byte[] encoded) { + if (encoded == null || encoded.length < HEADER_LENGTH + Integer.BYTES + || encoded.length > MAX_LENGTH) { + throw new IllegalArgumentException("Archive bootstrap anchor length is invalid"); + } + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(encoded, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new IllegalArgumentException("Archive bootstrap anchor checksum mismatch"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != encoded.length) { + throw new IllegalArgumentException("Unsupported archive bootstrap anchor header"); + } + int markerLength = input.readInt(); + if (markerLength <= 0 || markerLength != input.available() - Integer.BYTES) { + throw new IllegalArgumentException("Archive bootstrap anchor marker length is invalid"); + } + byte[] marker = new byte[markerLength]; + input.readFully(marker); + if (input.available() != Integer.BYTES) { + throw new IllegalArgumentException("Archive bootstrap anchor payload mismatch"); + } + return marker; + } catch (IOException invalid) { + throw new IllegalArgumentException("Archive bootstrap anchor is truncated", invalid); + } + } + + private static void requireStoreScope(HistoryCommitMarker marker, List stores) { + List expected = new ArrayList<>(stores); + Collections.sort(expected); + if (!marker.getDatabases().equals(expected)) { + throw new ArchivePersistenceException("Archive bootstrap anchor Store scope mismatch"); + } + } + + private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveCommittedPrefixPublisher.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveCommittedPrefixPublisher.java new file mode 100644 index 00000000000..0da6f8dacc8 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveCommittedPrefixPublisher.java @@ -0,0 +1,10 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; + +/** Publishes derived serving authority for one H/WAL-proven committed prefix. */ +@FunctionalInterface +public interface ArchiveCommittedPrefixPublisher { + + void publish(BlockSnapshotMeta target) throws IOException; +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveGenerationCapsule.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveGenerationCapsule.java index 02fbea822df..a4d9d1e6bf8 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveGenerationCapsule.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveGenerationCapsule.java @@ -8,27 +8,36 @@ public final class ArchiveGenerationCapsule { private final PersistentServingKeyIndexCatalog catalog; - private final Path readerVisiblePath; private final Path archiveDirectory; private final long maxSegmentSize; private final ArchiveReadSnapshot.PinnedLatestStateFactory latestFactory; + private final ReaderAuthority readerAuthority; public ArchiveGenerationCapsule(PersistentServingKeyIndexCatalog catalog, Path readerVisiblePath, Path archiveDirectory, long maxSegmentSize, ArchiveReadSnapshot.PinnedLatestStateFactory latestFactory) { + this(catalog, archiveDirectory, maxSegmentSize, latestFactory, + new ArchiveProgressFile(Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"), + new ArchiveProgressEnvelopeCodec())::load); + } + + public ArchiveGenerationCapsule(PersistentServingKeyIndexCatalog catalog, + Path archiveDirectory, long maxSegmentSize, + ArchiveReadSnapshot.PinnedLatestStateFactory latestFactory, + ReaderAuthority readerAuthority) { this.catalog = Objects.requireNonNull(catalog, "catalog"); - this.readerVisiblePath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); if (maxSegmentSize <= 0) { throw new IllegalArgumentException("maxSegmentSize must be positive"); } this.maxSegmentSize = maxSegmentSize; this.latestFactory = Objects.requireNonNull(latestFactory, "latestFactory"); + this.readerAuthority = Objects.requireNonNull(readerAuthority, "readerAuthority"); } public ArchiveReadSnapshot pin(long targetBlock) throws IOException { - ArchiveProgressEnvelope readerVisible = new ArchiveProgressFile(readerVisiblePath, - new ArchiveProgressEnvelopeCodec()).load(); + ArchiveProgressEnvelope readerVisible = Objects.requireNonNull(readerAuthority.read(), + "reader-visible authority"); return ArchiveReadSnapshot.pin(targetBlock, catalog, readerVisible, archiveDirectory, maxSegmentSize, serving -> pinLatest(serving)); } @@ -54,4 +63,9 @@ private ArchiveReadSnapshot.PinnedLatestState pinLatest( } return latest; } + + @FunctionalInterface + public interface ReaderAuthority { + ArchiveProgressEnvelope read() throws IOException; + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryScanAnchor.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryScanAnchor.java new file mode 100644 index 00000000000..18ddf6aa7e2 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryScanAnchor.java @@ -0,0 +1,197 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; + +/** Atomic scan anchor for a previously validated committed history prefix. */ +final class ArchiveHistoryScanAnchor { + + private static final int MAGIC = 0x54415341; // TASA + private static final short VERSION = 1; + private static final int HEADER_LENGTH = 36; + private static final int MAX_LENGTH = 2 * 1024 * 1024; + private static final String FILE_NAME = "history.scan-anchor"; + private static final String TEMP_FILE_NAME = "history.scan-anchor.tmp"; + + private final long firstEpoch; + private final long recordCount; + private final int commitRecordLength; + private final HistoryCommitMarker marker; + private final byte[] encodedMarker; + + private ArchiveHistoryScanAnchor(long firstEpoch, long recordCount, int commitRecordLength, + HistoryCommitMarker marker, byte[] encodedMarker) { + this.firstEpoch = firstEpoch; + this.recordCount = recordCount; + this.commitRecordLength = commitRecordLength; + this.marker = marker; + this.encodedMarker = Arrays.copyOf(encodedMarker, encodedMarker.length); + } + + static ArchiveHistoryScanAnchor load(Path archiveDirectory, + HistoryCommitMarkerCodec markerCodec) throws IOException { + Path path = archiveDirectory.resolve(FILE_NAME); + if (!Files.exists(path)) { + return null; + } + long size = Files.size(path); + if (size < HEADER_LENGTH + Integer.BYTES || size > MAX_LENGTH) { + throw new ArchivePersistenceException("Archive history scan anchor length is invalid"); + } + byte[] encoded = Files.readAllBytes(path); + try { + return decode(encoded, markerCodec); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Archive history scan anchor is corrupt", invalid); + } + } + + static ArchiveHistoryScanAnchor persist(Path archiveDirectory, long firstEpoch, + long recordCount, int commitRecordLength, HistoryCommitMarker marker, + HistoryCommitMarkerCodec markerCodec) throws IOException { + if (marker == null || recordCount <= 0 || commitRecordLength <= 0 + || firstEpoch + recordCount - 1 != marker.getMeta().getEpoch()) { + throw new IllegalArgumentException("Invalid archive history scan anchor state"); + } + byte[] markerBytes = markerCodec.encode(marker); + if (markerBytes.length != commitRecordLength) { + throw new IllegalArgumentException("Scan anchor commit record length mismatch"); + } + byte[] encoded = encode(firstEpoch, recordCount, commitRecordLength, markerBytes); + Files.createDirectories(archiveDirectory); + Path temporary = archiveDirectory.resolve(TEMP_FILE_NAME); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + writeFully(channel, ByteBuffer.wrap(encoded)); + channel.force(true); + } + Path target = archiveDirectory.resolve(FILE_NAME); + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new ArchivePersistenceException( + "Archive filesystem does not support atomic history scan anchor replacement", + unsupported); + } + HistorySegmentStore.syncDirectory(archiveDirectory); + return new ArchiveHistoryScanAnchor(firstEpoch, recordCount, commitRecordLength, marker, + markerBytes); + } + + long getFirstEpoch() { + return firstEpoch; + } + + long getRecordCount() { + return recordCount; + } + + int getCommitRecordLength() { + return commitRecordLength; + } + + HistoryCommitMarker getMarker() { + return marker; + } + + byte[] getEncodedMarker() { + return Arrays.copyOf(encodedMarker, encodedMarker.length); + } + + private static byte[] encode(long firstEpoch, long recordCount, int commitRecordLength, + byte[] markerBytes) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(0); + output.writeLong(firstEpoch); + output.writeLong(recordCount); + output.writeInt(commitRecordLength); + output.writeInt(markerBytes.length); + output.write(markerBytes); + output.flush(); + byte[] withoutChecksum = bytes.toByteArray(); + int length = withoutChecksum.length + Integer.BYTES; + if (length > MAX_LENGTH) { + throw new IllegalArgumentException("Archive history scan anchor is too large"); + } + ByteBuffer.wrap(withoutChecksum).putInt(8, length); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(withoutChecksum); + output.writeInt(crc32c(withoutChecksum)); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected scan anchor encoding failure", impossible); + } + } + + private static ArchiveHistoryScanAnchor decode(byte[] encoded, + HistoryCommitMarkerCodec markerCodec) { + if (encoded == null || encoded.length < HEADER_LENGTH + Integer.BYTES + || encoded.length > MAX_LENGTH) { + throw new IllegalArgumentException("History scan anchor length is invalid"); + } + int checksum = ByteBuffer.wrap(encoded, encoded.length - Integer.BYTES, + Integer.BYTES).getInt(); + byte[] withoutChecksum = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + if (checksum != crc32c(withoutChecksum)) { + throw new IllegalArgumentException("History scan anchor checksum mismatch"); + } + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != encoded.length) { + throw new IllegalArgumentException("Unsupported history scan anchor header"); + } + long firstEpoch = input.readLong(); + long recordCount = input.readLong(); + int recordLength = input.readInt(); + int markerLength = input.readInt(); + if (firstEpoch < 0 || recordCount <= 0 || recordLength <= 0 + || markerLength != recordLength || markerLength > input.available() - Integer.BYTES) { + throw new IllegalArgumentException("Invalid history scan anchor fields"); + } + byte[] markerBytes = new byte[markerLength]; + input.readFully(markerBytes); + if (input.available() != Integer.BYTES) { + throw new IllegalArgumentException("History scan anchor payload mismatch"); + } + HistoryCommitMarker marker = markerCodec.decode(markerBytes); + if (firstEpoch + recordCount - 1 != marker.getMeta().getEpoch()) { + throw new IllegalArgumentException("History scan anchor ordinal mismatch"); + } + return new ArchiveHistoryScanAnchor(firstEpoch, recordCount, recordLength, marker, + markerBytes); + } catch (IOException invalid) { + throw new IllegalArgumentException("History scan anchor is truncated", invalid); + } + } + + private static int crc32c(byte[] bytes) { + return Hashing.crc32c().hashBytes(bytes).asInt(); + } + + private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java index 8e9302ef2d6..1e84487bf86 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java @@ -7,9 +7,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.List; -import java.util.NoSuchElementException; import java.util.Set; import java.util.UUID; @@ -46,7 +44,7 @@ public ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, this.participatingDatabases.sort(String::compareTo); this.manifest = new ArchiveBaseManifest(archiveDirectory, this.participatingDatabases); new ArchiveTruncationRecovery(archiveDirectory, maxSegmentSize).recover(); - ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archiveDirectory, + ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archiveDirectory, commitCodec); this.bodies = new HistorySegmentStore(archiveDirectory, new BlockHistoryCodec(), maxSegmentSize, checkpoint); @@ -54,19 +52,20 @@ public ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, this.commits = new HistoryCommitStore(archiveDirectory, commitCodec, checkpoint); this.hook = hook; recoverPreparedSuffix(); - persistRestartCheckpoint(); + persistHistoryScanAnchor(); if (commits.head() != null) { manifest.ensureBase(commits.get(commits.firstEpoch()).getMeta()); } this.accountIndex = new AccountChangeIndex(archiveDirectory.resolve("account-change-index")); + HistoryCommitMarker bootstrap; try { catchUpAccountIndex(); + bootstrap = ArchiveBootstrapAnchor.loadAndValidateIfPresent( + archiveDirectory, this, this.participatingDatabases); } catch (IOException | RuntimeException failure) { closeAfterFailedConstruction(failure); throw failure; } - HistoryCommitMarker bootstrap = ArchiveBootstrapAnchor.loadAndValidateIfPresent( - archiveDirectory, this, this.participatingDatabases); this.bootstrapFloor = bootstrap == null ? null : bootstrap.getMeta().getEpoch(); } @@ -112,7 +111,7 @@ public synchronized void revert(BlockSnapshotMeta meta) { HistoryCommitMarker previous = commits.get(meta.getEpoch() - 1); accountIndex.revert(reverted, previous == null ? null : previous.getMeta()); commits.removeHead(meta); - persistRestartCheckpoint(); + persistHistoryScanAnchor(); previous = commits.head(); index.truncateAfter(previous == null ? null : previous.getIndexLocation(), commits.size()); bodies.truncateAfter(previous == null ? null : previous.getHistoryLocation(), @@ -164,6 +163,10 @@ public synchronized long firstEpoch() { return commits.firstEpoch(); } + Path getArchiveDirectory() { + return archiveDirectory; + } + @Override public synchronized HistoryCoverage coverage() { return commits.coverage(); @@ -242,38 +245,49 @@ public synchronized PersistentServingKeyIndexGeneration buildServingGeneration( public synchronized PersistentServingKeyIndexGeneration buildServingGeneration( Path shadowDirectory, String generationId, byte[] latestSourceIdentityDigest) throws IOException { + ServingSource source = servingSource(); + return PersistentServingKeyIndexGeneration.build(shadowDirectory, generationId, + source.baseEpoch, source.baseHash, source.committed, index::read, + participatingDatabases, latestSourceIdentityDigest); + } + + /** Recomputes the exact all-Store serving source identity without writing a generation. */ + public synchronized ServingKeyIndexGeneration buildServingIdentity(String generationId) + throws IOException { + ServingSource source = servingSource(); + return ServingKeyIndexGeneration.rebuild(generationId, source.baseEpoch, source.baseHash, + source.committed, index::read, participatingDatabases, + ServingKeyIndexGeneration.IndexLayout.prototypeDefaults()); + } + + private ServingSource servingSource() { HistoryCommitMarker first = commits.head() == null ? null : commits.get(commits.firstEpoch()); if (first == null) { throw new IllegalStateException("Cannot build a serving generation from empty history"); } long firstEpoch = bootstrapFloor == null ? commits.firstEpoch() : bootstrapFloor + 1; long lastEpoch = commits.head().getMeta().getEpoch(); - if (firstEpoch > lastEpoch) { - throw new IllegalStateException( - "Cannot build a serving generation before post-bootstrap history exists"); + List committed = new ArrayList<>(); + for (long epoch = firstEpoch; epoch <= lastEpoch; epoch++) { + committed.add(commits.get(epoch)); } - Iterable committed = () -> new Iterator() { - private long nextEpoch = firstEpoch; - - @Override - public boolean hasNext() { - return nextEpoch <= lastEpoch; - } - - @Override - public HistoryCommitMarker next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - return commits.get(nextEpoch++); - } - }; long baseEpoch = firstEpoch - 1; byte[] baseHash = bootstrapFloor == null ? first.getMeta().getParentHash() : first.getMeta().getBlockHash(); - return PersistentServingKeyIndexGeneration.build(shadowDirectory, generationId, - baseEpoch, baseHash, committed, index::read, - participatingDatabases, latestSourceIdentityDigest); + return new ServingSource(baseEpoch, baseHash, committed); + } + + private static final class ServingSource { + private final long baseEpoch; + private final byte[] baseHash; + private final List committed; + + private ServingSource(long baseEpoch, byte[] baseHash, + List committed) { + this.baseEpoch = baseEpoch; + this.baseHash = baseHash; + this.committed = committed; + } } long getStartupScannedRecords() { @@ -281,13 +295,13 @@ long getStartupScannedRecords() { + commits.getStartupScannedRecords(); } - private void persistRestartCheckpoint() throws IOException { + private void persistHistoryScanAnchor() throws IOException { HistoryCommitMarker head = commits.head(); if (head != null) { - ArchiveRestartCheckpoint.persist(archiveDirectory, commits.firstEpoch(), commits.size(), + ArchiveHistoryScanAnchor.persist(archiveDirectory, commits.firstEpoch(), commits.size(), commits.getRecordLength(), head, commitCodec); } else { - Files.deleteIfExists(archiveDirectory.resolve("restart.checkpoint")); + Files.deleteIfExists(archiveDirectory.resolve("history.scan-anchor")); HistorySegmentStore.syncDirectory(archiveDirectory); } } @@ -319,7 +333,7 @@ private void persistChunk(List diffs) throws IOException { previousEpoch = diff.getMeta().getEpoch(); } commits.commitAll(markers); - persistRestartCheckpoint(); + persistHistoryScanAnchor(); } private void validateNext(BlockSnapshotMeta previous, BlockSnapshotMeta meta) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipant.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipant.java deleted file mode 100644 index 8c00eefbadc..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipant.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.tron.core.db2.archive; - -import java.io.IOException; -import java.util.List; - -/** Engine-neutral archive participant with one atomic business+D apply boundary. */ -public interface ArchiveParticipant extends ArchiveParticipantProgressSource { - - void apply(List mutations, ArchiveProgressEnvelope progress) - throws IOException; -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantBatchFile.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantBatchFile.java deleted file mode 100644 index f759c889541..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantBatchFile.java +++ /dev/null @@ -1,186 +0,0 @@ -package org.tron.core.db2.archive; - -import com.google.common.hash.Hashing; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.file.AtomicMoveNotSupportedException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; - -/** Atomic-file prototype containing participant business bytes and D[i] in one durable unit. */ -public final class ArchiveParticipantBatchFile { - - private static final int MAGIC = 0x54414254; // TABT - private static final short VERSION = 1; - private static final int HEADER_LENGTH = 20; - private static final int MAX_BUSINESS_LENGTH = 64 * 1024 * 1024; - - private final Path path; - private final Path temporary; - private final String participant; - private final List participants; - private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); - private final FaultHook faultHook; - - public ArchiveParticipantBatchFile(Path path, String participant, List participants) { - this(path, participant, participants, temporary -> { }); - } - - ArchiveParticipantBatchFile(Path path, String participant, List participants, - FaultHook faultHook) { - this.path = Objects.requireNonNull(path, "path"); - this.temporary = path.resolveSibling(path.getFileName() + ".tmp"); - this.participants = validateParticipants(participants); - if (participant == null || participant.isEmpty() || !this.participants.contains(participant)) { - throw new IllegalArgumentException("Archive batch participant is invalid"); - } - this.participant = participant; - this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); - } - - public void store(byte[] businessPayload, ArchiveProgressEnvelope progress) throws IOException { - byte[] encoded = encode(businessPayload, progress); - Path directory = Objects.requireNonNull(path.getParent(), "participant batch directory"); - Files.createDirectories(directory); - try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { - ByteBuffer buffer = ByteBuffer.wrap(encoded); - while (buffer.hasRemaining()) { - channel.write(buffer); - } - channel.force(true); - } - faultHook.afterTemporaryForce(temporary); - try { - Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE, - StandardCopyOption.REPLACE_EXISTING); - } catch (AtomicMoveNotSupportedException unsupported) { - throw new ArchivePersistenceException( - "Archive participant filesystem does not support atomic replacement", unsupported); - } - HistorySegmentStore.syncDirectory(directory); - } - - public Snapshot load() throws IOException { - if (!Files.exists(path)) { - throw new ArchivePersistenceException("Archive participant batch is missing: " + path); - } - try { - return decode(Files.readAllBytes(path)); - } catch (IllegalArgumentException invalid) { - throw new ArchivePersistenceException("Archive participant batch is corrupt", invalid); - } - } - - Path getTemporaryPath() { - return temporary; - } - - private byte[] encode(byte[] businessPayload, ArchiveProgressEnvelope progress) { - byte[] business = Arrays.copyOf(Objects.requireNonNull(businessPayload, "businessPayload"), - businessPayload.length); - if (business.length > MAX_BUSINESS_LENGTH) { - throw new IllegalArgumentException("Archive participant business payload is too large"); - } - requireProgress(progress); - byte[] encodedProgress = progressCodec.encode(progress); - int length = HEADER_LENGTH + business.length + encodedProgress.length + Integer.BYTES; - ByteBuffer buffer = ByteBuffer.allocate(length); - buffer.putInt(MAGIC).putShort(VERSION).putShort((short) 0).putInt(length) - .putInt(business.length).putInt(encodedProgress.length).put(business).put(encodedProgress); - byte[] payload = Arrays.copyOf(buffer.array(), length - Integer.BYTES); - buffer.putInt(Hashing.crc32c().hashBytes(payload).asInt()); - return buffer.array(); - } - - private Snapshot decode(byte[] encoded) { - if (encoded == null || encoded.length < HEADER_LENGTH + Integer.BYTES) { - throw new IllegalArgumentException("Archive participant batch length is invalid"); - } - ByteBuffer buffer = ByteBuffer.wrap(encoded); - if (buffer.getInt() != MAGIC || buffer.getShort() != VERSION || buffer.getShort() != 0 - || buffer.getInt() != encoded.length) { - throw new IllegalArgumentException("Unsupported archive participant batch header"); - } - int businessLength = buffer.getInt(); - int progressLength = buffer.getInt(); - long expectedLength = HEADER_LENGTH + (long) businessLength + progressLength + Integer.BYTES; - if (businessLength < 0 || businessLength > MAX_BUSINESS_LENGTH - || progressLength <= 0 || expectedLength != encoded.length) { - throw new IllegalArgumentException("Archive participant batch payload length is invalid"); - } - int expectedChecksum = ByteBuffer.wrap(encoded, encoded.length - Integer.BYTES, - Integer.BYTES).getInt(); - byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); - if (expectedChecksum != Hashing.crc32c().hashBytes(payload).asInt()) { - throw new IllegalArgumentException("Archive participant batch checksum mismatch"); - } - byte[] business = new byte[businessLength]; - buffer.get(business); - byte[] encodedProgress = new byte[progressLength]; - buffer.get(encodedProgress); - ArchiveProgressEnvelope progress = progressCodec.decode(encodedProgress); - requireProgress(progress); - return new Snapshot(business, progress); - } - - private void requireProgress(ArchiveProgressEnvelope progress) { - Objects.requireNonNull(progress, "progress"); - if (progress.getKind() != Kind.PARTICIPANT_PROGRESS - || !participant.equals(progress.getParticipant()) - || !participants.equals(progress.getParticipants())) { - throw new IllegalArgumentException("Archive participant batch progress identity mismatch"); - } - } - - private static List validateParticipants(List participants) { - Objects.requireNonNull(participants, "participants"); - if (participants.isEmpty()) { - throw new IllegalArgumentException("Archive participant set must not be empty"); - } - List copy = new ArrayList<>(participants.size()); - String previous = null; - for (String participant : participants) { - if (participant == null || participant.isEmpty() - || previous != null && previous.compareTo(participant) >= 0) { - throw new IllegalArgumentException( - "Archive participants must be non-empty, unique, and sorted"); - } - copy.add(participant); - previous = participant; - } - return Collections.unmodifiableList(copy); - } - - public static final class Snapshot { - private final byte[] businessPayload; - private final ArchiveProgressEnvelope progress; - - private Snapshot(byte[] businessPayload, ArchiveProgressEnvelope progress) { - this.businessPayload = Arrays.copyOf(businessPayload, businessPayload.length); - this.progress = progress; - } - - public byte[] getBusinessPayload() { - return Arrays.copyOf(businessPayload, businessPayload.length); - } - - public ArchiveProgressEnvelope getProgress() { - return progress; - } - } - - @FunctionalInterface - interface FaultHook { - void afterTemporaryForce(Path temporary) throws IOException; - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java deleted file mode 100644 index 57d736eb875..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatch.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.tron.core.db2.archive; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; - -/** Immutable producer payload for one committed target's exact physical participant mutations. */ -public final class ArchiveParticipantMutationBatch { - - private final long targetEpoch; - private final byte[] blockHash; - private final byte[] batchId; - private final byte[] historyPayloadDigest; - private final String accountAssetFormatId; - private final Phase targetPhase; - private final List participants; - private final List mutations; - - public ArchiveParticipantMutationBatch(HistoryCommitMarker target, Phase targetPhase, - List mutations) { - this(target, P66AccountAssetCodec.FORMAT_ID, targetPhase, mutations); - } - - ArchiveParticipantMutationBatch(HistoryCommitMarker target, String accountAssetFormatId, - Phase targetPhase, List mutations) { - HistoryCommitMarker checkedTarget = Objects.requireNonNull(target, "target"); - targetEpoch = checkedTarget.getMeta().getEpoch(); - blockHash = checkedTarget.getMeta().getBlockHash(); - batchId = checkedTarget.getBatchId(); - historyPayloadDigest = checkedTarget.getHistoryLocation().getBodyDigest(); - this.accountAssetFormatId = Objects.requireNonNull(accountAssetFormatId, - "accountAssetFormatId"); - if (accountAssetFormatId.isEmpty()) { - throw new IllegalArgumentException("AccountAsset transition format must not be empty"); - } - this.targetPhase = Objects.requireNonNull(targetPhase, "targetPhase"); - participants = Collections.unmodifiableList( - new ArrayList<>(checkedTarget.getDatabases())); - List copy = new ArrayList<>(Objects.requireNonNull(mutations, "mutations")); - if (copy.contains(null)) { - throw new IllegalArgumentException("Participant mutation batch contains null mutation"); - } - this.mutations = Collections.unmodifiableList(copy); - } - - public long getTargetEpoch() { - return targetEpoch; - } - - byte[] getBlockHash() { - return Arrays.copyOf(blockHash, blockHash.length); - } - - byte[] getBatchId() { - return Arrays.copyOf(batchId, batchId.length); - } - - byte[] getHistoryPayloadDigest() { - return Arrays.copyOf(historyPayloadDigest, historyPayloadDigest.length); - } - - String getAccountAssetFormatId() { - return accountAssetFormatId; - } - - Phase getTargetPhase() { - return targetPhase; - } - - List getParticipants() { - return participants; - } - - List getMutations() { - return mutations; - } - - /** One immutable put/delete against an exact participant physical key. */ - public static final class Mutation { - private final String dbName; - private final byte[] physicalRawKey; - private final byte[] value; - - private Mutation(String dbName, byte[] physicalRawKey, byte[] value) { - if (dbName == null || dbName.isEmpty()) { - throw new IllegalArgumentException("Participant mutation dbName must not be empty"); - } - this.dbName = dbName; - this.physicalRawKey = Arrays.copyOf( - Objects.requireNonNull(physicalRawKey, "physicalRawKey"), physicalRawKey.length); - this.value = value == null ? null : Arrays.copyOf(value, value.length); - } - - public static Mutation put(String dbName, byte[] physicalRawKey, byte[] value) { - return new Mutation(dbName, physicalRawKey, Objects.requireNonNull(value, "value")); - } - - public static Mutation delete(String dbName, byte[] physicalRawKey) { - return new Mutation(dbName, physicalRawKey, null); - } - - String getDbName() { - return dbName; - } - - byte[] getPhysicalRawKey() { - return Arrays.copyOf(physicalRawKey, physicalRawKey.length); - } - - byte[] getValue() { - return value == null ? null : Arrays.copyOf(value, value.length); - } - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java deleted file mode 100644 index 03638dd43da..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollector.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.tron.core.db2.archive; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; -import org.tron.core.db2.archive.AccountAssetForwardProjector.Projection; -import org.tron.core.db2.archive.ArchiveParticipantMutationBatch.Mutation; -import org.tron.core.db2.archive.BlockChangeView.Change; -import org.tron.core.db2.archive.BlockChangeView.DatabaseChanges; -import org.tron.core.db2.archive.BlockChangeView.PostValue; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; - -/** Converts one immutable block post-state view into a target-bound physical mutation batch. */ -public final class ArchiveParticipantMutationBatchCollector { - - private final AccountAssetForwardProjector accountAssetProjector; - private final String accountAssetFormatId; - private final Phase targetPhase; - private final List participants; - - public ArchiveParticipantMutationBatchCollector(Phase targetPhase) { - this(P66AccountAssetCodec.FORMAT_ID, targetPhase, null); - } - - public ArchiveParticipantMutationBatchCollector( - AccountAssetForwardMutationManifest manifest) { - this(Objects.requireNonNull(manifest, "manifest").getFormatId(), manifest.getTargetPhase(), - manifest); - } - - public ArchiveParticipantMutationBatchCollector(Phase targetPhase, - AccountAssetForwardProjector accountAssetProjector) { - this(P66AccountAssetCodec.FORMAT_ID, targetPhase, accountAssetProjector); - } - - private ArchiveParticipantMutationBatchCollector(String accountAssetFormatId, - Phase targetPhase, AccountAssetForwardProjector accountAssetProjector) { - this.accountAssetFormatId = Objects.requireNonNull(accountAssetFormatId, - "accountAssetFormatId"); - this.targetPhase = Objects.requireNonNull(targetPhase, "targetPhase"); - this.accountAssetProjector = accountAssetProjector; - participants = ArchiveParticipantDescriptor.current().getParticipants(); - } - - public ArchiveParticipantMutationBatch collect(HistoryCommitMarker committedTarget, - BlockChangeView view) { - HistoryCommitMarker target = Objects.requireNonNull(committedTarget, "committedTarget"); - BlockChangeView input = Objects.requireNonNull(view, "view"); - if (!target.getMeta().equals(input.getMeta())) { - throw new ArchivePersistenceException("Block mutation view target identity mismatch"); - } - requireExactCoverage(target, input); - List changedAccountKeys = changedAccountKeys(input); - if (!changedAccountKeys.isEmpty() && accountAssetProjector == null) { - throw new ArchivePersistenceException( - "Account mutation requires an explicit AccountAsset forward projector"); - } - if (accountAssetProjector != null) { - accountAssetProjector.begin(target, changedAccountKeys); - } - List mutations = new ArrayList<>(); - for (DatabaseChanges database : input.getDatabases()) { - for (Change change : database.getChanges()) { - if (AccountAssetArchiveProjector.ACCOUNT_DB.equals(database.getDbName())) { - collectAccount(change, mutations); - } else { - mutations.add(toMutation(database.getDbName(), change.getKey(), - change.getPostValue())); - } - } - } - if (accountAssetProjector != null) { - accountAssetProjector.complete(); - } - return new ArchiveParticipantMutationBatch(target, accountAssetFormatId, targetPhase, - mutations); - } - - private void collectAccount(Change change, List mutations) { - if (accountAssetProjector == null) { - throw new ArchivePersistenceException( - "Account mutation requires an explicit AccountAsset forward projector"); - } - byte[] accountKey = change.getKey(); - Projection projection = accountAssetProjector.project(accountKey, change.getPostValue()); - if (projection == null) { - throw new ArchivePersistenceException("AccountAsset forward projection is missing"); - } - mutations.add(toMutation(AccountAssetArchiveProjector.ACCOUNT_DB, accountKey, - projection.getAccountPostValue())); - for (AssetMutation asset : projection.getAssetMutations()) { - mutations.add(toMutation(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, - asset.getPhysicalRawKey(), asset.getPostValue())); - } - } - - private void requireExactCoverage(HistoryCommitMarker target, BlockChangeView view) { - List actual = new ArrayList<>(); - for (DatabaseChanges database : view.getDatabases()) { - actual.add(database.getDbName()); - } - Collections.sort(actual); - List captured = new ArrayList<>(participants); - captured.remove(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB); - boolean legacyEmptyDerivedGroup = actual.equals(participants) - && view.getDatabases().stream() - .filter(database -> AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals( - database.getDbName())) - .allMatch(database -> database.getChanges().isEmpty()); - if ((!actual.equals(captured) && !legacyEmptyDerivedGroup) - || !target.getDatabases().equals(participants)) { - throw new ArchivePersistenceException( - "Block mutation source set mismatch: expected=" + captured + ", actual=" + actual); - } - } - - private static List changedAccountKeys(BlockChangeView view) { - List keys = new ArrayList<>(); - for (DatabaseChanges database : view.getDatabases()) { - if (AccountAssetArchiveProjector.ACCOUNT_DB.equals(database.getDbName())) { - for (Change change : database.getChanges()) { - keys.add(change.getKey()); - } - } - } - return keys; - } - - private static Mutation toMutation(String dbName, byte[] key, PostValue postValue) { - return postValue.isPresent() - ? Mutation.put(dbName, key, postValue.getValue()) - : Mutation.delete(dbName, key); - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java deleted file mode 100644 index 5268b5f5e27..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorage.java +++ /dev/null @@ -1,278 +0,0 @@ -package org.tron.core.db2.archive; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.TreeMap; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoveryStorage; - -/** File history plus mixed native participants implementing the H/C/D[i]/R executor. */ -public final class ArchiveParticipantRecoveryStorage implements RecoveryStorage, Closeable { - - private final Path archiveDirectory; - private final long maxSegmentSize; - private final List participants; - private final Map participantEngines; - private final ArchiveProgressFile checkpointFile; - private final ArchiveTargetMutationPlanFile mutationPlanFile; - private final HistorySegmentStore bodies; - private final HistoryIndexStore index; - private final HistoryCommitStore history; - private final ArchiveRecoveryAuthorityScanner scanner; - private final ArchiveReaderPublicationGate publicationGate; - private final ArchiveStateBarrier.ArchiveStateAction refresh; - - public ArchiveParticipantRecoveryStorage(Path archiveDirectory, long maxSegmentSize, - Path checkpointPath, Map participantEngines, - Path readerVisiblePath, List participants) - throws IOException { - this(archiveDirectory, maxSegmentSize, checkpointPath, participantEngines, - readerVisiblePath, participants, action -> action.run(), () -> { }); - } - - public ArchiveParticipantRecoveryStorage(Path archiveDirectory, long maxSegmentSize, - Path checkpointPath, Map participantEngines, - Path readerVisiblePath, List participants, ArchiveStateBarrier barrier, - ArchiveStateBarrier.ArchiveStateAction refresh) - throws IOException { - this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); - if (maxSegmentSize <= 0) { - throw new IllegalArgumentException("maxSegmentSize must be positive"); - } - this.maxSegmentSize = maxSegmentSize; - this.participants = validateParticipants(participants); - TreeMap sorted = new TreeMap<>( - Objects.requireNonNull(participantEngines, "participantEngines")); - if (!new ArrayList<>(sorted.keySet()).equals(this.participants) - || sorted.containsValue(null)) { - throw new IllegalArgumentException("Archive participant engine set mismatch"); - } - this.participantEngines = Collections.unmodifiableMap(sorted); - ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); - this.checkpointFile = new ArchiveProgressFile(checkpointPath, progressCodec); - this.mutationPlanFile = new ArchiveTargetMutationPlanFile(checkpointPath); - new ArchiveTruncationRecovery(archiveDirectory, maxSegmentSize).recover(); - ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archiveDirectory, - new HistoryCommitMarkerCodec()); - if (checkpoint == null) { - throw new ArchivePersistenceException("Archive restart checkpoint is missing"); - } - HistorySegmentStore openedBodies = null; - HistoryIndexStore openedIndex = null; - HistoryCommitStore openedHistory = null; - try { - openedBodies = new HistorySegmentStore(archiveDirectory, new BlockHistoryCodec(), - maxSegmentSize, checkpoint); - openedIndex = new HistoryIndexStore(archiveDirectory, new HistoryIndexCodec(), checkpoint); - openedHistory = new HistoryCommitStore(archiveDirectory, new HistoryCommitMarkerCodec(), - checkpoint); - } catch (IOException | RuntimeException failure) { - close(openedIndex, failure); - close(openedBodies, failure); - close(openedHistory, failure); - throw failure; - } - this.bodies = openedBodies; - this.index = openedIndex; - this.history = openedHistory; - this.scanner = ArchiveRecoveryAuthorityScanner.forParticipants(this.history, - checkpointPath, this.participantEngines, readerVisiblePath, this.participants); - this.publicationGate = new ArchiveReaderPublicationGate(this.history, - checkpointFile::load, - this.participantEngines, readerVisiblePath, this.participants, - Objects.requireNonNull(barrier, "barrier")); - this.refresh = Objects.requireNonNull(refresh, "refresh"); - } - - @Override - public RecoverySnapshot scan() throws IOException { - RecoverySnapshot snapshot = scanner.scan(); - ArchiveTargetMutationPlan plan = mutationPlanFile.loadIfPresent(); - boolean fixed = isFixed(snapshot); - if (plan == null) { - if (!authoritiesAtCheckpoint(snapshot)) { - throw new ArchivePersistenceException("Archive recovery mutation plan is missing"); - } - return snapshot; - } - long planEpoch = plan.getTarget().getEpoch(); - HistoryCommitMarker marker = history.get(planEpoch); - if (marker != null) { - plan.requireIdentity(marker, participants); - } - long checkpoint = snapshot.getCheckpointHead(); - boolean preparedOnly = planEpoch == checkpoint + 1 && authoritiesAtCheckpoint(snapshot); - if (planEpoch != checkpoint && !preparedOnly || marker == null && !preparedOnly) { - throw new ArchivePersistenceException("Mutation plan does not match recovery checkpoint"); - } - if (!preparedOnly) { - requirePlanDigest(plan, checkpointFile.load()); - } - return snapshot; - } - - /** Returns the committed H head observed by this startup recovery session. */ - public HistoryCommitMarker committedHead() { - return history.head(); - } - - @Override - public void truncateHistoryAndSync(long historyHead) throws IOException { - ArchiveTruncationIntent.prepare(archiveDirectory, history, index, bodies, historyHead, - new HistoryCommitMarkerCodec()); - new ArchiveTruncationRecovery(archiveDirectory, maxSegmentSize).recover(); - history.truncateAfter(historyHead); - } - - @Override - public void replayParticipantAndSyncProgress(String participant, long firstEpoch, - long lastEpoch) throws IOException { - ArchiveParticipant engine = participantEngines.get(participant); - if (engine == null) { - throw new ArchivePersistenceException("Unknown archive recovery participant: " + participant); - } - HistoryCommitMarker marker = history.get(lastEpoch); - if (marker == null || firstEpoch > lastEpoch) { - throw new ArchivePersistenceException("Archive participant replay range is invalid"); - } - ArchiveTargetMutationPlan plan = mutationPlanFile.loadRequired(); - plan.requireIdentity(marker, participants); - if (plan.getTarget().getEpoch() != lastEpoch) { - throw new ArchivePersistenceException("Mutation plan does not cover replay range"); - } - byte[] mutationPlanDigest = requirePlanDigest(plan, checkpointFile.load()); - List mutations = plan.getMutations(participant); - ArchiveProgressEnvelope progress = new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, - participant, lastEpoch, marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, participants); - engine.apply(mutations, progress); - } - - @Override - public void publishReaderHeadAndSync(long readerVisibleHead) throws IOException { - publicationGate.publishAfterRefresh(readerVisibleHead, refresh); - } - - @Override - public void recoveryComplete() throws IOException { - RecoverySnapshot snapshot = scanner.scan(); - if (!isFixed(snapshot)) { - throw new ArchivePersistenceException("Archive recovery did not reach a fixed point"); - } - ArchiveTargetMutationPlan plan = mutationPlanFile.loadIfPresent(); - if (plan != null) { - long epoch = plan.getTarget().getEpoch(); - HistoryCommitMarker marker = history.get(epoch); - if (marker != null) { - plan.requireIdentity(marker, participants); - } - if (epoch != snapshot.getCheckpointHead() && epoch != snapshot.getCheckpointHead() + 1) { - throw new ArchivePersistenceException("Completed recovery has an unrelated mutation plan"); - } - if (epoch == snapshot.getCheckpointHead()) { - requirePlanDigest(plan, checkpointFile.load()); - } - } - mutationPlanFile.retire(); - } - - @Override - public void close() throws IOException { - IOException failure = null; - try { - index.close(); - } catch (IOException closeFailure) { - failure = closeFailure; - } - try { - bodies.close(); - } catch (IOException closeFailure) { - failure = add(failure, closeFailure); - } - try { - history.close(); - } catch (IOException closeFailure) { - failure = add(failure, closeFailure); - } - if (failure != null) { - throw failure; - } - } - - private static List validateParticipants(List participants) { - List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); - if (copy.isEmpty()) { - throw new IllegalArgumentException("Archive participant set must not be empty"); - } - String previous = null; - for (String participant : copy) { - if (participant == null || participant.isEmpty() - || previous != null && previous.compareTo(participant) >= 0) { - throw new IllegalArgumentException( - "Archive participants must be non-empty, unique, and sorted"); - } - previous = participant; - } - return Collections.unmodifiableList(copy); - } - - private static IOException add(IOException current, IOException addition) { - if (current == null) { - return addition; - } - current.addSuppressed(addition); - return current; - } - - private static boolean isFixed(RecoverySnapshot snapshot) { - long checkpoint = snapshot.getCheckpointHead(); - if (snapshot.getHistoryHead() != checkpoint - || !authoritiesAtCheckpoint(snapshot)) { - return false; - } - return true; - } - - private static boolean authoritiesAtCheckpoint(RecoverySnapshot snapshot) { - long checkpoint = snapshot.getCheckpointHead(); - if (snapshot.getReaderVisibleHead() != checkpoint) { - return false; - } - for (long participant : snapshot.getParticipantHeads().values()) { - if (participant != checkpoint) { - return false; - } - } - return true; - } - - private static byte[] requirePlanDigest(ArchiveTargetMutationPlan plan, - ArchiveProgressEnvelope checkpoint) { - byte[] actual = plan.digest(); - if (!Arrays.equals(actual, checkpoint.getMutationPlanDigest())) { - throw new ArchivePersistenceException( - "Archive checkpoint mutation-plan digest mismatch"); - } - return actual; - } - - private static void close(Closeable resource, Exception failure) { - if (resource == null) { - return; - } - try { - resource.close(); - } catch (IOException closeFailure) { - failure.addSuppressed(closeFailure); - } - } - -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java deleted file mode 100644 index 4962e8e29bb..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderHeadPublisher.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.tron.core.db2.archive; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; - -/** Atomically publishes a reader-visible R identity derived from committed history. */ -public final class ArchiveReaderHeadPublisher { - - private final CommittedHistoryAuthority history; - private final ArchiveProgressFile progressFile; - private final List participants; - - public ArchiveReaderHeadPublisher(CommittedHistoryAuthority history, Path path, - List participants) { - this(history, path, participants, temporary -> { }); - } - - ArchiveReaderHeadPublisher(CommittedHistoryAuthority history, Path path, - List participants, - ArchiveProgressFile.FaultHook faultHook) { - this.history = Objects.requireNonNull(history, "history"); - this.progressFile = new ArchiveProgressFile(Objects.requireNonNull(path, "path"), - new ArchiveProgressEnvelopeCodec(), Objects.requireNonNull(faultHook, "faultHook")); - this.participants = validateParticipants(participants); - } - - public void publish(long epoch) throws IOException { - publish(epoch, null); - } - - public void publish(long epoch, byte[] mutationPlanDigest) throws IOException { - HistoryCommitMarker marker = history.get(epoch); - if (marker == null || marker.getMeta().getEpoch() != epoch - || !marker.getDatabases().equals(participants)) { - throw new ArchivePersistenceException( - "Missing or mismatched committed reader identity at epoch " + epoch); - } - progressFile.store(new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, epoch, - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, participants)); - } - - private static List validateParticipants(List participants) { - Objects.requireNonNull(participants, "participants"); - if (participants.isEmpty()) { - throw new IllegalArgumentException("Archive participant set must not be empty"); - } - List copy = new ArrayList<>(participants.size()); - String previous = null; - for (String participant : participants) { - if (participant == null || participant.isEmpty() - || previous != null && previous.compareTo(participant) >= 0) { - throw new IllegalArgumentException( - "Archive participants must be non-empty, unique, and sorted"); - } - copy.add(participant); - previous = participant; - } - return Collections.unmodifiableList(copy); - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java deleted file mode 100644 index 63961e94685..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReaderPublicationGate.java +++ /dev/null @@ -1,197 +0,0 @@ -package org.tron.core.db2.archive; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.TreeMap; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; - -/** Publishes reader-visible R only after fresh H/C/D identity convergence under one barrier. */ -public final class ArchiveReaderPublicationGate { - - private final CommittedHistoryAuthority history; - private final ProgressSource checkpointSource; - private final Map participantSources; - private final Path readerVisiblePath; - private final ArchiveProgressFile readerVisibleFile; - private final ArchiveReaderHeadPublisher publisher; - private final List participants; - private final ArchiveStateBarrier barrier; - - public ArchiveReaderPublicationGate(CommittedHistoryAuthority history, - ProgressSource checkpointSource, - Map participantSources, - Path readerVisiblePath, List participants, ArchiveStateBarrier barrier) { - this(history, checkpointSource, participantSources, readerVisiblePath, participants, barrier, - temporary -> { }); - } - - ArchiveReaderPublicationGate(CommittedHistoryAuthority history, - ProgressSource checkpointSource, - Map participantSources, - Path readerVisiblePath, List participants, ArchiveStateBarrier barrier, - ArchiveProgressFile.FaultHook faultHook) { - this.history = Objects.requireNonNull(history, "history"); - this.checkpointSource = Objects.requireNonNull(checkpointSource, "checkpointSource"); - this.participants = validateParticipants(participants); - TreeMap sorted = new TreeMap<>( - Objects.requireNonNull(participantSources, "participantSources")); - if (!new ArrayList<>(sorted.keySet()).equals(this.participants) - || sorted.containsValue(null)) { - throw new IllegalArgumentException("Archive publication participant source set mismatch"); - } - this.participantSources = Collections.unmodifiableMap(new LinkedHashMap<>(sorted)); - this.readerVisiblePath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); - this.readerVisibleFile = new ArchiveProgressFile(readerVisiblePath, - new ArchiveProgressEnvelopeCodec()); - this.publisher = new ArchiveReaderHeadPublisher(history, readerVisiblePath, this.participants, - Objects.requireNonNull(faultHook, "faultHook")); - this.barrier = Objects.requireNonNull(barrier, "barrier"); - } - - public static ArchiveReaderPublicationGate forFiles(CommittedHistoryAuthority history, - Path checkpointPath, Map participantPaths, Path readerVisiblePath, - List participants, ArchiveStateBarrier barrier) { - Objects.requireNonNull(checkpointPath, "checkpointPath"); - TreeMap sorted = new TreeMap<>( - Objects.requireNonNull(participantPaths, "participantPaths")); - if (sorted.containsValue(null)) { - throw new IllegalArgumentException("Archive publication participant path is missing"); - } - Map sources = new LinkedHashMap<>(); - ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - sorted.forEach((participant, path) -> sources.put(participant, - () -> new ArchiveProgressFile(path, codec).load())); - return new ArchiveReaderPublicationGate(history, - () -> new ArchiveProgressFile(checkpointPath, codec).load(), sources, - readerVisiblePath, participants, barrier); - } - - public void publish(long targetEpoch) throws IOException { - publishAfterRefresh(targetEpoch, () -> { }); - } - - public void publishAfterRefresh(long targetEpoch, - ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { - if (targetEpoch < 0) { - throw new IllegalArgumentException("Reader publication target must be non-negative"); - } - Objects.requireNonNull(refresh, "refresh"); - barrier.run(() -> { - refresh.run(); - publishInsideBarrier(targetEpoch); - }); - } - - private void publishInsideBarrier(long targetEpoch) throws IOException { - HistoryCommitMarker target = requireMarker(targetEpoch); - validateCurrentReader(targetEpoch); - byte[] firstDigest = validateAuthorities(target); - byte[] secondDigest = validateAuthorities(target); - if (!Arrays.equals(firstDigest, secondDigest)) { - throw new ArchivePersistenceException( - "Archive mutation-plan authority drifted during reader publication"); - } - HistoryCommitMarker reloaded = requireMarker(targetEpoch); - if (!sameIdentity(target, reloaded)) { - throw new ArchivePersistenceException( - "Committed history identity drifted during reader publication"); - } - publisher.publish(targetEpoch, secondDigest); - } - - private HistoryCommitMarker requireMarker(long targetEpoch) { - HistoryCommitMarker marker = history.get(targetEpoch); - if (marker == null || marker.getMeta().getEpoch() != targetEpoch - || !marker.getDatabases().equals(participants)) { - throw new ArchivePersistenceException( - "Missing or mismatched committed publication target: " + targetEpoch); - } - return marker; - } - - private void validateCurrentReader(long targetEpoch) throws IOException { - if (!Files.exists(readerVisiblePath)) { - return; - } - ArchiveProgressEnvelope current = readerVisibleFile.load(); - HistoryCommitMarker marker = requireMarker(current.getEpoch()); - requireIdentity(current, Kind.READER_VISIBLE, null, marker); - if (current.getEpoch() > targetEpoch) { - throw new ArchivePersistenceException("Reader-visible authority cannot move backwards"); - } - } - - private byte[] validateAuthorities(HistoryCommitMarker target) throws IOException { - ArchiveProgressEnvelope checkpoint = load(checkpointSource, "archive apply checkpoint"); - requireIdentity(checkpoint, Kind.APPLY_CHECKPOINT, null, target); - byte[] mutationPlanDigest = checkpoint.getMutationPlanDigest(); - for (Map.Entry entry - : participantSources.entrySet()) { - ArchiveProgressEnvelope progress = entry.getValue().loadProgress(); - if (progress == null) { - throw new ArchivePersistenceException( - "Missing archive participant progress: " + entry.getKey()); - } - requireIdentity(progress, Kind.PARTICIPANT_PROGRESS, entry.getKey(), target); - if (!Arrays.equals(mutationPlanDigest, progress.getMutationPlanDigest())) { - throw new ArchivePersistenceException( - "Archive participant mutation-plan digest mismatch: " + entry.getKey()); - } - } - return mutationPlanDigest; - } - - private ArchiveProgressEnvelope load(ProgressSource source, String name) throws IOException { - ArchiveProgressEnvelope envelope = source.load(); - if (envelope == null) { - throw new ArchivePersistenceException("Missing " + name); - } - return envelope; - } - - private void requireIdentity(ArchiveProgressEnvelope envelope, Kind kind, String participant, - HistoryCommitMarker marker) { - envelope.requireIdentity(kind, participant, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), participants); - } - - private static boolean sameIdentity(HistoryCommitMarker left, HistoryCommitMarker right) { - return left.getMeta().getEpoch() == right.getMeta().getEpoch() - && Arrays.equals(left.getMeta().getBlockHash(), right.getMeta().getBlockHash()) - && Arrays.equals(left.getBatchId(), right.getBatchId()) - && Arrays.equals(left.getHistoryLocation().getBodyDigest(), - right.getHistoryLocation().getBodyDigest()) - && left.getDatabases().equals(right.getDatabases()); - } - - private static List validateParticipants(List participants) { - List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); - if (copy.isEmpty()) { - throw new IllegalArgumentException("Archive publication participant set must not be empty"); - } - String previous = null; - for (String participant : copy) { - if (participant == null || participant.isEmpty() - || previous != null && previous.compareTo(participant) >= 0) { - throw new IllegalArgumentException( - "Archive publication participants must be non-empty, unique, and sorted"); - } - previous = participant; - } - return Collections.unmodifiableList(copy); - } - - @FunctionalInterface - public interface ProgressSource { - ArchiveProgressEnvelope load() throws IOException; - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java deleted file mode 100644 index 6dba5410e5a..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScanner.java +++ /dev/null @@ -1,165 +0,0 @@ -package org.tron.core.db2.archive; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.TreeMap; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; - -/** File-backed prototype authority adapters for one fresh validating recovery scan. */ -public final class ArchiveRecoveryAuthorityScanner { - - private final CommittedHistoryAuthority history; - private final Path checkpointPath; - private final Map participantSources; - private final Path readerVisiblePath; - private final List participants; - private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); - - public ArchiveRecoveryAuthorityScanner(CommittedHistoryAuthority history, Path checkpointPath, - Map participantPaths, Path readerVisiblePath, - List participants) { - this.history = Objects.requireNonNull(history, "history"); - this.checkpointPath = Objects.requireNonNull(checkpointPath, "checkpointPath"); - this.readerVisiblePath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); - this.participants = validateParticipants(participants); - TreeMap sortedPaths = new TreeMap<>( - Objects.requireNonNull(participantPaths, "participantPaths")); - if (!new ArrayList<>(sortedPaths.keySet()).equals(this.participants) - || sortedPaths.containsValue(null)) { - throw new IllegalArgumentException("Archive participant progress path set mismatch"); - } - Map sources = new LinkedHashMap<>(); - sortedPaths.forEach((participant, path) -> - sources.put(participant, () -> progressFile(path).load())); - this.participantSources = Collections.unmodifiableMap(sources); - } - - private ArchiveRecoveryAuthorityScanner(CommittedHistoryAuthority history, Path checkpointPath, - Map participantBatches, Path readerVisiblePath, - List participants, boolean batchAuthority) { - this.history = Objects.requireNonNull(history, "history"); - this.checkpointPath = Objects.requireNonNull(checkpointPath, "checkpointPath"); - this.readerVisiblePath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); - this.participants = validateParticipants(participants); - TreeMap sortedBatches = new TreeMap<>( - Objects.requireNonNull(participantBatches, "participantBatches")); - if (!new ArrayList<>(sortedBatches.keySet()).equals(this.participants) - || sortedBatches.containsValue(null)) { - throw new IllegalArgumentException("Archive participant batch set mismatch"); - } - Map sources = new LinkedHashMap<>(); - sortedBatches.forEach((participant, batch) -> - sources.put(participant, () -> batch.load().getProgress())); - this.participantSources = Collections.unmodifiableMap(sources); - } - - private ArchiveRecoveryAuthorityScanner(CommittedHistoryAuthority history, Path checkpointPath, - Map participantSources, - Path readerVisiblePath, - List participants, byte nativeEngineAuthority) { - this.history = Objects.requireNonNull(history, "history"); - this.checkpointPath = Objects.requireNonNull(checkpointPath, "checkpointPath"); - this.readerVisiblePath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); - this.participants = validateParticipants(participants); - TreeMap sortedSources = new TreeMap<>( - Objects.requireNonNull(participantSources, "participantSources")); - if (!new ArrayList<>(sortedSources.keySet()).equals(this.participants) - || sortedSources.containsValue(null)) { - throw new IllegalArgumentException("Archive participant source set mismatch"); - } - this.participantSources = Collections.unmodifiableMap( - new LinkedHashMap<>(sortedSources)); - } - - public static ArchiveRecoveryAuthorityScanner forParticipantBatches( - CommittedHistoryAuthority history, Path checkpointPath, - Map participantBatches, Path readerVisiblePath, - List participants) { - return new ArchiveRecoveryAuthorityScanner(history, checkpointPath, participantBatches, - readerVisiblePath, participants, true); - } - - public static ArchiveRecoveryAuthorityScanner forParticipants( - CommittedHistoryAuthority history, Path checkpointPath, - Map participantEngines, - Path readerVisiblePath, - List participants) { - Map sources = new LinkedHashMap<>(); - Objects.requireNonNull(participantEngines, "participantEngines") - .forEach(sources::put); - return new ArchiveRecoveryAuthorityScanner(history, checkpointPath, sources, - readerVisiblePath, participants, (byte) 1); - } - - public RecoverySnapshot scan() throws IOException { - ArchiveRecoveryScanner.HistoryIdentitySource historySource = - new ArchiveRecoveryScanner.HistoryIdentitySource() { - @Override - public long committedHeadEpoch() { - HistoryCommitMarker head = history.head(); - if (head == null) { - throw new ArchivePersistenceException("Committed archive history is empty"); - } - return head.getMeta().getEpoch(); - } - - @Override - public HistoryCommitMarker committedMarker(long epoch) { - return history.get(epoch); - } - }; - ArchiveRecoveryScanner.ProgressIdentitySource progressSource = - new ArchiveRecoveryScanner.ProgressIdentitySource() { - @Override - public ArchiveProgressEnvelope loadCheckpoint() throws IOException { - return progressFile(checkpointPath).load(); - } - - @Override - public Map loadParticipantProgress() - throws IOException { - Map loaded = new LinkedHashMap<>(); - for (Map.Entry entry - : participantSources.entrySet()) { - loaded.put(entry.getKey(), entry.getValue().loadProgress()); - } - return loaded; - } - - @Override - public ArchiveProgressEnvelope loadReaderVisible() throws IOException { - return progressFile(readerVisiblePath).load(); - } - }; - return new ArchiveRecoveryScanner(historySource, progressSource, participants).scan(); - } - - private ArchiveProgressFile progressFile(Path path) { - return new ArchiveProgressFile(path, progressCodec); - } - - private static List validateParticipants(List participants) { - Objects.requireNonNull(participants, "participants"); - if (participants.isEmpty()) { - throw new IllegalArgumentException("Archive participant set must not be empty"); - } - List copy = new ArrayList<>(participants.size()); - String previous = null; - for (String participant : participants) { - if (participant == null || participant.isEmpty() - || previous != null && previous.compareTo(participant) >= 0) { - throw new IllegalArgumentException( - "Archive participants must be non-empty, unique, and sorted"); - } - copy.add(participant); - previous = participant; - } - return Collections.unmodifiableList(copy); - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java deleted file mode 100644 index cdd7df4125d..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryExecutor.java +++ /dev/null @@ -1,121 +0,0 @@ -package org.tron.core.db2.archive; - -import java.io.IOException; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.SortedMap; -import java.util.TreeMap; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryAction; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; - -/** Executes a fresh H/C/D[i]/R plan against explicitly durable storage boundaries. */ -public final class ArchiveRecoveryExecutor { - - private final RecoveryStorage storage; - private final FaultHook faultHook; - - public ArchiveRecoveryExecutor(RecoveryStorage storage) { - this(storage, action -> { }); - } - - ArchiveRecoveryExecutor(RecoveryStorage storage, FaultHook faultHook) { - this.storage = Objects.requireNonNull(storage, "storage"); - this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); - } - - public RecoveryPlan recover() { - try { - RecoverySnapshot snapshot = Objects.requireNonNull(storage.scan(), "recovery snapshot"); - RecoveryPlan plan = ArchiveRecoveryPlanner.plan(snapshot.getHistoryHead(), - snapshot.getCheckpointHead(), snapshot.getParticipantHeads(), - snapshot.getReaderVisibleHead()); - for (RecoveryAction action : plan.getActions()) { - execute(action); - faultHook.afterDurableAction(action); - } - storage.recoveryComplete(); - return plan; - } catch (IOException failure) { - throw new ArchivePersistenceException("Archive recovery action failed", failure); - } - } - - private void execute(RecoveryAction action) throws IOException { - ActionType type = action.getType(); - switch (type) { - case TRUNCATE_HISTORY: - storage.truncateHistoryAndSync(action.getLastEpoch()); - return; - case REPLAY_PARTICIPANT: - storage.replayParticipantAndSyncProgress(action.getParticipant(), - action.getFirstEpoch(), action.getLastEpoch()); - return; - case PUBLISH_READER_HEAD: - storage.publishReaderHeadAndSync(action.getLastEpoch()); - return; - default: - throw new ArchivePersistenceException("Unsupported archive recovery action: " + type); - } - } - - /** - * Durable recovery boundary supplied by the archive history, checkpoint and participant engines. - * - *

{@link #replayParticipantAndSyncProgress} must atomically persist the participant's business - * mutations and D[i] progress in one sync engine batch. Returning before both are durable violates - * the recovery contract. - */ - public interface RecoveryStorage { - RecoverySnapshot scan() throws IOException; - - void truncateHistoryAndSync(long historyHead) throws IOException; - - void replayParticipantAndSyncProgress(String participant, long firstEpoch, long lastEpoch) - throws IOException; - - void publishReaderHeadAndSync(long readerVisibleHead) throws IOException; - - default void recoveryComplete() throws IOException { - } - } - - /** Immutable result of one fresh durable H/C/D[i]/R scan. */ - public static final class RecoverySnapshot { - private final long historyHead; - private final long checkpointHead; - private final SortedMap participantHeads; - private final long readerVisibleHead; - - public RecoverySnapshot(long historyHead, long checkpointHead, - Map participantHeads, long readerVisibleHead) { - this.historyHead = historyHead; - this.checkpointHead = checkpointHead; - this.participantHeads = Collections.unmodifiableSortedMap( - new TreeMap<>(Objects.requireNonNull(participantHeads, "participantHeads"))); - this.readerVisibleHead = readerVisibleHead; - } - - public long getHistoryHead() { - return historyHead; - } - - public long getCheckpointHead() { - return checkpointHead; - } - - public SortedMap getParticipantHeads() { - return participantHeads; - } - - public long getReaderVisibleHead() { - return readerVisibleHead; - } - } - - @FunctionalInterface - interface FaultHook { - void afterDurableAction(RecoveryAction action) throws IOException; - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryPlanner.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryPlanner.java deleted file mode 100644 index 8763ceedecc..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryPlanner.java +++ /dev/null @@ -1,181 +0,0 @@ -package org.tron.core.db2.archive; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.SortedMap; -import java.util.TreeMap; - -/** Deterministic fail-closed planner for one durable H/C/D[i]/R recovery snapshot. */ -public final class ArchiveRecoveryPlanner { - - static final long MAX_REPLAY_EPOCHS_PER_ACTION = 1024; - - private ArchiveRecoveryPlanner() { - } - - public static RecoveryPlan plan(long historyHead, long checkpointHead, - Map participantHeads, long readerVisibleHead) { - if (historyHead < 0 || checkpointHead < 0 || readerVisibleHead < 0) { - throw new ArchivePersistenceException("Archive recovery heads must be non-negative"); - } - if (checkpointHead > historyHead) { - throw new ArchivePersistenceException("Archive checkpoint is ahead of history"); - } - SortedMap sortedHeads = validateParticipants(participantHeads, checkpointHead); - long safeHead = Math.min(historyHead, checkpointHead); - for (long participantHead : sortedHeads.values()) { - safeHead = Math.min(safeHead, participantHead); - } - if (readerVisibleHead > safeHead) { - throw new ArchivePersistenceException("Reader-visible archive head is unsafe"); - } - - List actions = new ArrayList<>(); - if (historyHead > checkpointHead) { - actions.add(RecoveryAction.truncateHistory(checkpointHead)); - } - sortedHeads.forEach((participant, appliedHead) -> addReplayActions(actions, participant, - appliedHead, checkpointHead)); - if (readerVisibleHead < checkpointHead) { - actions.add(RecoveryAction.publishReaderHead(checkpointHead)); - } - return new RecoveryPlan(historyHead, checkpointHead, sortedHeads, readerVisibleHead, - safeHead, actions); - } - - private static SortedMap validateParticipants(Map participantHeads, - long checkpointHead) { - Objects.requireNonNull(participantHeads, "participantHeads"); - if (participantHeads.isEmpty()) { - throw new ArchivePersistenceException("Archive recovery participant set is empty"); - } - SortedMap sorted = new TreeMap<>(); - participantHeads.forEach((participant, head) -> { - if (participant == null || participant.isEmpty() || head == null || head < 0) { - throw new ArchivePersistenceException("Archive participant progress is invalid"); - } - if (head > checkpointHead) { - throw new ArchivePersistenceException( - "Archive participant is ahead of the checkpoint: " + participant); - } - sorted.put(participant, head); - }); - return sorted; - } - - private static void addReplayActions(List actions, String participant, - long appliedHead, long checkpointHead) { - if (appliedHead == checkpointHead) { - return; - } - long first = appliedHead + 1; - while (first <= checkpointHead) { - long remaining = checkpointHead - first; - long last = remaining >= MAX_REPLAY_EPOCHS_PER_ACTION - ? first + MAX_REPLAY_EPOCHS_PER_ACTION - 1 : checkpointHead; - actions.add(RecoveryAction.replayParticipant(participant, first, last)); - if (last == checkpointHead) { - break; - } - first = last + 1; - } - } - - public enum ActionType { - TRUNCATE_HISTORY, - REPLAY_PARTICIPANT, - PUBLISH_READER_HEAD - } - - public static final class RecoveryAction { - private final ActionType type; - private final String participant; - private final long firstEpoch; - private final long lastEpoch; - - private RecoveryAction(ActionType type, String participant, long firstEpoch, - long lastEpoch) { - this.type = type; - this.participant = participant; - this.firstEpoch = firstEpoch; - this.lastEpoch = lastEpoch; - } - - private static RecoveryAction truncateHistory(long head) { - return new RecoveryAction(ActionType.TRUNCATE_HISTORY, null, head, head); - } - - private static RecoveryAction replayParticipant(String participant, long firstEpoch, - long lastEpoch) { - return new RecoveryAction(ActionType.REPLAY_PARTICIPANT, participant, firstEpoch, - lastEpoch); - } - - private static RecoveryAction publishReaderHead(long head) { - return new RecoveryAction(ActionType.PUBLISH_READER_HEAD, null, head, head); - } - - public ActionType getType() { - return type; - } - - public String getParticipant() { - return participant; - } - - public long getFirstEpoch() { - return firstEpoch; - } - - public long getLastEpoch() { - return lastEpoch; - } - } - - public static final class RecoveryPlan { - private final long historyHead; - private final long checkpointHead; - private final SortedMap participantHeads; - private final long readerVisibleHead; - private final long safeHeadBeforeRecovery; - private final List actions; - - private RecoveryPlan(long historyHead, long checkpointHead, - SortedMap participantHeads, long readerVisibleHead, - long safeHeadBeforeRecovery, List actions) { - this.historyHead = historyHead; - this.checkpointHead = checkpointHead; - this.participantHeads = Collections.unmodifiableSortedMap(new TreeMap<>(participantHeads)); - this.readerVisibleHead = readerVisibleHead; - this.safeHeadBeforeRecovery = safeHeadBeforeRecovery; - this.actions = Collections.unmodifiableList(new ArrayList<>(actions)); - } - - public long getHistoryHead() { - return historyHead; - } - - public long getCheckpointHead() { - return checkpointHead; - } - - public SortedMap getParticipantHeads() { - return participantHeads; - } - - public long getReaderVisibleHead() { - return readerVisibleHead; - } - - public long getSafeHeadBeforeRecovery() { - return safeHeadBeforeRecovery; - } - - public List getActions() { - return actions; - } - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java deleted file mode 100644 index bcddcb1d002..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRecoveryScanner.java +++ /dev/null @@ -1,127 +0,0 @@ -package org.tron.core.db2.archive; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.SortedMap; -import java.util.TreeMap; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; - -/** Validates durable C and D[i] identities before exposing one recovery snapshot. */ -public final class ArchiveRecoveryScanner { - - private final HistoryIdentitySource history; - private final ProgressIdentitySource progress; - private final List participants; - - public ArchiveRecoveryScanner(HistoryIdentitySource history, ProgressIdentitySource progress, - List participants) { - this.history = Objects.requireNonNull(history, "history"); - this.progress = Objects.requireNonNull(progress, "progress"); - this.participants = validateParticipants(participants); - } - - public RecoverySnapshot scan() throws IOException { - ArchiveProgressEnvelope checkpoint = progress.loadCheckpoint(); - if (checkpoint == null) { - throw new ArchivePersistenceException("Missing archive apply checkpoint"); - } - Map loadedProgress = progress.loadParticipantProgress(); - if (loadedProgress == null) { - throw new ArchivePersistenceException("Missing archive participant progress set"); - } - SortedMap participantProgress = - new TreeMap<>(loadedProgress); - if (!new ArrayList<>(participantProgress.keySet()).equals(participants)) { - throw new ArchivePersistenceException("Archive participant progress set mismatch"); - } - - validateEnvelope(checkpoint, Kind.APPLY_CHECKPOINT, null); - SortedMap participantHeads = new TreeMap<>(); - for (String participant : participants) { - ArchiveProgressEnvelope envelope = participantProgress.get(participant); - if (envelope == null) { - throw new ArchivePersistenceException( - "Missing archive participant progress: " + participant); - } - validateEnvelope(envelope, Kind.PARTICIPANT_PROGRESS, participant); - if (envelope.getEpoch() == checkpoint.getEpoch() - && !Arrays.equals(envelope.getMutationPlanDigest(), - checkpoint.getMutationPlanDigest())) { - throw new ArchivePersistenceException( - "Archive participant mutation-plan digest mismatch: " + participant); - } - participantHeads.put(participant, envelope.getEpoch()); - } - ArchiveProgressEnvelope readerVisible = progress.loadReaderVisible(); - if (readerVisible == null) { - throw new ArchivePersistenceException("Missing archive reader-visible progress"); - } - validateEnvelope(readerVisible, Kind.READER_VISIBLE, null); - if (readerVisible.getEpoch() == checkpoint.getEpoch() - && !Arrays.equals(readerVisible.getMutationPlanDigest(), - checkpoint.getMutationPlanDigest())) { - throw new ArchivePersistenceException( - "Archive reader mutation-plan digest mismatch"); - } - return new RecoverySnapshot(history.committedHeadEpoch(), checkpoint.getEpoch(), - participantHeads, readerVisible.getEpoch()); - } - - private void validateEnvelope(ArchiveProgressEnvelope envelope, Kind kind, String participant) - throws IOException { - HistoryCommitMarker marker = history.committedMarker(envelope.getEpoch()); - if (marker == null) { - throw new ArchivePersistenceException( - "Missing committed history identity at epoch " + envelope.getEpoch()); - } - if (marker.getMeta().getEpoch() != envelope.getEpoch() - || !marker.getDatabases().equals(participants)) { - throw new ArchivePersistenceException( - "Committed history identity mismatch at epoch " + envelope.getEpoch()); - } - envelope.requireIdentity(kind, participant, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), participants); - } - - private static List validateParticipants(List participants) { - Objects.requireNonNull(participants, "participants"); - if (participants.isEmpty()) { - throw new IllegalArgumentException("Archive participant set must not be empty"); - } - List copy = new ArrayList<>(participants.size()); - String previous = null; - for (String participant : participants) { - if (participant == null || participant.isEmpty() - || previous != null && previous.compareTo(participant) >= 0) { - throw new IllegalArgumentException( - "Archive participants must be non-empty, unique, and sorted"); - } - copy.add(participant); - previous = participant; - } - return Collections.unmodifiableList(copy); - } - - /** Committed history identity lookup. A missing epoch returns {@code null}. */ - public interface HistoryIdentitySource { - long committedHeadEpoch() throws IOException; - - HistoryCommitMarker committedMarker(long epoch) throws IOException; - } - - /** Durable apply checkpoint, participant progress and reader-visible head lookup. */ - public interface ProgressIdentitySource { - ArchiveProgressEnvelope loadCheckpoint() throws IOException; - - Map loadParticipantProgress() throws IOException; - - ArchiveProgressEnvelope loadReaderVisible() throws IOException; - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java index 9fffc66b061..3e3a2c8b7ff 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeAttachment.java @@ -1,60 +1,53 @@ package org.tron.core.db2.archive; -import java.io.IOException; -import java.util.List; import java.util.Objects; /** Borrowed archive runtime collaborators installed into SnapshotManager as one unit. */ public final class ArchiveRuntimeAttachment { private final OldValueCollector collector; - private final ArchiveBlockProjectionPreparer projectionPreparer; private final DurableBlockReverseDiffSink sink; - private final ForwardFlushPublisher forwardFlushPublisher; + private final ArchiveCommittedPrefixPublisher committedPrefixPublisher; + private final ArchiveCommittedPrefixPublisher readableStatePublisher; public ArchiveRuntimeAttachment(OldValueCollector collector, - ArchiveBlockProjectionPreparer projectionPreparer, DurableBlockReverseDiffSink sink) { - this(collector, projectionPreparer, sink, null); + DurableBlockReverseDiffSink sink) { + this(collector, sink, null, null); } public ArchiveRuntimeAttachment(OldValueCollector collector, - ArchiveBlockProjectionPreparer projectionPreparer, DurableBlockReverseDiffSink sink, - ForwardFlushPublisher forwardFlushPublisher) { + DurableBlockReverseDiffSink sink, + ArchiveCommittedPrefixPublisher committedPrefixPublisher) { + this(collector, sink, committedPrefixPublisher, null); + } + + public ArchiveRuntimeAttachment(OldValueCollector collector, + DurableBlockReverseDiffSink sink, + ArchiveCommittedPrefixPublisher committedPrefixPublisher, + ArchiveCommittedPrefixPublisher readableStatePublisher) { this.collector = Objects.requireNonNull(collector, "collector"); - this.projectionPreparer = Objects.requireNonNull(projectionPreparer, "projectionPreparer"); this.sink = Objects.requireNonNull(sink, "sink"); - this.forwardFlushPublisher = forwardFlushPublisher; + this.committedPrefixPublisher = committedPrefixPublisher; + this.readableStatePublisher = readableStatePublisher; } public OldValueCollector getCollector() { return collector; } - public ArchiveBlockProjectionPreparer getProjectionPreparer() { - return projectionPreparer; - } - public DurableBlockReverseDiffSink getSink() { return sink; } - public boolean hasForwardFlushPublisher() { - return forwardFlushPublisher != null; - } - - public void publishForwardFlush(List payloads, - ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { - if (forwardFlushPublisher == null) { - throw new IllegalStateException("Archive runtime has no forward flush publisher"); + public void publishCommittedPrefix(BlockSnapshotMeta target) throws java.io.IOException { + if (committedPrefixPublisher != null) { + committedPrefixPublisher.publish(Objects.requireNonNull(target, "target")); } - forwardFlushPublisher.publish(payloads, refresh); } - /** Publishes one frozen normal-flush range target-by-target through C/D, refresh and R. */ - @FunctionalInterface - public interface ForwardFlushPublisher { - - void publish(List payloads, - ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException; + public void publishReadableState(BlockSnapshotMeta target) throws java.io.IOException { + if (readableStatePublisher != null) { + readableStatePublisher.publish(Objects.requireNonNull(target, "target")); + } } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java deleted file mode 100644 index b07f4949382..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinator.java +++ /dev/null @@ -1,196 +0,0 @@ -package org.tron.core.db2.archive; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.TreeMap; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; - -/** Advances one standalone normal target through C, mixed D, latest refresh, and R. */ -public final class ArchiveTargetApplyCoordinator { - - private final CommittedHistoryAuthority history; - private final ArchiveProgressFile checkpointFile; - private final ArchiveTargetMutationPlanFile mutationPlanFile; - private final Map participantEngines; - private final List participants; - private final ArchiveRecoveryAuthorityScanner scanner; - private final ArchiveReaderPublicationGate publicationGate; - private final FaultHook faultHook; - - public ArchiveTargetApplyCoordinator(CommittedHistoryAuthority history, Path checkpointPath, - Map participantEngines, Path readerVisiblePath, - List participants, ArchiveStateBarrier barrier) { - this(history, checkpointPath, participantEngines, readerVisiblePath, participants, barrier, - (stage, participant) -> { }, temporary -> { }, (stage, path) -> { }); - } - - ArchiveTargetApplyCoordinator(CommittedHistoryAuthority history, Path checkpointPath, - Map participantEngines, Path readerVisiblePath, - List participants, ArchiveStateBarrier barrier, FaultHook faultHook, - ArchiveProgressFile.FaultHook publicationFaultHook) { - this(history, checkpointPath, participantEngines, readerVisiblePath, participants, barrier, - faultHook, publicationFaultHook, (stage, path) -> { }); - } - - ArchiveTargetApplyCoordinator(CommittedHistoryAuthority history, Path checkpointPath, - Map participantEngines, Path readerVisiblePath, - List participants, ArchiveStateBarrier barrier, FaultHook faultHook, - ArchiveProgressFile.FaultHook publicationFaultHook, - ArchiveTargetMutationPlanFile.FaultHook planFaultHook) { - this.history = Objects.requireNonNull(history, "history"); - Path checkedCheckpointPath = Objects.requireNonNull(checkpointPath, "checkpointPath"); - Path checkedReaderPath = Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); - this.participants = validateParticipants(participants); - TreeMap sorted = new TreeMap<>( - Objects.requireNonNull(participantEngines, "participantEngines")); - if (!new ArrayList<>(sorted.keySet()).equals(this.participants) - || sorted.containsValue(null)) { - throw new IllegalArgumentException("Archive target participant engine set mismatch"); - } - this.participantEngines = Collections.unmodifiableMap(new LinkedHashMap<>(sorted)); - ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - this.checkpointFile = new ArchiveProgressFile(checkedCheckpointPath, codec); - this.mutationPlanFile = new ArchiveTargetMutationPlanFile(checkedCheckpointPath, - Objects.requireNonNull(planFaultHook, "planFaultHook")); - this.scanner = ArchiveRecoveryAuthorityScanner.forParticipants(history, - checkedCheckpointPath, this.participantEngines, checkedReaderPath, this.participants); - this.publicationGate = new ArchiveReaderPublicationGate(history, checkpointFile::load, - this.participantEngines, checkedReaderPath, this.participants, - Objects.requireNonNull(barrier, "barrier"), - Objects.requireNonNull(publicationFaultHook, "publicationFaultHook")); - this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); - } - - public void apply(long targetEpoch, Phase targetPhase, - Map> mutationPlans, - ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { - HistoryCommitMarker target = validateTarget(targetEpoch); - Map> plans = validatePlans(mutationPlans); - ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlan( - progress(Kind.APPLY_CHECKPOINT, null, target, null), - P66AccountAssetCodec.FORMAT_ID, Objects.requireNonNull(targetPhase, "targetPhase"), plans); - apply(target, plan, refresh); - } - - public void apply(ArchiveParticipantMutationBatch batch, - ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { - ArchiveParticipantMutationBatch input = Objects.requireNonNull(batch, "batch"); - HistoryCommitMarker target = validateTarget(input.getTargetEpoch()); - apply(target, new ArchiveTargetMutationPlanBuilder().build(target, input), refresh); - } - - private void apply(HistoryCommitMarker target, ArchiveTargetMutationPlan plan, - ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { - long targetEpoch = target.getMeta().getEpoch(); - plan.requireIdentity(target, participants); - requireFixedPointBeforeTarget(targetEpoch); - byte[] mutationPlanDigest = plan.digest(); - mutationPlanFile.store(plan); - faultHook.afterDurableStage(Stage.AFTER_PLAN, null); - ArchiveProgressEnvelope checkpoint = progress(Kind.APPLY_CHECKPOINT, null, target, - mutationPlanDigest); - checkpointFile.store(checkpoint); - faultHook.afterDurableStage(Stage.AFTER_CHECKPOINT, null); - for (String participant : participants) { - participantEngines.get(participant).apply(plan.getMutations(participant), - progress(Kind.PARTICIPANT_PROGRESS, participant, target, mutationPlanDigest)); - faultHook.afterDurableStage(Stage.AFTER_PARTICIPANT, participant); - } - publicationGate.publishAfterRefresh(targetEpoch, - Objects.requireNonNull(refresh, "refresh")); - faultHook.afterDurableStage(Stage.AFTER_READER, null); - mutationPlanFile.retire(); - } - - private HistoryCommitMarker validateTarget(long targetEpoch) { - if (targetEpoch < 0) { - throw new IllegalArgumentException("Archive apply target must be non-negative"); - } - HistoryCommitMarker target = history.get(targetEpoch); - if (target == null || target.getMeta().getEpoch() != targetEpoch - || !target.getDatabases().equals(participants)) { - throw new ArchivePersistenceException("Missing or mismatched archive apply target"); - } - return target; - } - - private Map> validatePlans( - Map> mutationPlans) { - TreeMap> sorted = new TreeMap<>( - Objects.requireNonNull(mutationPlans, "mutationPlans")); - if (!new ArrayList<>(sorted.keySet()).equals(participants) - || sorted.containsValue(null)) { - throw new IllegalArgumentException("Archive target mutation plan set mismatch"); - } - Map> copy = new LinkedHashMap<>(); - sorted.forEach((participant, mutations) -> { - List mutationCopy = new ArrayList<>(mutations); - if (mutationCopy.contains(null)) { - throw new IllegalArgumentException("Archive target mutation plan contains null"); - } - copy.put(participant, Collections.unmodifiableList(mutationCopy)); - }); - return Collections.unmodifiableMap(copy); - } - - private void requireFixedPointBeforeTarget(long targetEpoch) throws IOException { - RecoverySnapshot current = scanner.scan(); - long checkpoint = current.getCheckpointHead(); - if (mutationPlanFile.loadIfPresent() != null) { - throw new ArchivePersistenceException("Archive mutation plan requires recovery before apply"); - } - if (current.getHistoryHead() < targetEpoch || checkpoint + 1 != targetEpoch - || current.getReaderVisibleHead() != checkpoint) { - throw new ArchivePersistenceException("Archive apply source is not a safe fixed point"); - } - for (long participantHead : current.getParticipantHeads().values()) { - if (participantHead != checkpoint) { - throw new ArchivePersistenceException("Archive participant requires recovery before apply"); - } - } - } - - private ArchiveProgressEnvelope progress(Kind kind, String participant, - HistoryCommitMarker marker, byte[] mutationPlanDigest) { - return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, participants); - } - - private static List validateParticipants(List participants) { - List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); - if (copy.isEmpty()) { - throw new IllegalArgumentException("Archive target participant set must not be empty"); - } - String previous = null; - for (String participant : copy) { - if (participant == null || participant.isEmpty() - || previous != null && previous.compareTo(participant) >= 0) { - throw new IllegalArgumentException( - "Archive target participants must be non-empty, unique, and sorted"); - } - previous = participant; - } - return Collections.unmodifiableList(copy); - } - - enum Stage { - AFTER_PLAN, - AFTER_CHECKPOINT, - AFTER_PARTICIPANT, - AFTER_READER - } - - @FunctionalInterface - interface FaultHook { - void afterDurableStage(Stage stage, String participant) throws IOException; - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java deleted file mode 100644 index 701877dc3cd..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilder.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.tron.core.db2.archive; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import org.tron.core.db2.archive.ArchiveParticipantMutationBatch.Mutation; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; - -/** Builds one canonical target plan from an immutable exact physical participant batch. */ -final class ArchiveTargetMutationPlanBuilder { - - private final List participants; - - ArchiveTargetMutationPlanBuilder() { - participants = ArchiveParticipantDescriptor.current().getParticipants(); - } - - ArchiveTargetMutationPlan build(HistoryCommitMarker committedTarget, - ArchiveParticipantMutationBatch batch) { - HistoryCommitMarker target = Objects.requireNonNull(committedTarget, "committedTarget"); - ArchiveParticipantMutationBatch input = Objects.requireNonNull(batch, "batch"); - requireTargetIdentity(target, input); - if (!target.getDatabases().equals(participants) - || !input.getParticipants().equals(participants)) { - throw new ArchivePersistenceException( - "Participant mutation batch does not contain the exact VERSIONED_STATE set"); - } - if (!P66AccountAssetCodec.FORMAT_ID.equals(input.getAccountAssetFormatId())) { - throw new ArchivePersistenceException("Unsupported AccountAsset transition format"); - } - Map> grouped = new LinkedHashMap<>(); - for (String participant : participants) { - grouped.put(participant, new ArrayList<>()); - } - for (Mutation mutation : input.getMutations()) { - String dbName = mutation.getDbName(); - List participantMutations = grouped.get(dbName); - if (participantMutations == null || !ArchiveStoreScope.isStateDatabase(dbName)) { - throw new ArchivePersistenceException( - "Unknown or derived archive participant mutation: " + dbName); - } - byte[] value = mutation.getValue(); - participantMutations.add(value == null - ? ArchiveParticipantMutation.delete(mutation.getPhysicalRawKey()) - : ArchiveParticipantMutation.put(mutation.getPhysicalRawKey(), value)); - } - ArchiveProgressEnvelope targetEnvelope = new ArchiveProgressEnvelope( - Kind.APPLY_CHECKPOINT, null, target.getMeta().getEpoch(), - target.getMeta().getBlockHash(), target.getBatchId(), - target.getHistoryLocation().getBodyDigest(), participants); - return new ArchiveTargetMutationPlan(targetEnvelope, input.getAccountAssetFormatId(), - input.getTargetPhase(), grouped); - } - - private void requireTargetIdentity(HistoryCommitMarker target, - ArchiveParticipantMutationBatch batch) { - if (target.getMeta().getEpoch() != batch.getTargetEpoch() - || !Arrays.equals(target.getMeta().getBlockHash(), batch.getBlockHash()) - || !Arrays.equals(target.getBatchId(), batch.getBatchId()) - || !Arrays.equals(target.getHistoryLocation().getBodyDigest(), - batch.getHistoryPayloadDigest()) - || !target.getDatabases().equals(batch.getParticipants())) { - throw new ArchivePersistenceException( - "Participant mutation batch target identity mismatch"); - } - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java index b7120697af1..4e246251693 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java @@ -88,9 +88,9 @@ void clear(Path archiveDirectory) throws IOException { HistorySegmentStore.syncDirectory(archiveDirectory); } - ArchiveRestartCheckpoint persistCheckpoint(Path archiveDirectory, + ArchiveHistoryScanAnchor persistCheckpoint(Path archiveDirectory, HistoryCommitMarkerCodec markerCodec) throws IOException { - return ArchiveRestartCheckpoint.persist(archiveDirectory, firstEpoch, recordCount, + return ArchiveHistoryScanAnchor.persist(archiveDirectory, firstEpoch, recordCount, recordLength, marker, markerCodec); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java index 1f25d2b976f..3ee6ced72be 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java @@ -37,7 +37,7 @@ public boolean recover() throws IOException { } shrinkCommitLog(intent); faultHook.afterDurableStage(Stage.COMMIT_SHRUNK); - ArchiveRestartCheckpoint checkpoint = intent.persistCheckpoint(archiveDirectory, markerCodec); + ArchiveHistoryScanAnchor checkpoint = intent.persistCheckpoint(archiveDirectory, markerCodec); faultHook.afterDurableStage(Stage.CHECKPOINT_PUBLISHED); try (HistoryIndexStore index = new HistoryIndexStore( diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalBinding.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalBinding.java new file mode 100644 index 00000000000..7fb2b0e83e9 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalBinding.java @@ -0,0 +1,178 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import com.google.common.primitives.Ints; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** Immutable history identity stored in the same Chainbase checkpoint batch as state changes. */ +public final class ArchiveWalBinding { + + static final String CHECKPOINT_DATABASE = "__state_archive_wal__"; + private static final byte[] CHECKPOINT_KEY = checkpointKey(); + + private final BlockSnapshotMeta first; + private final BlockSnapshotMeta last; + private final long predecessorEpoch; + private final byte[] predecessorHash; + private final byte[] batchDigest; + private final byte[] storeScopeDigest; + private final byte[] historyRefsDigest; + private final byte[] blockIndexRefsDigest; + + ArchiveWalBinding(BlockSnapshotMeta first, BlockSnapshotMeta last, long predecessorEpoch, + byte[] predecessorHash, byte[] batchDigest, byte[] storeScopeDigest, + byte[] historyRefsDigest, byte[] blockIndexRefsDigest) { + this.first = Objects.requireNonNull(first, "first"); + this.last = Objects.requireNonNull(last, "last"); + this.predecessorEpoch = predecessorEpoch; + this.predecessorHash = hash(predecessorHash, "predecessorHash"); + this.batchDigest = hash(batchDigest, "batchDigest"); + this.storeScopeDigest = hash(storeScopeDigest, "storeScopeDigest"); + this.historyRefsDigest = hash(historyRefsDigest, "historyRefsDigest"); + this.blockIndexRefsDigest = hash(blockIndexRefsDigest, "blockIndexRefsDigest"); + if (predecessorEpoch != first.getEpoch() - 1 + || !Arrays.equals(this.predecessorHash, first.getParentHash()) + || first.getEpoch() > last.getEpoch() + || first.getBlockNumber() > last.getBlockNumber()) { + throw new IllegalArgumentException("Archive WAL binding range is invalid"); + } + } + + public static ArchiveWalBinding fromMarkers(List source) { + List markers = new ArrayList<>( + Objects.requireNonNull(source, "markers")); + if (markers.isEmpty()) { + throw new IllegalArgumentException("Archive WAL binding markers must not be empty"); + } + HistoryCommitMarkerCodec codec = new HistoryCommitMarkerCodec(); + Hasher batch = Hashing.sha256().newHasher(); + Hasher history = Hashing.sha256().newHasher(); + Hasher index = Hashing.sha256().newHasher(); + HistoryCommitMarker previous = null; + List databases = null; + for (HistoryCommitMarker marker : markers) { + HistoryCommitMarker current = Objects.requireNonNull(marker, "marker"); + if (previous != null && (current.getMeta().getEpoch() + != previous.getMeta().getEpoch() + 1 + || current.getMeta().getBlockNumber() + != previous.getMeta().getBlockNumber() + 1 + || current.getPreviousEpoch() != previous.getMeta().getEpoch() + || !Arrays.equals(current.getMeta().getParentHash(), + previous.getMeta().getBlockHash()))) { + throw new IllegalArgumentException("Archive WAL binding markers are not contiguous"); + } + if (databases == null) { + databases = current.getDatabases(); + } else if (!databases.equals(current.getDatabases())) { + throw new IllegalArgumentException("Archive WAL binding Store scope changed in batch"); + } + batch.putBytes(codec.encode(current)); + putHistoryReference(history, current); + putBlockIndexReference(index, current); + previous = current; + } + return new ArchiveWalBinding(markers.get(0).getMeta(), previous.getMeta(), + markers.get(0).getPreviousEpoch(), markers.get(0).getMeta().getParentHash(), + batch.hash().asBytes(), scopeDigest(databases), history.hash().asBytes(), + index.hash().asBytes()); + } + + public BlockSnapshotMeta getFirst() { + return first; + } + + public BlockSnapshotMeta getLast() { + return last; + } + + public long getPredecessorEpoch() { + return predecessorEpoch; + } + + public byte[] getPredecessorHash() { + return Arrays.copyOf(predecessorHash, predecessorHash.length); + } + + public byte[] getBatchDigest() { + return Arrays.copyOf(batchDigest, batchDigest.length); + } + + public byte[] getStoreScopeDigest() { + return Arrays.copyOf(storeScopeDigest, storeScopeDigest.length); + } + + public byte[] getHistoryRefsDigest() { + return Arrays.copyOf(historyRefsDigest, historyRefsDigest.length); + } + + public byte[] getBlockIndexRefsDigest() { + return Arrays.copyOf(blockIndexRefsDigest, blockIndexRefsDigest.length); + } + + public static byte[] getCheckpointKey() { + return Arrays.copyOf(CHECKPOINT_KEY, CHECKPOINT_KEY.length); + } + + public static boolean isCheckpointKey(byte[] key) { + return Arrays.equals(CHECKPOINT_KEY, key); + } + + public static ArchiveWalBinding fromCheckpointBatch(Map batch) { + for (Map.Entry entry : batch.entrySet()) { + if (isCheckpointKey(entry.getKey())) { + return new ArchiveWalBindingCodec().decode(entry.getValue()); + } + } + return null; + } + + private static byte[] checkpointKey() { + byte[] database = CHECKPOINT_DATABASE.getBytes(StandardCharsets.UTF_8); + byte[] field = "binding".getBytes(StandardCharsets.UTF_8); + byte[] key = new byte[Integer.BYTES + database.length + field.length]; + System.arraycopy(Ints.toByteArray(database.length), 0, key, 0, Integer.BYTES); + System.arraycopy(database, 0, key, Integer.BYTES, database.length); + System.arraycopy(field, 0, key, Integer.BYTES + database.length, field.length); + return key; + } + + private static byte[] scopeDigest(List databases) { + List sorted = new ArrayList<>(Objects.requireNonNull(databases, "databases")); + Collections.sort(sorted); + Hasher digest = Hashing.sha256().newHasher(); + digest.putInt(sorted.size()); + for (String database : sorted) { + byte[] encoded = database.getBytes(StandardCharsets.UTF_8); + digest.putInt(encoded.length).putBytes(encoded); + } + return digest.hash().asBytes(); + } + + private static void putHistoryReference(Hasher digest, HistoryCommitMarker marker) { + HistoryLocation location = marker.getHistoryLocation(); + digest.putLong(marker.getMeta().getEpoch()).putInt(location.getSegmentId()) + .putLong(location.getOffset()).putInt(location.getRecordLength()) + .putInt(location.getBodyChecksum()).putBytes(location.getBodyDigest()); + } + + private static void putBlockIndexReference(Hasher digest, HistoryCommitMarker marker) { + HistoryIndexLocation location = marker.getIndexLocation(); + digest.putLong(marker.getMeta().getEpoch()).putLong(marker.getMeta().getBlockNumber()) + .putBytes(marker.getMeta().getBlockHash()).putLong(location.getOffset()) + .putInt(location.getRecordLength()).putBytes(location.getDigest()); + } + + private static byte[] hash(byte[] value, String name) { + if (value == null || value.length != 32) { + throw new IllegalArgumentException(name + " must be exactly 32 bytes"); + } + return Arrays.copyOf(value, value.length); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalBindingCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalBindingCodec.java new file mode 100644 index 00000000000..a916df34a7f --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalBindingCodec.java @@ -0,0 +1,99 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** Versioned and checksummed codec for the Chainbase checkpoint Archive binding. */ +public final class ArchiveWalBindingCodec { + + private static final int MAGIC = 0x54415742; // TAWB + private static final short VERSION = 1; + private static final int ENCODED_LENGTH = 360; + + public byte[] encode(ArchiveWalBinding binding) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(ENCODED_LENGTH); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(ENCODED_LENGTH); + writeMeta(output, binding.getFirst()); + writeMeta(output, binding.getLast()); + output.writeLong(binding.getPredecessorEpoch()); + output.write(binding.getPredecessorHash()); + output.write(binding.getBatchDigest()); + output.write(binding.getStoreScopeDigest()); + output.write(binding.getHistoryRefsDigest()); + output.write(binding.getBlockIndexRefsDigest()); + output.flush(); + byte[] payload = bytes.toByteArray(); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected Archive WAL binding encoding failure", impossible); + } + } + + public ArchiveWalBinding decode(byte[] encoded) { + if (encoded == null || encoded.length != ENCODED_LENGTH) { + throw new IllegalArgumentException("Archive WAL binding length is invalid"); + } + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(encoded, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new IllegalArgumentException("Archive WAL binding checksum mismatch"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != encoded.length) { + throw new IllegalArgumentException("Unsupported Archive WAL binding header"); + } + BlockSnapshotMeta first = readMeta(input); + BlockSnapshotMeta last = readMeta(input); + long predecessorEpoch = input.readLong(); + byte[] predecessorHash = readExact(input, 32); + byte[] batchDigest = readExact(input, 32); + byte[] scopeDigest = readExact(input, 32); + byte[] historyDigest = readExact(input, 32); + byte[] indexDigest = readExact(input, 32); + if (input.available() != Integer.BYTES) { + throw new IllegalArgumentException("Archive WAL binding payload mismatch"); + } + return new ArchiveWalBinding(first, last, predecessorEpoch, predecessorHash, + batchDigest, scopeDigest, historyDigest, indexDigest); + } catch (EOFException truncated) { + throw new IllegalArgumentException("Archive WAL binding is truncated", truncated); + } catch (IOException invalid) { + throw new IllegalArgumentException("Archive WAL binding is invalid", invalid); + } + } + + private static void writeMeta(DataOutputStream output, BlockSnapshotMeta meta) + throws IOException { + output.writeLong(meta.getEpoch()); + output.writeLong(meta.getBlockNumber()); + output.write(meta.getBlockHash()); + output.write(meta.getParentHash()); + output.writeLong(meta.getTimestamp()); + } + + private static BlockSnapshotMeta readMeta(DataInputStream input) throws IOException { + return new BlockSnapshotMeta(input.readLong(), input.readLong(), readExact(input, 32), + readExact(input, 32), input.readLong()); + } + + private static byte[] readExact(DataInputStream input, int length) throws IOException { + byte[] value = new byte[length]; + input.readFully(value); + return value; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalStartupValidator.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalStartupValidator.java new file mode 100644 index 00000000000..8cca6f8d383 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveWalStartupValidator.java @@ -0,0 +1,97 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** Validates recovered Chainbase WAL identity before the normal archive writer is attached. */ +final class ArchiveWalStartupValidator { + + private static final int MAX_FLUSH_MARKERS = 500; + + private ArchiveWalStartupValidator() { + } + + static void requireFixedPoint(ArchiveHistoryWriter history, BlockSnapshotMeta canonicalHead, + ArchiveWalBinding recoveredBinding, List stores) throws IOException { + ArchiveHistoryWriter writer = Objects.requireNonNull(history, "history"); + BlockSnapshotMeta canonical = Objects.requireNonNull(canonicalHead, "canonicalHead"); + List expectedStores = new ArrayList<>(Objects.requireNonNull(stores, "stores")); + HistoryCommitMarker head = writer.committedHead(); + if (head == null || !canonical.equals(head.getMeta())) { + throw new ArchivePersistenceException( + "Archive committed history does not match canonical Chainbase head"); + } + + HistoryCommitMarker first = writer.get(writer.firstEpoch()); + if (first == null) { + throw new ArchivePersistenceException("Archive committed history base marker is missing"); + } + ArchiveBaseManifest.ExistingBase manifest = ArchiveBaseManifest.validateExisting( + writer.getArchiveDirectory(), expectedStores); + if (manifest.getEpoch() != first.getPreviousEpoch() + || !Arrays.equals(manifest.getHash(), first.getMeta().getParentHash())) { + throw new ArchivePersistenceException( + "Archive MANIFEST base does not match committed history"); + } + + HistoryCommitMarker bootstrap = ArchiveBootstrapAnchor.loadAndValidateIfPresent( + writer.getArchiveDirectory(), writer, expectedStores); + if (recoveredBinding == null) { + HistoryCoverage coverage = writer.coverage(); + if (bootstrap == null || coverage.getRecordCount() != 1 + || !bootstrap.getMeta().equals(head.getMeta())) { + throw new ArchivePersistenceException( + "Archive WAL binding is missing outside the exact fresh bootstrap baseline"); + } + return; + } + if (bootstrap != null && writer.coverage().getRecordCount() == 1) { + throw new ArchivePersistenceException( + "Fresh bootstrap baseline must not claim a normal Archive WAL binding"); + } + if (!recoveredBinding.getLast().equals(head.getMeta())) { + throw new ArchivePersistenceException( + "Recovered Archive WAL binding does not end at committed H/canonical P"); + } + long count; + try { + count = Math.addExact(Math.subtractExact(recoveredBinding.getLast().getEpoch(), + recoveredBinding.getFirst().getEpoch()), 1L); + } catch (ArithmeticException invalid) { + throw new ArchivePersistenceException("Recovered Archive WAL binding range overflows", + invalid); + } + if (count <= 0 || count > MAX_FLUSH_MARKERS) { + throw new ArchivePersistenceException("Recovered Archive WAL binding range is invalid"); + } + List markers = new ArrayList<>((int) count); + for (long epoch = recoveredBinding.getFirst().getEpoch(); + epoch <= recoveredBinding.getLast().getEpoch(); epoch++) { + HistoryCommitMarker marker = writer.get(epoch); + if (marker == null) { + throw new ArchivePersistenceException( + "Recovered Archive WAL binding references missing committed history"); + } + markers.add(marker); + } + ArchiveWalBinding expected = ArchiveWalBinding.fromMarkers(markers); + if (!sameIdentity(recoveredBinding, expected)) { + throw new ArchivePersistenceException( + "Recovered Archive WAL binding differs from committed history refs"); + } + } + + private static boolean sameIdentity(ArchiveWalBinding left, ArchiveWalBinding right) { + return left.getFirst().equals(right.getFirst()) + && left.getLast().equals(right.getLast()) + && left.getPredecessorEpoch() == right.getPredecessorEpoch() + && Arrays.equals(left.getPredecessorHash(), right.getPredecessorHash()) + && Arrays.equals(left.getBatchDigest(), right.getBatchDigest()) + && Arrays.equals(left.getStoreScopeDigest(), right.getStoreScopeDigest()) + && Arrays.equals(left.getHistoryRefsDigest(), right.getHistoryRefsDigest()) + && Arrays.equals(left.getBlockIndexRefsDigest(), right.getBlockIndexRefsDigest()); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeEvidence.java b/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeEvidence.java index 9692daf5896..bde5743de4c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeEvidence.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/DurableHistoryMarkerRangeEvidence.java @@ -5,7 +5,6 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; /** Bounded authoritative marker evidence for one exact frozen flush range. */ public final class DurableHistoryMarkerRangeEvidence { @@ -28,11 +27,6 @@ public DurableHistoryMarkerRangeEvidence(ArchiveHistoryWriter writer, int maxMar participants = ArchiveParticipantDescriptor.current().getParticipants(); } - public List seal(FrozenBatch batch) { - FrozenBatch target = Objects.requireNonNull(batch, "batch"); - return target.seal(read(target.getExpectedMetas())); - } - public List read(List expectedMetas) { List expected = new ArrayList<>( Objects.requireNonNull(expectedMetas, "expectedMetas")); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java index c931ed36a84..4e3516139ce 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryCommitStore.java @@ -45,12 +45,12 @@ public HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec) } HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec, - ArchiveRestartCheckpoint checkpoint) throws IOException { + ArchiveHistoryScanAnchor checkpoint) throws IOException { this(archiveDirectory, codec, checkpoint, ignored -> { }); } HistoryCommitStore(Path archiveDirectory, HistoryCommitMarkerCodec codec, - ArchiveRestartCheckpoint checkpoint, DirectorySync postForceHook) throws IOException { + ArchiveHistoryScanAnchor checkpoint, DirectorySync postForceHook) throws IOException { this.directory = archiveDirectory.resolve("commits"); this.logPath = directory.resolve(FILE_NAME); this.codec = codec; @@ -223,7 +223,7 @@ long getStartupScannedRecords() { return startupScannedRecords; } - private void scanAndRepairTruncatedTail(ArchiveRestartCheckpoint checkpoint) + private void scanAndRepairTruncatedTail(ArchiveHistoryScanAnchor checkpoint) throws IOException { long offset = 0; HistoryCommitMarker previous = null; @@ -237,13 +237,13 @@ private void scanAndRepairTruncatedTail(ArchiveRestartCheckpoint checkpoint) long checkpointOffset = (count - 1) * (long) expectedLength; if (checkpointOffset < 0 || checkpointOffset + expectedLength > size) { throw new ArchivePersistenceException( - "Restart checkpoint is outside the committed history log"); + "History scan anchor is outside the committed history log"); } byte[] checkpointRecord = read(checkpointOffset, expectedLength); startupScannedRecords++; if (!java.util.Arrays.equals(checkpointRecord, checkpoint.getEncodedMarker())) { throw new ArchivePersistenceException( - "Restart checkpoint does not match the committed history log"); + "History scan anchor does not match the committed history log"); } previous = codec.decode(checkpointRecord); offset = checkpointOffset + expectedLength; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java index cb173374062..e1cf97bb99c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryIndexStore.java @@ -28,7 +28,7 @@ public HistoryIndexStore(Path archiveDirectory, HistoryIndexCodec codec) throws } HistoryIndexStore(Path archiveDirectory, HistoryIndexCodec codec, - ArchiveRestartCheckpoint checkpoint) throws IOException { + ArchiveHistoryScanAnchor checkpoint) throws IOException { this.archiveDirectory = archiveDirectory; this.indexPath = archiveDirectory.resolve("state_history.idx"); this.codec = codec; @@ -125,7 +125,7 @@ long getStartupScannedRecords() { return startupScannedRecords; } - private ScanResult scan(ArchiveRestartCheckpoint checkpoint) throws IOException { + private ScanResult scan(ArchiveHistoryScanAnchor checkpoint) throws IOException { long recordCount = 0; ScannedIndexRecord head = null; Long invalidOffset = null; @@ -139,7 +139,7 @@ private ScanResult scan(ArchiveRestartCheckpoint checkpoint) throws IOException startupScannedRecords++; if (!marker.getMeta().equals(record.getMeta())) { throw new ArchivePersistenceException( - "Restart checkpoint does not match the history index"); + "History scan anchor does not match the history index"); } recordCount = checkpoint.getRecordCount(); head = new ScannedIndexRecord(record, location); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java index 73cb9c917cd..4374041e005 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java @@ -36,7 +36,7 @@ public HistorySegmentStore(Path archiveDirectory, BlockHistoryCodec codec, long } HistorySegmentStore(Path archiveDirectory, BlockHistoryCodec codec, long maxSegmentSize, - ArchiveRestartCheckpoint checkpoint) throws IOException { + ArchiveHistoryScanAnchor checkpoint) throws IOException { if (maxSegmentSize <= 0) { throw new IllegalArgumentException("maxSegmentSize must be positive"); } @@ -149,7 +149,7 @@ long getStartupScannedRecords() { return startupScannedRecords; } - private ScanResult scan(ArchiveRestartCheckpoint checkpoint) throws IOException { + private ScanResult scan(ArchiveHistoryScanAnchor checkpoint) throws IOException { long recordCount = 0; ScannedRecord head = null; InvalidTail invalidTail = null; @@ -163,7 +163,7 @@ private ScanResult scan(ArchiveRestartCheckpoint checkpoint) throws IOException startupScannedRecords++; if (!marker.getMeta().equals(diff.getMeta())) { throw new ArchivePersistenceException( - "Restart checkpoint does not match the history body"); + "History scan anchor does not match the history body"); } recordCount = checkpoint.getRecordCount(); head = new ScannedRecord(diff, marker.getHistoryLocation()); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java index 204a4a51940..2aca0962138 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java @@ -13,6 +13,9 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; +import org.tron.common.storage.leveldb.LevelDbDataSourceImpl; +import org.tron.common.storage.rocksdb.RocksDbDataSourceImpl; +import org.tron.core.db.common.DbSourceInter; import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestStateFactory; import org.tron.core.db2.common.DB; @@ -66,6 +69,90 @@ public static LatestStateGenerationAdapter fromDatabases(List participan return new LatestStateGenerationAdapter(participants, capable); } + /** Adapts one out-of-registry native Store to the same stable snapshot contract. */ + public static SnapshotCapableStore fromDataSource(String dbName, + DbSourceInter source) throws ArchivePersistenceException { + Objects.requireNonNull(dbName, "dbName"); + Objects.requireNonNull(source, "source"); + if (!dbName.equals(source.getDBName())) { + throw new ArchivePersistenceException("Supplemental latest Store name mismatch: " + dbName); + } + if (source instanceof LevelDbDataSourceImpl) { + LevelDbDataSourceImpl level = (LevelDbDataSourceImpl) source; + return capable(dbName, level.getSnapshotSourceIdentity(), (blockNumber, blockHash) -> { + LevelDbDataSourceImpl.PinnedSnapshot pinned = level.pinSnapshot(); + return snapshot(dbName, pinned.getSourceIdentity(), blockNumber, blockHash, + pinned::get, pinned::close); + }); + } + if (source instanceof RocksDbDataSourceImpl) { + RocksDbDataSourceImpl rocks = (RocksDbDataSourceImpl) source; + return capable(dbName, rocks.getSnapshotSourceIdentity(), (blockNumber, blockHash) -> { + RocksDbDataSourceImpl.PinnedSnapshot pinned = rocks.pinSnapshot(); + return snapshot(dbName, pinned.getSourceIdentity(), blockNumber, blockHash, + pinned::get, pinned::close); + }); + } + throw new ArchivePersistenceException( + "Supplemental latest Store lacks a supported native snapshot engine: " + dbName); + } + + private static SnapshotCapableStore capable(String dbName, String sourceIdentity, + SnapshotFactory factory) { + return new SnapshotCapableStore() { + @Override + public String getDbName() { + return dbName; + } + + @Override + public String getSourceIdentity() { + return sourceIdentity; + } + + @Override + public StoreSnapshot pin(long blockNumber, byte[] blockHash) throws IOException { + return factory.pin(blockNumber, blockHash); + } + }; + } + + private static StoreSnapshot snapshot(String dbName, String sourceIdentity, long blockNumber, + byte[] blockHash, PointReader reader, CloseableAction close) { + byte[] expectedHash = Arrays.copyOf(blockHash, blockHash.length); + return new StoreSnapshot() { + @Override + public String getDbName() { + return dbName; + } + + @Override + public String getSourceIdentity() { + return sourceIdentity; + } + + @Override + public long getBlockNumber() { + return blockNumber; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(expectedHash, expectedHash.length); + } + + @Override + public byte[] get(byte[] physicalRawKey) { + return reader.get(physicalRawKey); + } + + @Override + public void close() throws IOException { + close.close(); + } + }; + } + @Override public PinnedLatestState pin(PersistentServingKeyIndexGeneration serving) throws IOException { Objects.requireNonNull(serving, "serving"); @@ -189,6 +276,21 @@ public interface StoreSnapshot extends Closeable { byte[] get(byte[] physicalRawKey) throws IOException; } + @FunctionalInterface + private interface SnapshotFactory { + StoreSnapshot pin(long blockNumber, byte[] blockHash) throws IOException; + } + + @FunctionalInterface + private interface PointReader { + byte[] get(byte[] key); + } + + @FunctionalInterface + private interface CloseableAction { + void close() throws IOException; + } + private static final class PinnedGeneration implements PinnedLatestState { private final String generationId; private final long blockNumber; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java index 99117720c67..908b039ed39 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java @@ -9,8 +9,6 @@ import java.util.TreeSet; import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; import org.tron.core.db2.common.DB; -import org.tron.core.db2.common.LevelDB; -import org.tron.core.db2.common.RocksDB; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.Snapshot; import org.tron.core.db2.core.SnapshotManager; @@ -24,8 +22,25 @@ private LatestStateGenerationCoordinatorFactory() { public static LatestStateGenerationCoordinator create(SnapshotManager manager, Path readerVisiblePath) throws ArchivePersistenceException { - Objects.requireNonNull(manager, "manager"); Objects.requireNonNull(readerVisiblePath, "readerVisiblePath"); + ArchiveProgressFile readerVisible = new ArchiveProgressFile(readerVisiblePath, + new ArchiveProgressEnvelopeCodec()); + return create(manager, readerVisible::load); + } + + public static LatestStateGenerationCoordinator create(SnapshotManager manager, + LatestStateGenerationCoordinator.AuthorityReader authorityReader) + throws ArchivePersistenceException { + return create(manager, java.util.Collections.emptyMap(), authorityReader); + } + + public static LatestStateGenerationCoordinator create(SnapshotManager manager, + Map supplementalStores, + LatestStateGenerationCoordinator.AuthorityReader authorityReader) + throws ArchivePersistenceException { + Objects.requireNonNull(manager, "manager"); + Objects.requireNonNull(supplementalStores, "supplementalStores"); + Objects.requireNonNull(authorityReader, "authorityReader"); List registered = new ArrayList<>(manager.getDbs()); try { ArchiveStoreScope.validate(registered); @@ -41,9 +56,9 @@ public static LatestStateGenerationCoordinator create(SnapshotManager manager, } } TreeSet expected = new TreeSet<>(ArchiveStoreScope.getStateDatabases()); - if (!stateDatabases.keySet().equals(expected)) { + if (!expected.containsAll(stateDatabases.keySet())) { throw new ArchivePersistenceException( - "SnapshotManager archive state Store set is incomplete or unexpected"); + "SnapshotManager archive state Store set is unexpected"); } TreeMap stores = new TreeMap<>(); @@ -54,19 +69,29 @@ public static LatestStateGenerationCoordinator create(SnapshotManager manager, "Archive state Store does not resolve to SnapshotRoot: " + entry.getKey()); } DB engine = ((SnapshotRoot) root).getDb(); - if (!(engine instanceof LevelDB || engine instanceof RocksDB) - || !(engine instanceof SnapshotCapableStore) + if (!(engine instanceof SnapshotCapableStore) || !entry.getKey().equals(engine.getDbName())) { throw new ArchivePersistenceException( "Archive state Store root lacks a supported snapshot engine: " + entry.getKey()); } stores.put(entry.getKey(), (SnapshotCapableStore) engine); } + for (Map.Entry entry : supplementalStores.entrySet()) { + SnapshotCapableStore store = Objects.requireNonNull(entry.getValue(), + "supplemental Store"); + if (!entry.getKey().equals(store.getDbName()) + || stores.putIfAbsent(entry.getKey(), store) != null) { + throw new ArchivePersistenceException( + "Duplicate or mismatched supplemental latest Store: " + entry.getKey()); + } + } + if (!stores.keySet().equals(expected)) { + throw new ArchivePersistenceException( + "SnapshotManager plus supplemental archive Store set is incomplete or unexpected"); + } List participants = new ArrayList<>(stores.keySet()); - ArchiveProgressFile readerVisible = new ArchiveProgressFile(readerVisiblePath, - new ArchiveProgressEnvelopeCodec()); return new LatestStateGenerationCoordinator(participants, stores, - manager::withArchiveStateBarrier, readerVisible::load); + manager::withArchiveStateBarrier, authorityReader); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java b/chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java deleted file mode 100644 index 11c75e04e0d..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/LevelDbArchiveParticipant.java +++ /dev/null @@ -1,158 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.fusesource.leveldbjni.JniDBFactory.factory; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import org.iq80.leveldb.DB; -import org.iq80.leveldb.Options; -import org.iq80.leveldb.WriteBatch; -import org.iq80.leveldb.WriteOptions; - -/** LevelDB participant whose business mutations and D[i] share one synced native WriteBatch. */ -public final class LevelDbArchiveParticipant implements Closeable, ArchiveParticipant { - - private static final byte BUSINESS_PREFIX = 1; - private static final byte[] PROGRESS_KEY = new byte[]{0, 'p', 'r', 'o', 'g', 'r', 'e', 's', 's'}; - - private final Path directory; - private final String participant; - private final List participants; - private final Options options = new Options().createIfMissing(true); - private final WriteOptions syncWrites = new WriteOptions().sync(true); - private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); - private final FaultHook faultHook; - private DB database; - - public LevelDbArchiveParticipant(Path directory, String participant, - List participants) throws IOException { - this(directory, participant, participants, stage -> { }); - } - - LevelDbArchiveParticipant(Path directory, String participant, List participants, - FaultHook faultHook) throws IOException { - this.directory = Objects.requireNonNull(directory, "directory"); - this.participant = Objects.requireNonNull(participant, "participant"); - this.participants = validateParticipants(participants); - if (!this.participants.contains(participant)) { - throw new IllegalArgumentException("Archive participant is outside the exact set"); - } - this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); - Files.createDirectories(directory); - database = open(); - } - - @Override - public synchronized void apply(List mutations, - ArchiveProgressEnvelope progress) - throws IOException { - Objects.requireNonNull(mutations, "mutations"); - requireProgress(progress); - try (WriteBatch batch = database.createWriteBatch()) { - for (ArchiveParticipantMutation mutation : mutations) { - Objects.requireNonNull(mutation, "mutation"); - byte[] value = mutation.getValue(); - if (value == null) { - batch.delete(businessKey(mutation.getKey())); - } else { - batch.put(businessKey(mutation.getKey()), value); - } - } - batch.put(PROGRESS_KEY, progressCodec.encode(progress)); - faultHook.atStage(Stage.BEFORE_WRITE); - database.write(batch, syncWrites); - faultHook.atStage(Stage.AFTER_WRITE); - } - } - - public synchronized byte[] get(byte[] key) { - byte[] value = database.get(businessKey(key)); - return value == null ? null : Arrays.copyOf(value, value.length); - } - - @Override - public synchronized ArchiveProgressEnvelope loadProgress() { - byte[] encoded = database.get(PROGRESS_KEY); - if (encoded == null) { - throw new ArchivePersistenceException("Archive participant progress is missing"); - } - try { - ArchiveProgressEnvelope progress = progressCodec.decode(encoded); - requireProgress(progress); - return progress; - } catch (IllegalArgumentException invalid) { - throw new ArchivePersistenceException("Archive participant progress is corrupt", invalid); - } - } - - public synchronized void reset() throws IOException { - database.close(); - database = null; - try { - factory.destroy(directory.toFile(), options); - } finally { - database = open(); - } - } - - @Override - public synchronized void close() throws IOException { - if (database != null) { - database.close(); - database = null; - } - } - - private DB open() throws IOException { - return factory.open(directory.toFile(), options); - } - - private void requireProgress(ArchiveProgressEnvelope progress) { - Objects.requireNonNull(progress, "progress"); - if (progress.getKind() != ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS - || !participant.equals(progress.getParticipant()) - || !participants.equals(progress.getParticipants())) { - throw new IllegalArgumentException("Archive participant progress identity mismatch"); - } - } - - private static byte[] businessKey(byte[] key) { - Objects.requireNonNull(key, "key"); - return ByteBuffer.allocate(1 + key.length).put(BUSINESS_PREFIX).put(key).array(); - } - - private static List validateParticipants(List participants) { - List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); - if (copy.isEmpty()) { - throw new IllegalArgumentException("Archive participant set must not be empty"); - } - String previous = null; - for (String current : copy) { - if (current == null || current.isEmpty() - || previous != null && previous.compareTo(current) >= 0) { - throw new IllegalArgumentException( - "Archive participants must be non-empty, unique, and sorted"); - } - previous = current; - } - return Collections.unmodifiableList(copy); - } - - enum Stage { - BEFORE_WRITE, - AFTER_WRITE - } - - @FunctionalInterface - interface FaultHook { - void atStage(Stage stage) throws IOException; - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java index d5a3362f0c5..d5ceacd2f2b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java @@ -25,10 +25,10 @@ public final class PersistentCommittedHistoryReader private PersistentCommittedHistoryReader(Path archiveDirectory, long maxSegmentSize, PersistentServingKeyIndexGeneration serving) throws IOException { Objects.requireNonNull(serving, "serving"); - ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archiveDirectory, + ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archiveDirectory, new HistoryCommitMarkerCodec()); if (checkpoint == null) { - throw new ArchivePersistenceException("Archive restart checkpoint is missing"); + throw new ArchivePersistenceException("Archive history scan anchor is missing"); } HistorySegmentStore openedBodies = null; HistoryIndexStore openedIndex = null; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java index 98b360d321d..110f5d92753 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java @@ -69,6 +69,32 @@ static PersistentServingKeyIndexCatalog open(Path root, FaultHook faultHook) public static PersistentServingKeyIndexCatalog create(Path root, Path initialShadow, ArchiveProgressEnvelope readerVisible) throws IOException { + return createInternal(root, initialShadow, readerVisible); + } + + public static PersistentServingKeyIndexCatalog create(Path root, Path initialShadow) + throws IOException { + return createInternal(root, initialShadow, null); + } + + static PersistentServingKeyIndexCatalog create(Path root, Path initialShadow, + FaultHook faultHook) throws IOException { + Objects.requireNonNull(faultHook, "faultHook"); + Objects.requireNonNull(root, "root"); + if (Files.exists(root.resolve(CURRENT))) { + throw new IllegalArgumentException("Serving index catalog already exists"); + } + Files.createDirectories(root.resolve(GENERATIONS)); + PersistentServingKeyIndexCatalog catalog = + new PersistentServingKeyIndexCatalog(root, null, faultHook); + if (!catalog.publishInternal(null, initialShadow, null)) { + throw new IllegalStateException("Failed to publish initial serving generation"); + } + return catalog; + } + + private static PersistentServingKeyIndexCatalog createInternal(Path root, Path initialShadow, + ArchiveProgressEnvelope readerVisible) throws IOException { Objects.requireNonNull(root, "root"); if (Files.exists(root.resolve(CURRENT))) { throw new IllegalArgumentException("Serving index catalog already exists"); @@ -76,7 +102,7 @@ public static PersistentServingKeyIndexCatalog create(Path root, Path initialSha Files.createDirectories(root.resolve(GENERATIONS)); PersistentServingKeyIndexCatalog catalog = new PersistentServingKeyIndexCatalog(root, null, stage -> { }); - if (!catalog.publish(null, initialShadow, readerVisible)) { + if (!catalog.publishInternal(null, initialShadow, readerVisible)) { throw new IllegalStateException("Failed to publish initial serving generation"); } return catalog; @@ -85,6 +111,15 @@ public static PersistentServingKeyIndexCatalog create(Path root, Path initialSha /** Pins one immutable RocksDB handle and holds its generation refcount until close. */ public synchronized PersistentServingKeyIndexGeneration pin( ArchiveProgressEnvelope readerVisible) throws IOException { + return pinInternal(readerVisible); + } + + public synchronized PersistentServingKeyIndexGeneration pin() throws IOException { + return pinInternal(null); + } + + private PersistentServingKeyIndexGeneration pinInternal( + ArchiveProgressEnvelope readerVisible) throws IOException { ensureOpen(); if (currentId == null) { throw new ArchivePersistenceException("Serving index catalog has no current generation"); @@ -100,7 +135,9 @@ public synchronized PersistentServingKeyIndexGeneration pin( throw failure; } try { - validateReaderVisibility(pinned, readerVisible); + if (readerVisible != null) { + validateReaderVisibility(pinned, readerVisible); + } return pinned; } catch (RuntimeException failure) { pinned.close(); @@ -111,7 +148,17 @@ public synchronized PersistentServingKeyIndexGeneration pin( /** Atomically publishes a completed shadow generation if {@code expectedId} is still current. */ public synchronized boolean publish(String expectedId, Path shadow, ArchiveProgressEnvelope readerVisible) throws IOException { + return publishInternal(expectedId, shadow, readerVisible); + } + + public synchronized boolean publish(String expectedId, Path shadow) throws IOException { + return publishInternal(expectedId, shadow, null); + } + + private boolean publishInternal(String expectedId, Path shadow, + ArchiveProgressEnvelope readerVisible) throws IOException { ensureOpen(); + discoverRetired(); if (!Objects.equals(expectedId, currentId)) { return false; } @@ -120,7 +167,9 @@ public synchronized boolean publish(String expectedId, Path shadow, long replacementThrough; try (PersistentServingKeyIndexGeneration replacement = PersistentServingKeyIndexGeneration.open(shadow)) { - validateReaderVisibility(replacement, readerVisible); + if (readerVisible != null) { + validateReaderVisibility(replacement, readerVisible); + } replacementId = replacement.getGenerationId(); replacementFrom = replacement.getIndexedFrom(); replacementThrough = replacement.getIndexedThrough(); @@ -148,11 +197,13 @@ public synchronized boolean publish(String expectedId, Path shadow, HistorySegmentStore.syncDirectory(generations); faultHook.afterDurableStage(PublicationStage.GENERATION_INSTALLED); persistCurrent(replacementId); - faultHook.afterDurableStage(PublicationStage.CURRENT_PUBLISHED); String previous = currentId; currentId = replacementId; if (previous != null) { retired.add(previous); + } + faultHook.afterDurableStage(PublicationStage.CURRENT_PUBLISHED); + if (previous != null) { reapIfUnused(previous); } return true; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java b/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java deleted file mode 100644 index 1c5cbea10f7..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/RocksDbArchiveParticipant.java +++ /dev/null @@ -1,164 +0,0 @@ -package org.tron.core.db2.archive; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import org.rocksdb.Options; -import org.rocksdb.RocksDB; -import org.rocksdb.RocksDBException; -import org.rocksdb.WriteBatch; -import org.rocksdb.WriteOptions; - -/** RocksDB participant whose business mutations and D[i] share one synced native WriteBatch. */ -public final class RocksDbArchiveParticipant implements Closeable, ArchiveParticipant { - - private static final byte BUSINESS_PREFIX = 1; - private static final byte[] PROGRESS_KEY = new byte[]{0, 'p', 'r', 'o', 'g', 'r', 'e', 's', 's'}; - - static { - RocksDB.loadLibrary(); - } - - private final String participant; - private final List participants; - private final Options options = new Options().setCreateIfMissing(true); - private final WriteOptions syncWrites = new WriteOptions().setSync(true); - private final RocksDB database; - private final ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); - private final FaultHook faultHook; - - public RocksDbArchiveParticipant(Path directory, String participant, - List participants) throws IOException { - this(directory, participant, participants, stage -> { }); - } - - RocksDbArchiveParticipant(Path directory, String participant, List participants, - FaultHook faultHook) throws IOException { - this.participant = Objects.requireNonNull(participant, "participant"); - this.participants = validateParticipants(participants); - if (!this.participants.contains(participant)) { - throw new IllegalArgumentException("Archive participant is outside the exact set"); - } - this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); - try { - Files.createDirectories(directory); - } catch (IOException failure) { - options.close(); - syncWrites.close(); - throw failure; - } - try { - database = RocksDB.open(options, directory.toString()); - } catch (RocksDBException failure) { - options.close(); - syncWrites.close(); - throw new IOException("Failed to open archive participant engine", failure); - } - } - - @Override - public synchronized void apply(List mutations, - ArchiveProgressEnvelope progress) - throws IOException { - Objects.requireNonNull(mutations, "mutations"); - requireProgress(progress); - try (WriteBatch batch = new WriteBatch()) { - for (ArchiveParticipantMutation mutation : mutations) { - Objects.requireNonNull(mutation, "mutation"); - byte[] value = mutation.getValue(); - if (value == null) { - batch.delete(businessKey(mutation.getKey())); - } else { - batch.put(businessKey(mutation.getKey()), value); - } - } - batch.put(PROGRESS_KEY, progressCodec.encode(progress)); - faultHook.atStage(Stage.BEFORE_WRITE); - database.write(syncWrites, batch); - faultHook.atStage(Stage.AFTER_WRITE); - } catch (RocksDBException failure) { - throw new IOException("Failed to apply archive participant batch", failure); - } - } - - public synchronized byte[] get(byte[] key) throws IOException { - try { - byte[] value = database.get(businessKey(key)); - return value == null ? null : Arrays.copyOf(value, value.length); - } catch (RocksDBException failure) { - throw new IOException("Failed to read archive participant business state", failure); - } - } - - @Override - public synchronized ArchiveProgressEnvelope loadProgress() throws IOException { - try { - byte[] encoded = database.get(PROGRESS_KEY); - if (encoded == null) { - throw new ArchivePersistenceException("Archive participant progress is missing"); - } - ArchiveProgressEnvelope progress = progressCodec.decode(encoded); - requireProgress(progress); - return progress; - } catch (RocksDBException failure) { - throw new IOException("Failed to read archive participant progress", failure); - } catch (IllegalArgumentException invalid) { - throw new ArchivePersistenceException("Archive participant progress is corrupt", invalid); - } - } - - @Override - public synchronized void close() { - syncWrites.close(); - database.close(); - options.close(); - } - - private void requireProgress(ArchiveProgressEnvelope progress) { - Objects.requireNonNull(progress, "progress"); - if (progress.getKind() != ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS - || !participant.equals(progress.getParticipant()) - || !participants.equals(progress.getParticipants())) { - throw new IllegalArgumentException("Archive participant progress identity mismatch"); - } - } - - private static byte[] businessKey(byte[] key) { - Objects.requireNonNull(key, "key"); - return ByteBuffer.allocate(1 + key.length).put(BUSINESS_PREFIX).put(key).array(); - } - - private static List validateParticipants(List participants) { - List copy = new ArrayList<>(Objects.requireNonNull(participants, "participants")); - if (copy.isEmpty()) { - throw new IllegalArgumentException("Archive participant set must not be empty"); - } - String previous = null; - for (String current : copy) { - if (current == null || current.isEmpty() - || previous != null && previous.compareTo(current) >= 0) { - throw new IllegalArgumentException( - "Archive participants must be non-empty, unique, and sorted"); - } - previous = current; - } - return Collections.unmodifiableList(copy); - } - - enum Stage { - BEFORE_WRITE, - AFTER_WRITE - } - - @FunctionalInterface - interface FaultHook { - void atStage(Stage stage) throws IOException; - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java index dcd149b732d..b7292e5c697 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java @@ -1,5 +1,6 @@ package org.tron.core.db2.archive; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -14,21 +15,28 @@ public final class SnapshotOldValueCollector implements OldValueCollector { private final AccountAssetArchiveProjector accountAssetProjector; private final AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource; - private final BooleanSupplier optimizationEnabled; + private final TargetAssetOptimizationResolver targetAssetOptimizationResolver; public SnapshotOldValueCollector() { - this(null, null, null); + this(null, null, (TargetAssetOptimizationResolver) null); } public SnapshotOldValueCollector(AccountAssetArchiveProjector accountAssetProjector, AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource, BooleanSupplier optimizationEnabled) { + this(accountAssetProjector, oldPhysicalAssetsSource, constant(optimizationEnabled)); + } + + public SnapshotOldValueCollector(AccountAssetArchiveProjector accountAssetProjector, + AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource, + TargetAssetOptimizationResolver targetAssetOptimizationResolver) { this.accountAssetProjector = accountAssetProjector; this.oldPhysicalAssetsSource = oldPhysicalAssetsSource; - this.optimizationEnabled = optimizationEnabled; + this.targetAssetOptimizationResolver = targetAssetOptimizationResolver; if (accountAssetProjector != null) { Objects.requireNonNull(oldPhysicalAssetsSource, "oldPhysicalAssetsSource"); - Objects.requireNonNull(optimizationEnabled, "optimizationEnabled"); + Objects.requireNonNull(targetAssetOptimizationResolver, + "targetAssetOptimizationResolver"); } } @@ -37,7 +45,7 @@ public BlockReverseDiff collect(BlockChangeView view) { List groups = new ArrayList<>(); List accountAssetEntries = new ArrayList<>(); boolean targetAssetOptimizationEnabled = accountAssetProjector != null - && optimizationEnabled.getAsBoolean(); + && targetAssetOptimizationResolver.isEnabled(view); for (BlockChangeView.DatabaseChanges database : view.getDatabases()) { List entries = new ArrayList<>(); for (BlockChangeView.Change change : database.getChanges()) { @@ -74,10 +82,49 @@ public BlockReverseDiff collect(BlockChangeView view) { return new BlockReverseDiff(view.getMeta(), groups); } + /** Resolves proposal 66 from the same immutable target block view being projected. */ + public static boolean resolveTargetAssetOptimization(BlockChangeView view) { + Objects.requireNonNull(view, "view"); + byte[] propertyKey = HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(); + for (BlockChangeView.DatabaseChanges database : view.getDatabases()) { + if (!HistoricalAccountAssetBalanceResolver.PROPERTIES_DATABASE.equals( + database.getDbName())) { + continue; + } + byte[] targetValue = database.getPrevious(propertyKey); + for (BlockChangeView.Change change : database.getChanges()) { + if (Arrays.equals(propertyKey, change.getKey())) { + targetValue = change.getPostValue().isPresent() + ? change.getPostValue().getValue() : null; + } + } + if (targetValue == null || targetValue.length != Long.BYTES) { + throw new ArchivePersistenceException( + "Target proposal-66 property must be exactly eight bytes"); + } + long enabled = ByteBuffer.wrap(targetValue).getLong(); + if (enabled != 0L && enabled != 1L) { + throw new ArchivePersistenceException("Target proposal-66 property must be 0 or 1"); + } + return enabled == 1L; + } + throw new ArchivePersistenceException("Target proposal-66 properties Store is absent"); + } + private boolean sameLogicalValue(OldValue oldValue, BlockChangeView.PostValue postValue) { if (oldValue.isPresent() != postValue.isPresent()) { return false; } return !oldValue.isPresent() || Arrays.equals(oldValue.getValue(), postValue.getValue()); } + + @FunctionalInterface + public interface TargetAssetOptimizationResolver { + boolean isEnabled(BlockChangeView view); + } + + private static TargetAssetOptimizationResolver constant(BooleanSupplier supplier) { + BooleanSupplier checked = Objects.requireNonNull(supplier, "optimizationEnabled"); + return view -> checked.getAsBoolean(); + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java index 0691e9183ef..c9f28f83839 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java @@ -8,23 +8,32 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.IdentityHashMap; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.core.SnapshotManager; /** Sole owner for exact-27 State Archive resources from recovered startup through shutdown. */ public final class StateArchiveRuntimeOwner implements Closeable { + public enum ServingIndexStage { + BEFORE_BUILD, + GENERATION_INSTALLED, + CURRENT_PUBLISHED + } + + @FunctionalInterface + public interface ServingIndexFaultHook { + + void afterStage(ServingIndexStage stage) throws IOException; + } + public enum State { RECOVERED, RUNNING, @@ -37,13 +46,17 @@ public enum State { private final Path archiveDirectory; private final long maxSegmentSize; private final List participants; - private final Map participantEngines; private final BlockSnapshotMeta recoveredHead; private final int startupRecoveryActionCount; + private final ServingIndexFaultHook servingIndexFaultHook; private ArchiveRuntimeAttachment attachment; private ArchiveRuntimeQueryGate queryGate; private Closeable latestCoordinator; private Closeable servingCatalog; + private PersistentServingKeyIndexCatalog servingIndexCatalog; + private LatestStateGenerationCoordinator latestStateCoordinator; + private BlockSnapshotMeta latestAuthorityHead; + private volatile BlockSnapshotMeta readableHead; private Closeable sink; private ArchiveHistoryWriter historyWriter; private State state; @@ -61,22 +74,25 @@ public StateArchiveRuntimeOwner(SnapshotManager snapshotManager, this.queryGate = Objects.requireNonNull(queryGate, "queryGate"); this.latestCoordinator = Objects.requireNonNull(latestCoordinator, "latestCoordinator"); this.servingCatalog = Objects.requireNonNull(servingCatalog, "servingCatalog"); + this.servingIndexCatalog = null; + this.latestStateCoordinator = null; + this.latestAuthorityHead = null; + this.readableHead = null; if (!(attachment.getSink() instanceof Closeable)) { throw new IllegalArgumentException("Attached archive sink must be Closeable"); } this.sink = (Closeable) attachment.getSink(); this.participants = immutableParticipants(participants); - this.participantEngines = Collections.emptyMap(); this.recoveredHead = null; this.startupRecoveryActionCount = 0; + this.servingIndexFaultHook = stage -> { }; this.state = State.RUNNING; validateUniqueOwnership(); } private StateArchiveRuntimeOwner(SnapshotManager snapshotManager, - Path archiveDirectory, long maxSegmentSize, List participants, - Map participantEngines, - BlockSnapshotMeta recoveredHead, int startupRecoveryActionCount) { + Path archiveDirectory, long maxSegmentSize, BlockSnapshotMeta recoveredHead, + int startupRecoveryActionCount, ServingIndexFaultHook servingIndexFaultHook) { this.snapshotManager = Objects.requireNonNull(snapshotManager, "snapshotManager"); this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); this.maxSegmentSize = maxSegmentSize; @@ -84,87 +100,63 @@ private StateArchiveRuntimeOwner(SnapshotManager snapshotManager, this.queryGate = null; this.latestCoordinator = null; this.servingCatalog = null; - this.participants = immutableParticipants(participants); - this.participantEngines = immutableParticipantEngines(participantEngines); + this.servingIndexCatalog = null; + this.latestStateCoordinator = null; + this.latestAuthorityHead = null; + this.readableHead = null; + this.participants = Collections.emptyList(); this.sink = null; this.recoveredHead = Objects.requireNonNull(recoveredHead, "recoveredHead"); this.startupRecoveryActionCount = startupRecoveryActionCount; + this.servingIndexFaultHook = Objects.requireNonNull(servingIndexFaultHook, + "servingIndexFaultHook"); this.state = State.RECOVERED; } - /** - * Opens the canonical exact-27 native participants and converges startup recovery before any - * normal archive producer is attached to {@link SnapshotManager}. - */ + /** Opens and validates the committed history authority before normal writes are attached. */ public static StateArchiveRuntimeOwner recover(SnapshotManager snapshotManager, - Path archiveDirectory, long maxSegmentSize, String databaseEngine) throws IOException { + Path archiveDirectory, long maxSegmentSize) throws IOException { + return recover(snapshotManager, archiveDirectory, maxSegmentSize, stage -> { }); + } + + public static StateArchiveRuntimeOwner recover(SnapshotManager snapshotManager, + Path archiveDirectory, long maxSegmentSize, ServingIndexFaultHook servingIndexFaultHook) + throws IOException { Objects.requireNonNull(snapshotManager, "snapshotManager"); Path root = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); - String engine = Objects.requireNonNull(databaseEngine, "databaseEngine") - .toUpperCase(Locale.ROOT); - if (!"LEVELDB".equals(engine) && !"ROCKSDB".equals(engine)) { - throw new IllegalArgumentException("Unsupported State Archive database engine: " + engine); - } - List names = ArchiveParticipantDescriptor.current().getParticipants(); - Map openedByName = new LinkedHashMap<>(); - List opened = new ArrayList<>(); - try { - for (String participant : names) { - Closeable nativeEngine = openParticipant(root.resolve("participants").resolve(participant), - participant, names, engine); - opened.add(nativeEngine); - openedByName.put(participant, (ArchiveParticipant) nativeEngine); - } - Path checkpoint = root.resolve("progress").resolve("checkpoint.progress"); - Path reader = root.resolve("progress").resolve("reader.progress"); - RecoveryPlan first; - HistoryCommitMarker head; - try (ArchiveParticipantRecoveryStorage recovery = - new ArchiveParticipantRecoveryStorage(root, maxSegmentSize, checkpoint, - openedByName, reader, names)) { - first = new ArchiveRecoveryExecutor(recovery).recover(); - RecoveryPlan fixed = new ArchiveRecoveryExecutor(recovery).recover(); - if (!fixed.getActions().isEmpty()) { - throw new ArchivePersistenceException( - "State Archive second startup recovery was not zero-action"); - } - head = recovery.committedHead(); - } - if (head == null) { - throw new ArchivePersistenceException("State Archive recovered H head is missing"); - } - return new StateArchiveRuntimeOwner(snapshotManager, root, maxSegmentSize, opened, - openedByName, head.getMeta(), first.getActions().size()); - } catch (IOException | RuntimeException failure) { - closeReverse(opened, failure); - throw failure; + HistoryCommitMarker head; + try (ArchiveHistoryWriter history = new ArchiveHistoryWriter(root, maxSegmentSize, + ArchiveStoreScope.getStateDatabases())) { + head = history.committedHead(); + } + if (head == null) { + throw new ArchivePersistenceException("State Archive recovered H head is missing"); } + return new StateArchiveRuntimeOwner(snapshotManager, root, maxSegmentSize, head.getMeta(), 0, + servingIndexFaultHook); } /** - * Atomically establishes one empty-diff H/C/27D/R baseline at the persisted Chainbase head, - * then reopens it through the ordinary startup recovery path. The staging directory is never - * published until every durable authority is complete and independently recoverable. + * Atomically establishes one empty-diff H baseline at the persisted Chainbase head, then + * reopens it through the ordinary startup path. No legacy participant/progress path is created. */ public static StateArchiveRuntimeOwner bootstrapAndRecover(SnapshotManager snapshotManager, - Path archiveDirectory, long maxSegmentSize, String databaseEngine, - BlockSnapshotMeta baseHead, Phase targetPhase) throws IOException { + Path archiveDirectory, long maxSegmentSize, BlockSnapshotMeta baseHead) throws IOException { + return bootstrapAndRecover(snapshotManager, archiveDirectory, maxSegmentSize, baseHead, + stage -> { }); + } + + public static StateArchiveRuntimeOwner bootstrapAndRecover(SnapshotManager snapshotManager, + Path archiveDirectory, long maxSegmentSize, BlockSnapshotMeta baseHead, + ServingIndexFaultHook servingIndexFaultHook) throws IOException { Objects.requireNonNull(snapshotManager, "snapshotManager"); Path root = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); BlockSnapshotMeta head = Objects.requireNonNull(baseHead, "baseHead"); - Phase phase = Objects.requireNonNull(targetPhase, "targetPhase"); Path parent = Objects.requireNonNull(root.getParent(), "archive parent directory"); requireEmptyBootstrapTarget(root); Files.createDirectories(parent); Path staging = parent.resolve("." + root.getFileName() + ".bootstrap-" + UUID.randomUUID()); - String engine = Objects.requireNonNull(databaseEngine, "databaseEngine") - .toUpperCase(Locale.ROOT); - if (!"LEVELDB".equals(engine) && !"ROCKSDB".equals(engine)) { - throw new IllegalArgumentException("Unsupported State Archive database engine: " + engine); - } - - List names = ArchiveParticipantDescriptor.current().getParticipants(); - List opened = new ArrayList<>(); + List names = storeNames(); try { HistoryCommitMarker marker; try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(staging, maxSegmentSize, @@ -173,35 +165,10 @@ public static StateArchiveRuntimeOwner bootstrapAndRecover(SnapshotManager snaps marker = Objects.requireNonNull(writer.committedHead(), "bootstrap history head"); } - Map engines = new LinkedHashMap<>(); - for (String participant : names) { - Closeable nativeEngine = openParticipant( - staging.resolve("participants").resolve(participant), participant, names, engine); - opened.add(nativeEngine); - engines.put(participant, (ArchiveParticipant) nativeEngine); - } - Map> emptyMutations = new LinkedHashMap<>(); - for (String participant : names) { - emptyMutations.put(participant, Collections.emptyList()); - } - ArchiveProgressEnvelope target = progress(Kind.APPLY_CHECKPOINT, null, marker, null, names); - byte[] planDigest = new ArchiveTargetMutationPlan(target, - P66AccountAssetCodec.FORMAT_ID, phase, emptyMutations).digest(); - ArchiveBootstrapAnchor.store(staging, marker, planDigest, names); - ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - new ArchiveProgressFile(staging.resolve("progress/checkpoint.progress"), codec) - .store(progress(Kind.APPLY_CHECKPOINT, null, marker, planDigest, names)); - for (String participant : names) { - engines.get(participant).apply(Collections.emptyList(), - progress(Kind.PARTICIPANT_PROGRESS, participant, marker, planDigest, names)); - } - new ArchiveProgressFile(staging.resolve("progress/reader.progress"), codec) - .store(progress(Kind.READER_VISIBLE, null, marker, planDigest, names)); - closeReverseOrThrow(opened); - opened.clear(); + ArchiveBootstrapAnchor.store(staging, marker, names); try (StateArchiveRuntimeOwner verified = recover(snapshotManager, staging, - maxSegmentSize, engine)) { + maxSegmentSize)) { if (!head.equals(verified.getRecoveredHead()) || verified.getStartupRecoveryActionCount() != 0) { throw new ArchivePersistenceException( @@ -218,9 +185,8 @@ public static StateArchiveRuntimeOwner bootstrapAndRecover(SnapshotManager snaps "State Archive bootstrap requires atomic directory publication", failure); } HistorySegmentStore.syncDirectory(parent); - return recover(snapshotManager, root, maxSegmentSize, engine); + return recover(snapshotManager, root, maxSegmentSize, servingIndexFaultHook); } catch (IOException | RuntimeException failure) { - closeReverse(opened, failure); throw failure; } } @@ -242,34 +208,59 @@ public int getStartupRecoveryActionCount() { /** Continues this recovered owner into one atomically attached normal-write runtime. */ public synchronized ArchiveHistoryWriter attachNormalWriter(OldValueCollector collector, - ArchiveBlockProjectionPreparer projectionPreparer, int queueCapacity) throws IOException { + int queueCapacity, BlockSnapshotMeta canonicalHead) throws IOException { + return attachNormalWriter(collector, queueCapacity, canonicalHead, Collections.emptyMap()); + } + + public synchronized ArchiveHistoryWriter attachNormalWriter(OldValueCollector collector, + int queueCapacity, BlockSnapshotMeta canonicalHead, + Map supplementalStores) + throws IOException { if (state != State.RECOVERED) { throw new IllegalStateException("State Archive owner is not recovered"); } ArchiveHistoryWriter writer = null; AsyncArchiveHistorySink asyncSink = null; + PersistentServingKeyIndexCatalog catalog = null; + LatestStateGenerationCoordinator latest = null; ArchiveRuntimeAttachment candidate = null; boolean attached = false; try { writer = new ArchiveHistoryWriter(archiveDirectory, maxSegmentSize, - new java.util.LinkedHashSet<>(participantEngines.keySet())); + ArchiveStoreScope.getStateDatabases()); if (!recoveredHead.equals(writer.committedHeadMeta())) { throw new ArchivePersistenceException( "Recovered archive head changed before normal writer attachment"); } + ArchiveWalStartupValidator.requireFixedPoint(writer, canonicalHead, + snapshotManager.getRecoveredArchiveWalBinding(), + storeNames()); + catalog = openOrCreateServingCatalog(writer); + validateServingIndex(writer, catalog, canonicalHead); + latest = LatestStateGenerationCoordinatorFactory.create(snapshotManager, + supplementalStores, this::readLatestAuthority); + restoreLatestState(writer, catalog, latest, canonicalHead); asyncSink = new AsyncArchiveHistorySink(writer, queueCapacity); - Path checkpoint = archiveDirectory.resolve("progress").resolve("checkpoint.progress"); - Path reader = archiveDirectory.resolve("progress").resolve("reader.progress"); - ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(writer, - checkpoint, participantEngines, reader, new ArrayList<>(participantEngines.keySet()), - snapshotManager::withArchiveStateBarrier); - candidate = new ArchiveRuntimeAttachment(collector, projectionPreparer, asyncSink, - (payloads, refresh) -> publishTargets(coordinator, payloads, refresh)); + ArchiveHistoryWriter attachedWriter = writer; + PersistentServingKeyIndexCatalog attachedCatalog = catalog; + LatestStateGenerationCoordinator attachedLatest = latest; + candidate = new ArchiveRuntimeAttachment(collector, asyncSink, + target -> publishServingIndex(attachedWriter, attachedCatalog, target), + target -> publishReadableState(attachedWriter, attachedCatalog, attachedLatest, target)); snapshotManager.attachArchiveRuntime(candidate); attached = true; + snapshotManager.markArchiveReadableThrough(canonicalHead.getEpoch()); + validateReadableState(catalog, latest, canonicalHead); attachment = candidate; sink = asyncSink; historyWriter = writer; + servingIndexCatalog = catalog; + latestStateCoordinator = latest; + latestCoordinator = latest; + servingCatalog = catalog; + readableHead = canonicalHead; + queryGate = new ArchiveRuntimeQueryGate(new ArchiveGenerationCapsule(catalog, + archiveDirectory, maxSegmentSize, latest, this::readReadableAuthority)); state = State.RUNNING; return writer; } catch (IOException | RuntimeException failure) { @@ -289,6 +280,20 @@ public synchronized ArchiveHistoryWriter attachNormalWriter(OldValueCollector co failure.addSuppressed(closeFailure); } } + if (catalog != null) { + try { + catalog.close(); + } catch (IOException | RuntimeException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + if (latest != null) { + try { + latest.close(); + } catch (IOException | RuntimeException closeFailure) { + failure.addSuppressed(closeFailure); + } + } throw failure; } } @@ -300,39 +305,246 @@ public synchronized ArchiveHistoryWriter getHistoryWriter() { return historyWriter; } - /** Machine-checks the current H=C=D[0..26]=R identity and retired mutation plan. */ + /** Acquires one request-owned historical snapshot from the currently published fixed point. */ + public synchronized ArchiveRuntimeQueryGate.Lease pinHistoricalState(long targetBlock) + throws IOException { + if (state != State.RUNNING || queryGate == null) { + throw new IllegalStateException("State Archive historical query runtime is not running"); + } + return queryGate.pin(targetBlock); + } + + /** Machine-checks the latest committed H against the last Chainbase WAL binding. */ public synchronized BlockSnapshotMeta verifyNormalWriteFixedPoint() throws IOException { ArchiveHistoryWriter writer = getHistoryWriter(); HistoryCommitMarker head = Objects.requireNonNull(writer.committedHead(), "archive history head"); - List names = new ArrayList<>(participantEngines.keySet()); - Path checkpointPath = archiveDirectory.resolve("progress").resolve("checkpoint.progress"); - if (new ArchiveTargetMutationPlanFile(checkpointPath).loadIfPresent() != null) { - throw new ArchivePersistenceException("Archive mutation plan is not retired"); - } - ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - ArchiveProgressEnvelope checkpoint = new ArchiveProgressFile(checkpointPath, codec).load(); - ArchiveProgressEnvelope reader = new ArchiveProgressFile( - archiveDirectory.resolve("progress").resolve("reader.progress"), codec).load(); - requireAuthority(checkpoint, Kind.APPLY_CHECKPOINT, null, head, names); - requireAuthority(reader, Kind.READER_VISIBLE, null, head, names); - if (!java.util.Arrays.equals(checkpoint.getMutationPlanDigest(), - reader.getMutationPlanDigest())) { - throw new ArchivePersistenceException("Archive C/R mutation-plan identity differs"); - } - for (Map.Entry entry : participantEngines.entrySet()) { - ArchiveProgressEnvelope progress = entry.getValue().loadProgress(); - requireAuthority(progress, Kind.PARTICIPANT_PROGRESS, entry.getKey(), head, names); - if (!java.util.Arrays.equals(checkpoint.getMutationPlanDigest(), - progress.getMutationPlanDigest())) { + ArchiveWalBinding binding = snapshotManager.getLatestArchiveWalBinding(); + if (binding == null) { + binding = snapshotManager.getRecoveredArchiveWalBinding(); + } + ArchiveWalStartupValidator.requireFixedPoint(writer, head.getMeta(), + binding, storeNames()); + validateServingIndex(writer, requireServingIndexCatalog(), head.getMeta()); + validateReadableState(requireServingIndexCatalog(), head.getMeta()); + return head.getMeta(); + } + + private ArchiveProgressEnvelope readLatestAuthority() { + BlockSnapshotMeta target = latestAuthorityHead; + if (target == null) { + throw new ArchivePersistenceException("Latest-state authority is not being published"); + } + return new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, target.getEpoch(), + target.getBlockHash(), new byte[16], new byte[32], storeNames()); + } + + private synchronized void restoreLatestState(ArchiveHistoryWriter writer, + PersistentServingKeyIndexCatalog catalog, LatestStateGenerationCoordinator latest, + BlockSnapshotMeta target) throws IOException { + try (PersistentServingKeyIndexGeneration serving = catalog.pin()) { + validateServingGeneration(writer, serving, target); + if (serving.isLatestSourceIdentityBound()) { + publishExistingLatest(latest, serving, target); + return; + } + } + bindAndPublishLatest(writer, catalog, latest, target); + } + + private synchronized void publishReadableState(ArchiveHistoryWriter writer, + PersistentServingKeyIndexCatalog catalog, LatestStateGenerationCoordinator latest, + BlockSnapshotMeta target) throws IOException { + if (!target.equals(writer.committedHeadMeta())) { + throw new ArchivePersistenceException( + "Readable-state target differs from committed history head"); + } + bindAndPublishLatest(writer, catalog, latest, target); + long previousReadable = snapshotManager.getArchiveReadableEpoch(); + snapshotManager.markArchiveReadableThrough(target.getEpoch()); + try { + validateReadableState(catalog, target); + readableHead = target; + } catch (IOException | RuntimeException failure) { + snapshotManager.markArchiveReadableThrough(previousReadable); + throw failure; + } + } + + private void publishExistingLatest(LatestStateGenerationCoordinator latest, + PersistentServingKeyIndexGeneration serving, BlockSnapshotMeta target) throws IOException { + latestAuthorityHead = target; + try (LatestStateGenerationCoordinator.Candidate candidate = + latest.acquire(serving.getGenerationId())) { + if (!latest.publish(null, candidate, serving)) { + throw new ArchivePersistenceException("Latest-state startup publication changed"); + } + } finally { + latestAuthorityHead = null; + } + } + + private void bindAndPublishLatest(ArchiveHistoryWriter writer, + PersistentServingKeyIndexCatalog catalog, LatestStateGenerationCoordinator latest, + BlockSnapshotMeta target) throws IOException { + String generationId = generationId(target); + String expectedLatest = latest.getCurrentGenerationId(); + latestAuthorityHead = target; + try (LatestStateGenerationCoordinator.Candidate candidate = latest.acquire(generationId)) { + Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); + try (PersistentServingKeyIndexGeneration built = writer.buildServingGeneration(shadow, + generationId, candidate.getSourceIdentityDigest())) { + validateServingGeneration(writer, built, target); + } + String expectedServing = catalog.getCurrentGenerationId(); + if (!catalog.publish(expectedServing, shadow)) { throw new ArchivePersistenceException( - "Archive participant mutation-plan identity differs: " + entry.getKey()); + "Serving index catalog changed during latest-state publication"); + } + try (PersistentServingKeyIndexGeneration serving = catalog.pin()) { + if (!latest.publish(expectedLatest, candidate, serving)) { + throw new ArchivePersistenceException("Latest-state generation changed during publish"); + } } + } finally { + latestAuthorityHead = null; } - if (snapshotManager.getArchiveReadableEpoch() != head.getMeta().getEpoch()) { - throw new ArchivePersistenceException("SnapshotManager readable epoch differs from R"); + } + + private void validateReadableState(PersistentServingKeyIndexCatalog catalog, + BlockSnapshotMeta target) throws IOException { + LatestStateGenerationCoordinator latest = latestStateCoordinator; + if (latest == null) { + throw new ArchivePersistenceException("Latest-state coordinator is not attached"); + } + validateReadableState(catalog, latest, target); + } + + private void validateReadableState(PersistentServingKeyIndexCatalog catalog, + LatestStateGenerationCoordinator latest, BlockSnapshotMeta target) throws IOException { + try (PersistentServingKeyIndexGeneration serving = catalog.pin()) { + if (!serving.isLatestSourceIdentityBound() + || !serving.getGenerationId().equals(latest.getCurrentGenerationId()) + || serving.getIndexedThrough() != target.getEpoch() + || !Arrays.equals(serving.getHeadHash(), target.getBlockHash()) + || snapshotManager.getArchiveReadableEpoch() != target.getEpoch()) { + throw new ArchivePersistenceException("Archive P/H/I/latest/R fixed point mismatch"); + } + } + } + + private ArchiveProgressEnvelope readReadableAuthority() throws IOException { + BlockSnapshotMeta head = readableHead; + ArchiveHistoryWriter writer = historyWriter; + PersistentServingKeyIndexCatalog catalog = servingIndexCatalog; + LatestStateGenerationCoordinator latest = latestStateCoordinator; + ArchiveWalBinding binding = snapshotManager.getLatestArchiveWalBinding(); + if (binding == null) { + binding = snapshotManager.getRecoveredArchiveWalBinding(); + } + BlockSnapshotMeta persisted = binding == null ? recoveredHead : binding.getLast(); + if (head == null || writer == null || catalog == null || latest == null + || snapshotManager.getArchiveReadableEpoch() != head.getEpoch() + || !head.equals(writer.committedHeadMeta()) || !head.equals(persisted)) { + throw new ArchivePersistenceException( + "Archive historical query is outside the P/H/I/latest/R fixed point"); + } + try (PersistentServingKeyIndexGeneration serving = catalog.pin()) { + if (!serving.isLatestSourceIdentityBound() + || !serving.getGenerationId().equals(latest.getCurrentGenerationId()) + || serving.getIndexedThrough() != head.getEpoch() + || !Arrays.equals(serving.getHeadHash(), head.getBlockHash())) { + throw new ArchivePersistenceException( + "Archive historical query generation is outside readable R"); + } + } + return new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, head.getEpoch(), + head.getBlockHash(), new byte[16], new byte[32], storeNames()); + } + + private PersistentServingKeyIndexCatalog openOrCreateServingCatalog( + ArchiveHistoryWriter writer) throws IOException { + Path root = archiveDirectory.resolve("serving-index"); + if (Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + return PersistentServingKeyIndexCatalog.open(root, this::afterCatalogStage); + } + String generationId = generationId(writer.committedHeadMeta()); + Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); + try (PersistentServingKeyIndexGeneration ignored = + writer.buildServingGeneration(shadow, generationId)) { + // The catalog reopens and validates the immutable generation before publishing it. + } + return PersistentServingKeyIndexCatalog.create(root, shadow, this::afterCatalogStage); + } + + private synchronized void publishServingIndex(ArchiveHistoryWriter writer, + PersistentServingKeyIndexCatalog catalog, BlockSnapshotMeta target) throws IOException { + BlockSnapshotMeta historyHead = writer.committedHeadMeta(); + if (!target.equals(historyHead)) { + throw new ArchivePersistenceException( + "Serving index target differs from committed history head"); + } + servingIndexFaultHook.afterStage(ServingIndexStage.BEFORE_BUILD); + String generationId = generationId(target); + Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); + try (PersistentServingKeyIndexGeneration candidate = + writer.buildServingGeneration(shadow, generationId)) { + validateServingGeneration(writer, candidate, target); + } + String expected = catalog.getCurrentGenerationId(); + if (!catalog.publish(expected, shadow)) { + throw new ArchivePersistenceException("Serving index catalog changed during publication"); + } + validateServingIndex(writer, catalog, target); + } + + private void afterCatalogStage(PersistentServingKeyIndexCatalog.PublicationStage stage) + throws IOException { + servingIndexFaultHook.afterStage(stage + == PersistentServingKeyIndexCatalog.PublicationStage.GENERATION_INSTALLED + ? ServingIndexStage.GENERATION_INSTALLED : ServingIndexStage.CURRENT_PUBLISHED); + } + + private static String generationId(BlockSnapshotMeta target) { + return "h-" + target.getEpoch() + "-" + UUID.randomUUID(); + } + + private PersistentServingKeyIndexCatalog requireServingIndexCatalog() { + if (servingIndexCatalog == null) { + throw new IllegalStateException("State Archive serving index is not attached"); + } + return servingIndexCatalog; + } + + private static void validateServingIndex(ArchiveHistoryWriter writer, + PersistentServingKeyIndexCatalog catalog, BlockSnapshotMeta target) throws IOException { + try (PersistentServingKeyIndexGeneration pinned = catalog.pin()) { + validateServingGeneration(writer, pinned, target); + } + } + + private static void validateServingGeneration(ArchiveHistoryWriter writer, + PersistentServingKeyIndexGeneration generation, BlockSnapshotMeta target) + throws IOException { + ServingKeyIndexGeneration expected = writer.buildServingIdentity("expected"); + List stores = storeNames(); + if (generation.getIndexedFrom() != expected.getIndexedFrom() + || generation.getIndexedThrough() != target.getEpoch() + || expected.getIndexedThrough() != target.getEpoch() + || !Arrays.equals(generation.getHeadHash(), target.getBlockHash()) + || !Arrays.equals(expected.getHeadHash(), target.getBlockHash()) + || !Arrays.equals(generation.getAuthoritativePrefixDigest(), + expected.getAuthoritativePrefixDigest()) + || !generation.getParticipatingDatabases().equals(stores)) { + throw new ArchivePersistenceException( + "Serving index generation differs from committed history authority"); + } + for (String store : stores) { + if (!expected.getStoreCoverage(store).isPresent()) { + throw new ArchivePersistenceException( + "Serving index identity is missing Store coverage: " + store); + } } - return head.getMeta(); } /** Quiesces, detaches and closes owned resources without waiting for active query leases. */ @@ -358,6 +570,7 @@ public synchronized void close() throws IOException { if (queryGate != null) { queryGate.quiesce(); } + readableHead = null; if (!detached) { ArchiveRuntimeAttachment returned = snapshotManager.detachArchiveRuntime(attachment); if (returned != attachment) { @@ -402,14 +615,6 @@ private IOException closeParticipants() { return failure; } - private static Closeable openParticipant(Path directory, String participant, - List participants, String engine) throws IOException { - if ("ROCKSDB".equals(engine)) { - return new RocksDbArchiveParticipant(directory, participant, participants); - } - return new LevelDbArchiveParticipant(directory, participant, participants); - } - private static void requireEmptyBootstrapTarget(Path root) throws IOException { if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { return; @@ -425,32 +630,10 @@ private static void requireEmptyBootstrapTarget(Path root) throws IOException { } } - private static ArchiveProgressEnvelope progress(Kind kind, String participant, - HistoryCommitMarker marker, byte[] planDigest, List participants) { - return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), planDigest, participants); - } - - private static void closeReverseOrThrow(List resources) throws IOException { - IOException failure = null; - while (!resources.isEmpty()) { - int index = resources.size() - 1; - failure = closeOwned("bootstrap participant " + index, resources.remove(index), failure); - } - if (failure != null) { - throw failure; - } - } - - private static void closeReverse(List resources, Exception failure) { - for (int i = resources.size() - 1; i >= 0; i--) { - try { - resources.get(i).close(); - } catch (IOException | RuntimeException closeFailure) { - failure.addSuppressed(closeFailure); - } - } + private static List storeNames() { + List names = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(names); + return names; } private static List immutableParticipants( @@ -463,15 +646,6 @@ private static List immutableParticipants( return Collections.unmodifiableList(copy); } - private static Map immutableParticipantEngines( - Map engines) { - Map source = Objects.requireNonNull(engines, "engines"); - Map copy = new LinkedHashMap<>(); - source.forEach((name, engine) -> copy.put(Objects.requireNonNull(name, "participant name"), - Objects.requireNonNull(engine, "participant engine"))); - return Collections.unmodifiableMap(copy); - } - private void validateUniqueOwnership() { Set unique = Collections.newSetFromMap(new IdentityHashMap()); requireUnique(unique, latestCoordinator, "latestCoordinator"); @@ -482,40 +656,6 @@ private void validateUniqueOwnership() { requireUnique(unique, sink, "sink"); } - private static void publishTargets(ArchiveTargetApplyCoordinator coordinator, - List payloads, - ArchiveStateBarrier.ArchiveStateAction refresh) throws IOException { - if (payloads.isEmpty()) { - throw new ArchivePersistenceException("Archive normal flush has no forward payload"); - } - BlockSnapshotMeta previous = null; - for (ArchiveBlockForwardPayload payload : payloads) { - BlockSnapshotMeta current = Objects.requireNonNull(payload, "forward payload").getMeta(); - if (previous != null && (current.getEpoch() != previous.getEpoch() + 1 - || current.getBlockNumber() != previous.getBlockNumber() + 1 - || !java.util.Arrays.equals(current.getParentHash(), previous.getBlockHash()))) { - throw new ArchivePersistenceException( - "Archive normal flush forward payloads are not contiguous"); - } - previous = current; - } - for (ArchiveBlockForwardPayload payload : payloads) { - ArchiveParticipantMutationBatch batch = new ArchiveParticipantMutationBatchCollector( - payload.getAccountAssetManifest()).collect(payload.getMarker(), payload.getView()); - coordinator.apply(batch, refresh); - } - } - - private static void requireAuthority(ArchiveProgressEnvelope envelope, Kind kind, - String participant, HistoryCommitMarker marker, List participants) { - if (envelope == null) { - throw new ArchivePersistenceException("Missing archive authority: " + kind); - } - envelope.requireIdentity(kind, participant, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), participants); - } - private static void requireUnique(Set unique, Closeable resource, String name) { if (!unique.add(resource)) { throw new IllegalArgumentException("Archive runtime resource has multiple owners: " + name); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index def4ccc882d..2feba4f4dc2 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -36,15 +36,12 @@ import org.tron.core.db.RevokingDatabase; import org.tron.core.db.TronDatabase; import org.tron.core.db2.ISession; -import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.PreparedBlockProjection; -import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner; -import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; -import org.tron.core.db2.archive.ArchiveBlockForwardPayload; -import org.tron.core.db2.archive.ArchiveBlockProjectionPreparer; import org.tron.core.db2.archive.ArchivePersistenceException; import org.tron.core.db2.archive.ArchiveRuntimeAttachment; import org.tron.core.db2.archive.ArchiveStateBarrier.ArchiveStateAction; import org.tron.core.db2.archive.ArchiveStoreScope; +import org.tron.core.db2.archive.ArchiveWalBinding; +import org.tron.core.db2.archive.ArchiveWalBindingCodec; import org.tron.core.db2.archive.BlockChangeView; import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockReverseDiffSink; @@ -104,16 +101,13 @@ public class SnapshotManager implements RevokingDatabase { private int checkpointVersion = 1; // default v1 private OldValueCollector oldValueCollector; - private ArchiveBlockProjectionPreparer archiveBlockProjectionPreparer; private ArchiveRuntimeAttachment archiveRuntimeAttachment; - private final Map - archiveForwardPayloadOwners = new HashMap<>(); - private FrozenBatch pendingArchiveForwardFlush; - private List sealedArchiveForwardFlush; - private Long submittedArchiveForwardHistoryEpoch; + private Long submittedArchiveHistoryEpoch; private BlockReverseDiffSink blockReverseDiffSink; @Getter private volatile long archiveReadableEpoch = -1; + private volatile ArchiveWalBinding latestArchiveWalBinding; + private volatile ArchiveWalBinding recoveredArchiveWalBinding; public SnapshotManager(String checkpointPath) { } @@ -269,51 +263,22 @@ public synchronized void commit(BlockSnapshotMeta meta) { } BlockReverseDiff reverseDiff = null; - AccountAssetPreparedBlockPayloadOwner forwardOwner = null; - PreparedBlockProjection projection = null; - boolean forwardOwnerAttached = false; - try { - if (archiveBlockProjectionPreparer != null) { - if (archiveForwardPayloadOwners.containsKey(meta)) { - throw new IllegalStateException("Archive forward payload owner already exists: " + meta); - } - BlockChangeView view = BlockChangeView.capture(meta, dbs); - projection = Objects.requireNonNull( - archiveBlockProjectionPreparer.prepare(view), - "archive projection preparer returned null"); - forwardOwner = new AccountAssetPreparedBlockPayloadOwner(meta); - forwardOwner.attach(projection); - forwardOwnerAttached = true; - reverseDiff = forwardOwner.getReverseDiff(); - } else if (oldValueCollector != null) { - reverseDiff = Objects.requireNonNull( - oldValueCollector.collect(BlockChangeView.capture(meta, dbs)), - "archive collector returned null"); - } + if (oldValueCollector != null) { + reverseDiff = Objects.requireNonNull( + oldValueCollector.collect(BlockChangeView.capture(meta, dbs)), + "archive collector returned null"); + } - dbs.forEach(db -> { - if (db.getHead().isOptimized()) { - db.getHead().reloadToMem(); - } - }); - } catch (RuntimeException | Error failure) { - if (forwardOwnerAttached) { - forwardOwner.discard(); - } else if (projection != null) { - projection.abort(); + dbs.forEach(db -> { + if (db.getHead().isOptimized()) { + db.getHead().reloadToMem(); } - throw failure; - } + }); - // All fallible work is complete. From here the prepared payload is owned by the block layer; - // fastPop/reorg can discard it without touching durable archive state. for (Chainbase db : dbs) { ((SnapshotImpl) db.getHead()).attachArchiveBlock(meta, ArchiveStoreScope.isStateDatabase(db.getDbName()) ? reverseDiff : null); } - if (forwardOwner != null) { - archiveForwardPayloadOwners.put(meta, forwardOwner); - } --activeSession; } @@ -359,19 +324,6 @@ public synchronized void installArchiveCollector(OldValueCollector collector, blockReverseDiffSink = Objects.requireNonNull(sink, "sink"); } - /** Enables shared reverse/forward preparation; absent by default and installed independently. */ - public synchronized void installArchiveProjectionPreparer( - ArchiveBlockProjectionPreparer preparer) { - ArchiveStoreScope.validate(dbs); - if (archiveRuntimeAttachment != null) { - throw new IllegalStateException("Borrowed archive runtime is already attached"); - } - if (oldValueCollector == null || blockReverseDiffSink == null) { - throw new IllegalStateException("Archive collector must be installed before its preparer"); - } - archiveBlockProjectionPreparer = Objects.requireNonNull(preparer, "preparer"); - } - /** Atomically installs one borrowed archive runtime bundle after store registration. */ public synchronized void attachArchiveRuntime(ArchiveRuntimeAttachment attachment) { ArchiveStoreScope.validate(dbs); @@ -379,12 +331,10 @@ public synchronized void attachArchiveRuntime(ArchiveRuntimeAttachment attachmen if (archiveRuntimeAttachment != null) { throw new IllegalStateException("Archive runtime is already attached"); } - if (oldValueCollector != null || archiveBlockProjectionPreparer != null - || blockReverseDiffSink != null) { + if (oldValueCollector != null || blockReverseDiffSink != null) { throw new IllegalStateException("Legacy archive collaborators are already installed"); } oldValueCollector = candidate.getCollector(); - archiveBlockProjectionPreparer = candidate.getProjectionPreparer(); blockReverseDiffSink = candidate.getSink(); archiveRuntimeAttachment = candidate; } @@ -399,150 +349,14 @@ public synchronized ArchiveRuntimeAttachment detachArchiveRuntime( if (archiveRuntimeAttachment != candidate) { throw new IllegalStateException("Cannot detach a foreign archive runtime"); } - abortArchiveForwardPayloads(); archiveRuntimeAttachment = null; oldValueCollector = null; - archiveBlockProjectionPreparer = null; blockReverseDiffSink = null; + submittedArchiveHistoryEpoch = null; archiveReadableEpoch = -1; return candidate; } - /** Visible for lifecycle verification until the flush freeze coordinator consumes this registry. */ - public synchronized int getArchiveForwardPayloadOwnerCount() { - return archiveForwardPayloadOwners.size(); - } - - /** Visible for lifecycle verification until the flush freeze coordinator consumes this registry. */ - public synchronized boolean hasArchiveForwardPayloadOwner(BlockSnapshotMeta meta) { - return archiveForwardPayloadOwners.containsKey(Objects.requireNonNull(meta, "meta")); - } - - /** Atomically transfers the exact oldest flush range from the registry to one pending owner. */ - public synchronized FrozenBatch freezeArchiveForwardFlushRange() { - if (pendingArchiveForwardFlush != null) { - return pendingArchiveForwardFlush; - } - if (sealedArchiveForwardFlush != null) { - throw new IllegalStateException("Archive forward flush is sealed and awaiting claim"); - } - if (archiveBlockProjectionPreparer == null) { - throw new IllegalStateException("Archive projection preparer is not installed"); - } - List topology = stateLayerMetas(); - if (flushCount <= 0 || flushCount > topology.size()) { - throw new IllegalStateException("Archive forward flush range is empty or exceeds topology"); - } - java.util.Set unique = new java.util.LinkedHashSet<>(topology); - if (unique.size() != topology.size()) { - throw new IllegalStateException("Archive state topology contains duplicate block metadata"); - } - BlockSnapshotMeta previous = null; - for (BlockSnapshotMeta current : topology) { - if (previous != null && (current.getEpoch() != previous.getEpoch() + 1 - || current.getBlockNumber() != previous.getBlockNumber() + 1 - || !Arrays.equals(current.getParentHash(), previous.getBlockHash()))) { - throw new IllegalStateException("Archive state topology is not contiguous"); - } - previous = current; - } - if (!archiveForwardPayloadOwners.keySet().equals(unique)) { - throw new IllegalStateException("Archive forward owner registry does not match topology"); - } - for (BlockSnapshotMeta meta : topology) { - AccountAssetPreparedBlockPayloadOwner owner = archiveForwardPayloadOwners.get(meta); - if (owner == null || !owner.isAttachedTo(meta)) { - throw new IllegalStateException("Archive forward owner is missing or not attached"); - } - } - - List range = new ArrayList<>(topology.subList(0, flushCount)); - List owners = new ArrayList<>(range.size()); - for (BlockSnapshotMeta meta : range) { - owners.add(archiveForwardPayloadOwners.get(meta)); - } - - FrozenBatch frozen = AccountAssetPreparedBlockPayloadOwner.freezeContiguous(owners); - for (BlockSnapshotMeta meta : range) { - archiveForwardPayloadOwners.remove(meta); - } - pendingArchiveForwardFlush = frozen; - return frozen; - } - - public synchronized boolean hasPendingArchiveForwardFlush() { - return pendingArchiveForwardFlush != null || sealedArchiveForwardFlush != null; - } - - /** Seals the pending range with an externally validated exact marker list. */ - public synchronized void sealPendingArchiveForwardFlush(List markers) { - FrozenBatch pending = requirePendingArchiveForwardFlush(); - List sealed = pending.seal(markers); - sealedArchiveForwardFlush = sealed; - pendingArchiveForwardFlush = null; - } - - /** Reads exact durable marker evidence and seals the same pending range. */ - public synchronized void sealPendingArchiveForwardFlush( - DurableHistoryMarkerRangeEvidence evidence) { - FrozenBatch pending = requirePendingArchiveForwardFlush(); - List sealed = Objects.requireNonNull(evidence, "evidence") - .seal(pending); - sealedArchiveForwardFlush = sealed; - pendingArchiveForwardFlush = null; - } - - /** Transfers the sealed ordered payloads exactly once and clears manager ownership. */ - public synchronized List claimArchiveForwardFlushPayloads() { - if (sealedArchiveForwardFlush == null) { - throw new IllegalStateException("Archive forward flush is not sealed"); - } - List claimed = sealedArchiveForwardFlush; - sealedArchiveForwardFlush = null; - return claimed; - } - - private FrozenBatch requirePendingArchiveForwardFlush() { - if (sealedArchiveForwardFlush != null) { - throw new IllegalStateException("Archive forward flush is already sealed"); - } - if (pendingArchiveForwardFlush == null) { - throw new IllegalStateException("Archive forward flush range is not frozen"); - } - return pendingArchiveForwardFlush; - } - - private List stateLayerMetas() { - List reference = null; - for (Chainbase db : dbs) { - if (!ArchiveStoreScope.isStateDatabase(db.getDbName())) { - continue; - } - List candidate = new ArrayList<>(); - Snapshot head = db.getHead(); - Snapshot next = db.getHead().getRoot(); - while (next != head) { - next = next.getNext(); - if (!Snapshot.isImpl(next)) { - throw new IllegalStateException("Archive state topology is missing a snapshot layer"); - } - BlockSnapshotMeta meta = ((SnapshotImpl) next).getBlockSnapshotMeta(); - if (meta == null) { - throw new IllegalStateException("Archive state topology contains an unbound layer"); - } - candidate.add(meta); - } - if (reference != null && !reference.equals(candidate)) { - throw new IllegalStateException("Block metadata differs across state database topology"); - } - reference = candidate; - } - if (reference == null) { - throw new IllegalStateException("Archive mode has no state database topology"); - } - return reference; - } - /** Runs latest-state snapshot acquisition inside the canonical apply/flush monitor. */ public synchronized void withArchiveStateBarrier(ArchiveStateAction action) throws IOException { Objects.requireNonNull(action, "action").run(); @@ -582,50 +396,12 @@ public synchronized void fastPop() { throw new RevokingStoreIllegalStateException( String.format("there is not snapshot to be popped, current: %d", size)); } - BlockSnapshotMeta poppedMeta = currentStateHeadBlockMeta(); - if ((pendingArchiveForwardFlush != null && pendingArchiveForwardFlush.contains(poppedMeta)) - || sealedArchiveForwardFlushContains(poppedMeta)) { - throw new IllegalStateException("Cannot pop a block owned by pending archive flush"); - } - if (poppedMeta != null) { - AccountAssetPreparedBlockPayloadOwner owner = archiveForwardPayloadOwners.get(poppedMeta); - if (owner != null) { - owner.discard(); - archiveForwardPayloadOwners.remove(poppedMeta); - } + if (submittedArchiveHistoryEpoch != null) { + throw new IllegalStateException("Cannot pop while archive history flush is pending"); } pop(); } - private BlockSnapshotMeta currentStateHeadBlockMeta() { - BlockSnapshotMeta current = null; - for (Chainbase db : dbs) { - if (!ArchiveStoreScope.isStateDatabase(db.getDbName()) || !Snapshot.isImpl(db.getHead())) { - continue; - } - BlockSnapshotMeta candidate = ((SnapshotImpl) db.getHead()).getBlockSnapshotMeta(); - if (candidate != null && current != null && !current.equals(candidate)) { - throw new IllegalStateException("Current block metadata differs across state databases"); - } - if (candidate != null) { - current = candidate; - } - } - return current; - } - - private boolean sealedArchiveForwardFlushContains(BlockSnapshotMeta meta) { - if (sealedArchiveForwardFlush == null || meta == null) { - return false; - } - for (ArchiveBlockForwardPayload payload : sealedArchiveForwardFlush) { - if (meta.equals(payload.getMeta())) { - return true; - } - } - return false; - } - public synchronized void enable() { disabled = false; } @@ -664,11 +440,10 @@ public void shutdown() { } private synchronized Closeable prepareArchiveShutdown() { - abortArchiveForwardPayloads(); + submittedArchiveHistoryEpoch = null; if (archiveRuntimeAttachment != null) { archiveRuntimeAttachment = null; oldValueCollector = null; - archiveBlockProjectionPreparer = null; blockReverseDiffSink = null; archiveReadableEpoch = -1; return null; @@ -676,23 +451,6 @@ private synchronized Closeable prepareArchiveShutdown() { return blockReverseDiffSink instanceof Closeable ? (Closeable) blockReverseDiffSink : null; } - private synchronized void abortArchiveForwardPayloads() { - if (pendingArchiveForwardFlush != null) { - pendingArchiveForwardFlush.abortIfFrozen(); - pendingArchiveForwardFlush = null; - } - sealedArchiveForwardFlush = null; - submittedArchiveForwardHistoryEpoch = null; - for (Map.Entry entry - : archiveForwardPayloadOwners.entrySet()) { - AccountAssetPreparedBlockPayloadOwner owner = entry.getValue(); - if (owner != null && owner.isAttachedTo(entry.getKey())) { - owner.discard(); - } - } - archiveForwardPayloadOwners.clear(); - } - public void updateSolidity(int hops) { for (int i = 0; i < hops; i++) { for (Chainbase db : dbs) { @@ -782,19 +540,32 @@ private synchronized void flush(boolean force) { if (force || shouldBeRefreshed()) { try { long start = System.currentTimeMillis(); - Long archiveEpoch = publishArchiveHistoryForFlush(); + ArchiveWalBinding archiveBinding = publishArchiveHistoryForFlush(); if (!isV2Open()) { deleteCheckpoint(); } - createCheckpoint(); + createCheckpoint(archiveBinding); long checkPointEnd = System.currentTimeMillis(); - if (!publishArchiveForwardStateForFlush()) { - refresh(); + if (archiveBinding != null && archiveRuntimeAttachment != null) { + try { + archiveRuntimeAttachment.publishCommittedPrefix(archiveBinding.getLast()); + } catch (IOException | RuntimeException failure) { + throw new TronDBException("Archive committed-prefix publication failed", failure); + } + } + refresh(); + if (archiveBinding != null && archiveRuntimeAttachment != null) { + try { + archiveRuntimeAttachment.publishReadableState(archiveBinding.getLast()); + } catch (IOException | RuntimeException failure) { + throw new TronDBException("Archive readable-state publication failed", failure); + } } - if (archiveEpoch != null) { - archiveReadableEpoch = archiveEpoch; - ((DurableBlockReverseDiffSink) blockReverseDiffSink).releaseThrough(archiveEpoch); + if (archiveBinding != null) { + ((DurableBlockReverseDiffSink) blockReverseDiffSink) + .releaseThrough(archiveBinding.getLast().getEpoch()); + submittedArchiveHistoryEpoch = null; } flushCount = 0; logger.info("Flush cost: {} ms, create checkpoint cost: {} ms, refresh cost: {} ms.", @@ -810,15 +581,13 @@ private synchronized void flush(boolean force) { } } - private Long publishArchiveHistoryForFlush() { + private ArchiveWalBinding publishArchiveHistoryForFlush() { if (oldValueCollector == null) { return null; } if (!(blockReverseDiffSink instanceof DurableBlockReverseDiffSink)) { throw new TronDBException("Archive sink cannot prove durable history before checkpoint"); } - FrozenBatch frozenForward = archiveBlockProjectionPreparer == null - ? null : freezeArchiveForwardFlushRange(); Chainbase stateDatabase = dbs.stream() .filter(db -> ArchiveStoreScope.isStateDatabase(db.getDbName())) .findFirst() @@ -852,47 +621,29 @@ private Long publishArchiveHistoryForFlush() { try { DurableBlockReverseDiffSink durableSink = (DurableBlockReverseDiffSink) blockReverseDiffSink; - if (frozenForward == null || submittedArchiveForwardHistoryEpoch == null) { + if (submittedArchiveHistoryEpoch == null) { durableSink.acceptAll(prepared); - if (frozenForward != null) { - submittedArchiveForwardHistoryEpoch = last.getEpoch(); - } - } else if (submittedArchiveForwardHistoryEpoch.longValue() != last.getEpoch()) { + submittedArchiveHistoryEpoch = last.getEpoch(); + } else if (submittedArchiveHistoryEpoch.longValue() != last.getEpoch()) { throw new ArchivePersistenceException( - "Submitted archive history target does not match frozen forward range"); + "Submitted archive history target does not match the pending flush range"); } durableSink.awaitCommitted(last.getEpoch()); - if (frozenForward != null) { - DurableHistoryMarkerRangeEvidence evidence = - durableSink.createMarkerRangeEvidence(prepared.size()); - sealPendingArchiveForwardFlush(evidence); - submittedArchiveForwardHistoryEpoch = null; - } + List expectedMetas = prepared.stream() + .map(BlockReverseDiff::getMeta).collect(Collectors.toList()); + DurableHistoryMarkerRangeEvidence evidence = + durableSink.createMarkerRangeEvidence(prepared.size()); + return ArchiveWalBinding.fromMarkers(evidence.read(expectedMetas)); } catch (RuntimeException e) { throw new TronDBException("Archive history durability gate failed", e); } - return last.getEpoch(); - } - - private boolean publishArchiveForwardStateForFlush() { - ArchiveRuntimeAttachment runtime = archiveRuntimeAttachment; - if (runtime == null || !runtime.hasForwardFlushPublisher()) { - return false; - } - List payloads = claimArchiveForwardFlushPayloads(); - try { - runtime.publishForwardFlush(payloads, this::refreshOneArchiveTarget); - return true; - } catch (IOException | RuntimeException failure) { - throw new TronDBException("Archive forward publication failed", failure); - } } - private void refreshOneArchiveTarget() { - refresh(1); + public void createCheckpoint() { + createCheckpoint(null); } - public void createCheckpoint() { + private void createCheckpoint(ArchiveWalBinding archiveBinding) { TronDatabase checkPointStore = null; try { Map batch = new HashMap<>(); @@ -922,6 +673,10 @@ public void createCheckpoint() { } } } + if (archiveBinding != null) { + batch.put(WrappedByteArray.of(ArchiveWalBinding.getCheckpointKey()), + WrappedByteArray.of(new ArchiveWalBindingCodec().encode(archiveBinding))); + } if (isV2Open()) { String dbName = String.valueOf(System.currentTimeMillis()); checkPointStore = getCheckpointDB(dbName); @@ -932,6 +687,7 @@ public void createCheckpoint() { checkPointStore.updateByBatch(batch.entrySet().stream() .map(e -> Maps.immutableEntry(e.getKey().getBytes(), e.getValue().getBytes())) .collect(HashMap::new, (m, k) -> m.put(k.getKey(), k.getValue()), HashMap::putAll)); + latestArchiveWalBinding = archiveBinding; } catch (Exception e) { throw new TronDBException(e); @@ -1007,6 +763,7 @@ private void pruneCheckpoint() { // ensure run this method first after process start. @Override public void check() { + recoveredArchiveWalBinding = null; if (!isV2Open()) { List cpList = getCheckpointList(); if (cpList != null && cpList.size() != 0) { @@ -1050,21 +807,46 @@ private void checkV2() { continue; } TronDatabase checkPointV2Store = getCheckpointDB(cp); - recover(checkPointV2Store); - checkPointV2Store.close(); + try { + recover(checkPointV2Store); + } finally { + checkPointV2Store.close(); + } } logger.info("checkpoint v2 recover success"); unChecked = false; } private void recover(TronDatabase tronDatabase) { + List> entries = new ArrayList<>(); + ArchiveWalBinding recoveredBinding = null; + for (Map.Entry entry : tronDatabase.getDbSource()) { + byte[] key = Arrays.copyOf(entry.getKey(), entry.getKey().length); + byte[] value = Arrays.copyOf(entry.getValue(), entry.getValue().length); + if (ArchiveWalBinding.isCheckpointKey(key)) { + if (recoveredBinding != null) { + throw new ArchivePersistenceException( + "Checkpoint contains duplicate Archive WAL bindings"); + } + try { + recoveredBinding = new ArchiveWalBindingCodec().decode(value); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException( + "Checkpoint Archive WAL binding is corrupt", invalid); + } + } + entries.add(Maps.immutableEntry(key, value)); + } Map dbMap = dbs.stream() .map(db -> Maps.immutableEntry(db.getDbName(), db)) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); advance(); - for (Map.Entry e: tronDatabase.getDbSource()) { + for (Map.Entry e : entries) { byte[] key = e.getKey(); byte[] value = e.getValue(); + if (ArchiveWalBinding.isCheckpointKey(key)) { + continue; + } String db = simpleDecode(key); if (dbMap.get(db) == null) { continue; @@ -1085,6 +867,17 @@ private void recover(TronDatabase tronDatabase) { dbs.forEach(db -> db.getHead().getRoot().merge(db.getHead())); retreat(); + if (recoveredBinding != null) { + recoveredArchiveWalBinding = recoveredBinding; + } + } + + public ArchiveWalBinding getLatestArchiveWalBinding() { + return latestArchiveWalBinding; + } + + public ArchiveWalBinding getRecoveredArchiveWalBinding() { + return recoveredArchiveWalBinding; } private boolean isV2Open() { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index f02c3a7dfca..12733cefc28 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -116,14 +116,14 @@ import org.tron.core.db.api.MoveAbiHelper; import org.tron.core.db2.ISession; import org.tron.core.db2.archive.AccountAssetArchiveProjector; -import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge; -import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.TargetAssetOptimization; import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator.Result; import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator.Status; import org.tron.core.db2.archive.ArchiveHistoryWriter; +import org.tron.core.db2.archive.ArchiveStoreScope; import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.HistoricalAccountAssetBalanceResolver; import org.tron.core.db2.archive.HistoricalAccountBalanceReader; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; +import org.tron.core.db2.archive.OldValue; import org.tron.core.db2.archive.SnapshotOldValueCollector; import org.tron.core.db2.archive.StateArchiveRuntimeOwner; import org.tron.core.db2.core.Chainbase; @@ -204,6 +204,8 @@ public class Manager { private ArchiveHistoryWriter archiveHistoryWriter; @Getter private StateArchiveRuntimeOwner stateArchiveRuntime; + private StateArchiveRuntimeOwner.ServingIndexFaultHook stateArchiveServingIndexFaultHook = + stage -> { }; private static final int NO_BLOCK_WAITING_LOCK = 0; private final int shieldedTransInPendingMaxCounts = Args.getInstance().getShieldedTransInPendingMaxCounts(); @@ -639,22 +641,22 @@ private void initStateArchive() { } StateArchiveRuntimeOwner recovered = null; try { + long headNumber = getDynamicPropertiesStore().getLatestBlockHeaderNumber(); + BlockCapsule headBlock = chainBaseManager.getBlockByNum(headNumber); + BlockSnapshotMeta canonicalHead = BlockSnapshotMeta.forBlock(headNumber, + getDynamicPropertiesStore().getLatestBlockHeaderHash().getBytes(), + headBlock.getParentHash().getBytes(), headBlock.getTimeStamp()); if (admission.getStatus() == Status.EMPTY_NEW) { - long headNumber = getDynamicPropertiesStore().getLatestBlockHeaderNumber(); - BlockCapsule headBlock = chainBaseManager.getBlockByNum(headNumber); - BlockSnapshotMeta baseHead = BlockSnapshotMeta.forBlock(headNumber, - getDynamicPropertiesStore().getLatestBlockHeaderHash().getBytes(), - headBlock.getParentHash().getBytes(), headBlock.getTimeStamp()); - Phase phase = getDynamicPropertiesStore().supportAllowAssetOptimization() - ? Phase.P66_ON : Phase.P66_OFF; recovered = StateArchiveRuntimeOwner.bootstrapAndRecover( (SnapshotManager) revokingStore, archiveDirectory, - storage.getStateArchiveMaxSegmentSize(), storage.getDbEngine(), baseHead, phase); + storage.getStateArchiveMaxSegmentSize(), canonicalHead, + stateArchiveServingIndexFaultHook); logger.info("State archive fresh baseline published: directory={}, head={}", archiveDirectory, headNumber); } else { recovered = StateArchiveRuntimeOwner.recover((SnapshotManager) revokingStore, - archiveDirectory, storage.getStateArchiveMaxSegmentSize(), storage.getDbEngine()); + archiveDirectory, storage.getStateArchiveMaxSegmentSize(), + stateArchiveServingIndexFaultHook); } BlockSnapshotMeta archiveHead = recovered.getRecoveredHead(); if (archiveHead != null @@ -665,13 +667,30 @@ private void initStateArchive() { throw new IllegalStateException( "State archive committed head differs from the persisted state root"); } - AccountAssetBlockProjectionBridge bridge = new AccountAssetBlockProjectionBridge( - new AccountAssetArchiveProjector(), - accountKey -> getAccountAssetStore().prefixQuery(accountKey)); - archiveHistoryWriter = recovered.attachNormalWriter(new SnapshotOldValueCollector(), - view -> bridge.prepare(view, TargetAssetOptimization.forTarget(view.getMeta(), - getDynamicPropertiesStore().supportAllowAssetOptimization())), - storage.getStateArchiveQueueCapacity()); + java.util.Map + supplementalStores = java.util.Collections.emptyMap(); + SnapshotOldValueCollector archiveCollector = new SnapshotOldValueCollector(); + boolean accountAssetRegistered = ((SnapshotManager) revokingStore).getDbs().stream() + .anyMatch(database -> AccountAssetArchiveProjector.ACCOUNT_ASSET_DB + .equals(database.getDbName())); + if (!accountAssetRegistered) { + AccountAssetStore accountAssetStore = chainBaseManager.getAccountAssetStore(); + if (accountAssetStore == null) { + throw new IllegalStateException("State archive requires account-asset Store"); + } + supplementalStores = java.util.Collections.singletonMap( + AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + org.tron.core.db2.archive.LatestStateGenerationAdapter.fromDataSource( + AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + accountAssetStore.getDbSource())); + archiveCollector = new SnapshotOldValueCollector( + new AccountAssetArchiveProjector(), accountAssetStore::prefixQuery, + SnapshotOldValueCollector::resolveTargetAssetOptimization); + } + archiveHistoryWriter = recovered.attachNormalWriter(archiveCollector, + storage.getStateArchiveQueueCapacity(), canonicalHead, + supplementalStores); stateArchiveRuntime = recovered; recovered = null; logger.info("State archive runtime attached: directory={}, head={}, actions={}, engine={}", @@ -692,24 +711,58 @@ private void initStateArchive() { public HistoricalAccountBalanceReader.Result getArchiveAccountBalance(long blockNumber, byte[] address) throws ItemNotFoundException, BadItemException { - ArchiveHistoryWriter writer = archiveHistoryWriter; - if (writer == null) { + StateArchiveRuntimeOwner runtime = stateArchiveRuntime; + if (runtime == null) { throw new IllegalStateException("Experimental state archive is disabled"); } - BlockSnapshotMeta archiveHead = writer.committedHeadMeta(); - if (archiveHead == null - || ((SnapshotManager) revokingStore).getArchiveReadableEpoch() != archiveHead.getEpoch()) { - throw new IllegalStateException("State archive has no readable committed root"); + try (org.tron.core.db2.archive.ArchiveRuntimeQueryGate.Lease lease = + runtime.pinHistoricalState(blockNumber)) { + return HistoricalAccountBalanceReader.read(lease.getSnapshot(), address); + } catch (java.io.IOException failure) { + throw new org.tron.core.db2.archive.ArchivePersistenceException( + "Failed to read request-owned historical account snapshot", failure); } - AccountCapsule rootAccount; - try { - rootAccount = chainBaseManager.getAccountStore().getFromRoot(address); - } catch (ItemNotFoundException missing) { - rootAccount = null; + } + + /** Resolves one P66-aware historical TRC10 balance from a single request generation. */ + public HistoricalAccountAssetBalanceResolver.Result getArchiveAccountAssetBalance( + long blockNumber, byte[] address, String tokenId) { + StateArchiveRuntimeOwner runtime = stateArchiveRuntime; + if (runtime == null) { + throw new IllegalStateException("Experimental state archive is disabled"); + } + try (org.tron.core.db2.archive.ArchiveRuntimeQueryGate.Lease lease = + runtime.pinHistoricalState(blockNumber)) { + return new HistoricalAccountAssetBalanceResolver().resolve( + lease.getSnapshot(), address, tokenId); + } catch (java.io.IOException failure) { + throw new org.tron.core.db2.archive.ArchivePersistenceException( + "Failed to resolve request-owned historical AccountAsset snapshot", failure); } - org.tron.core.db2.archive.OldValue value = writer.readAccountAt(blockNumber, address, - rootAccount == null ? null : rootAccount.getData()); - return HistoricalAccountBalanceReader.decode(blockNumber, address, value); + } + + /** Reads one physical key from an exact versioned State Store at a historical block. */ + public OldValue getArchiveStateValue(long blockNumber, String dbName, byte[] physicalRawKey) { + if (!ArchiveStoreScope.isStateDatabase(dbName)) { + throw new IllegalArgumentException("Not a versioned archive state database: " + dbName); + } + Objects.requireNonNull(physicalRawKey, "physicalRawKey"); + StateArchiveRuntimeOwner runtime = stateArchiveRuntime; + if (runtime == null) { + throw new IllegalStateException("Experimental state archive is disabled"); + } + try (org.tron.core.db2.archive.ArchiveRuntimeQueryGate.Lease lease = + runtime.pinHistoricalState(blockNumber)) { + return lease.getSnapshot().get(dbName, physicalRawKey); + } catch (java.io.IOException failure) { + throw new org.tron.core.db2.archive.ArchivePersistenceException( + "Failed to read request-owned historical State Store snapshot", failure); + } + } + + /** Tests one physical key without opening range or cross-Store iteration semantics. */ + public boolean hasArchiveStateValue(long blockNumber, String dbName, byte[] physicalRawKey) { + return getArchiveStateValue(blockNumber, dbName, physicalRawKey).isPresent(); } /** From da2b5730b809026201ab784f2d4745c3566d6747 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 25 Aug 2026 10:04:54 +0800 Subject: [PATCH 059/161] test(chainbase): remove legacy archive coverage Drop participant, forward-projection, and standalone recovery tests for code paths replaced by the Chainbase checkpoint lifecycle. --- ...AccountAssetBlockProjectionBridgeTest.java | 852 ----------------- ...chiveBlockForwardMutationRecoveryTest.java | 851 ----------------- .../ArchiveMixedEngineProgressSourceTest.java | 195 ---- .../ArchiveParticipantBatchFileTest.java | 225 ----- ...ParticipantMutationBatchCollectorTest.java | 875 ------------------ ...ArchiveParticipantRecoveryStorageTest.java | 246 ----- .../ArchiveReaderPublicationGateTest.java | 292 ------ .../ArchiveRecoveryAuthorityScannerTest.java | 260 ------ .../archive/ArchiveRecoveryExecutorTest.java | 162 ---- .../archive/ArchiveRecoveryPlannerTest.java | 107 --- .../archive/ArchiveRecoveryScannerTest.java | 224 ----- .../ArchiveTargetApplyCoordinatorTest.java | 566 ----------- .../ArchiveTargetMutationPlanBuilderTest.java | 231 ----- .../LevelDbArchiveParticipantTest.java | 131 --- .../RocksDbArchiveParticipantTest.java | 167 ---- 15 files changed, 5384 deletions(-) delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveMixedEngineProgressSourceTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantBatchFileTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScannerTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryExecutorTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryPlannerTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryScannerTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java delete mode 100644 framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java diff --git a/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java b/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java deleted file mode 100644 index 53ab4356fb8..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/AccountAssetBlockProjectionBridgeTest.java +++ /dev/null @@ -1,852 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.common.primitives.Longs; -import com.google.protobuf.ByteString; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.Test; -import org.tron.common.BaseMethodTest; -import org.tron.common.utils.ByteArray; -import org.tron.core.db2.ISession; -import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.PreparedBlockProjection; -import org.tron.core.db2.archive.AccountAssetBlockProjectionBridge.TargetAssetOptimization; -import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; -import org.tron.core.db2.common.DB; -import org.tron.core.db2.common.Flusher; -import org.tron.core.db2.common.WrappedByteArray; -import org.tron.core.db2.core.Chainbase; -import org.tron.core.db2.core.SnapshotManager; -import org.tron.core.db2.core.SnapshotRoot; -import org.tron.core.store.AccountAssetStore; -import org.tron.protos.Protocol.Account; - -public class AccountAssetBlockProjectionBridgeTest extends BaseMethodTest { - - @Test - public void sharesExactAccountProjectionAcrossDeterministicReverseAndForwardBuilders() { - BlockSnapshotMeta meta = meta(1); - HistoryCommitMarker marker = marker(meta); - byte[] updateKey = accountKey(1); - byte[] deleteKey = accountKey(2); - byte[] updateAsset = assetKey(updateKey, "1000001"); - byte[] deleteAsset = assetKey(deleteKey, "1000002"); - Account oldUpdate = optimizedAccount(updateKey); - Account oldDelete = optimizedAccount(deleteKey); - Account postUpdate = oldUpdate.toBuilder().putAssetV2("1000001", 80L).build(); - AccountAssetStore assetStore = mock(AccountAssetStore.class); - when(assetStore.prefixQuery(any(byte[].class))).thenAnswer(invocation -> { - byte[] accountKey = invocation.getArgument(0); - Map assets = new LinkedHashMap<>(); - if (Arrays.equals(accountKey, updateKey)) { - assets.put(WrappedByteArray.copyOf(updateAsset), Longs.toByteArray(100L)); - } else if (Arrays.equals(accountKey, deleteKey)) { - assets.put(WrappedByteArray.copyOf(deleteAsset), Longs.toByteArray(200L)); - } - return assets; - }); - AccountAssetBlockProjectionBridge bridge = bridge(assetStore); - - try (Fixture fixture = new Fixture(participants())) { - fixture.rootPut("account", updateKey, oldUpdate.toByteArray()); - fixture.rootPut("account", deleteKey, oldDelete.toByteArray()); - BlockChangeView view = fixture.capture(meta, databases -> { - databases.get("account").put(updateKey, postUpdate.toByteArray()); - databases.get("account").delete(deleteKey); - }); - - PreparedBlockProjection first = bridge.prepare(view, - TargetAssetOptimization.forTarget(meta, true)); - verify(assetStore, times(2)).prefixQuery(any(byte[].class)); - BlockReverseDiff.DbGroup reverseAssets = group(first.getReverseDiff(), "account-asset"); - assertEquals(2, reverseAssets.getEntries().size()); - assertArrayEquals(Longs.toByteArray(100L), - reverseAssets.getEntries().get(0).getOldValue().getValue()); - assertArrayEquals(Longs.toByteArray(200L), - reverseAssets.getEntries().get(1).getOldValue().getValue()); - - ArchiveTargetMutationPlan firstPlan = plan(marker, view, first.seal(marker)); - assertArrayEquals(Longs.toByteArray(80L), - firstPlan.getMutations("account-asset").get(0).getValue()); - assertNull(firstPlan.getMutations("account-asset").get(1).getValue()); - - PreparedBlockProjection retry = bridge.prepare(view, - TargetAssetOptimization.forTarget(meta, true)); - verify(assetStore, times(4)).prefixQuery(any(byte[].class)); - ArchiveTargetMutationPlan retryPlan = plan(marker, view, retry.seal(marker)); - assertArrayEquals(new BlockHistoryCodec().encode(first.getReverseDiff()), - new BlockHistoryCodec().encode(retry.getReverseDiff())); - assertArrayEquals(firstPlan.digest(), retryPlan.digest()); - } - } - - @Test - public void rejectsActivationIdentityAndCoverageBeforeAnyPhysicalRead() { - BlockSnapshotMeta meta = meta(2); - HistoryCommitMarker marker = marker(meta); - byte[] accountKey = accountKey(3); - Account old = optimizedAccount(accountKey); - AccountAssetStore assetStore = mock(AccountAssetStore.class); - AccountAssetBlockProjectionBridge bridge = bridge(assetStore); - - try (Fixture exact = new Fixture(participants())) { - exact.rootPut("account", accountKey, old.toByteArray()); - BlockChangeView view = exact.capture(meta, - databases -> databases.get("account").delete(accountKey)); - assertThrows(ArchivePersistenceException.class, - () -> bridge.prepare(view, - TargetAssetOptimization.forTarget(meta(3), true))); - } - try (Fixture incomplete = new Fixture(Collections.singletonList("account"))) { - incomplete.rootPut("account", accountKey, old.toByteArray()); - BlockChangeView view = incomplete.capture(meta, - databases -> databases.get("account").delete(accountKey)); - assertThrows(ArchivePersistenceException.class, - () -> bridge.prepare(view, - TargetAssetOptimization.forTarget(meta, true))); - } - try (Fixture duplicateSource = new Fixture(archiveParticipants())) { - duplicateSource.rootPut("account", accountKey, old.toByteArray()); - BlockChangeView view = duplicateSource.capture(meta, databases -> { - databases.get("account").delete(accountKey); - databases.get("account-asset").put(assetKey(accountKey, "1000004"), - Longs.toByteArray(40L)); - }); - assertThrows(ArchivePersistenceException.class, - () -> bridge.prepare(view, - TargetAssetOptimization.forTarget(meta, true))); - } - verify(assetStore, never()).prefixQuery(any(byte[].class)); - } - - @Test - public void projectionFailurePublishesNoPartialResultAndAllowsFreshRetry() { - BlockSnapshotMeta meta = meta(4); - HistoryCommitMarker marker = marker(meta); - byte[] validKey = accountKey(4); - byte[] invalidKey = accountKey(5); - Account validOld = optimizedAccount(validKey); - Account validPost = validOld.toBuilder().putAssetV2("1000003", 30L).build(); - AccountAssetStore assetStore = mock(AccountAssetStore.class); - when(assetStore.prefixQuery(any(byte[].class))).thenReturn(Collections.emptyMap()); - AccountAssetBlockProjectionBridge bridge = bridge(assetStore); - - try (Fixture fixture = new Fixture(participants())) { - fixture.rootPut("account", validKey, validOld.toByteArray()); - BlockChangeView failing = fixture.capture(meta, databases -> { - databases.get("account").put(validKey, validPost.toByteArray()); - databases.get("account").put(invalidKey, bytes(3, 99)); - }); - assertThrows(ArchivePersistenceException.class, - () -> bridge.prepare(failing, - TargetAssetOptimization.forTarget(meta, true))); - - BlockChangeView retry = fixture.capture(meta, - databases -> databases.get("account").put(validKey, validPost.toByteArray())); - PreparedBlockProjection result = bridge.prepare(retry, - TargetAssetOptimization.forTarget(meta, true)); - assertEquals(meta, result.getReverseDiff().getMeta()); - assertEquals(1, plan(marker, retry, result.seal(marker)) - .getMutations("account").size()); - } - verify(assetStore, times(2)).prefixQuery(any(byte[].class)); - } - - @Test - public void physicalInputFailurePublishesNothingAndFreshPrepareCanRetry() { - BlockSnapshotMeta meta = meta(24); - byte[] accountKey = accountKey(24); - Account old = optimizedAccount(accountKey); - int[] failureMode = {1}; - AccountAssetOldPhysicalAssetsSource source = key -> { - if (failureMode[0] == 1) { - throw new IllegalStateException("injected physical input failure"); - } - if (failureMode[0] == 2) { - return Collections.singletonMap( - WrappedByteArray.copyOf(assetKey(key, "01000001")), Longs.toByteArray(1L)); - } - if (failureMode[0] == 3) { - return Collections.singletonMap( - WrappedByteArray.copyOf(assetKey(key, "1000001")), new byte[7]); - } - if (failureMode[0] == 4) { - return Collections.singletonMap( - WrappedByteArray.copyOf(assetKey(key, "1000001")), Longs.toByteArray(0L)); - } - return Collections.emptyMap(); - }; - AccountAssetBlockProjectionBridge bridge = new AccountAssetBlockProjectionBridge( - new AccountAssetArchiveProjector(), source); - - try (Fixture fixture = new Fixture(participants())) { - fixture.rootPut("account", accountKey, old.toByteArray()); - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").delete(accountKey)); - TargetAssetOptimization activation = TargetAssetOptimization.forTarget(meta, true); - - ArchivePersistenceException failure = assertThrows(ArchivePersistenceException.class, - () -> bridge.prepare(view, activation)); - assertTrue(failure.getMessage().contains("old physical AccountAsset input")); - - for (int mode = 2; mode <= 4; mode++) { - failureMode[0] = mode; - assertThrows(ArchivePersistenceException.class, - () -> bridge.prepare(view, activation)); - } - failureMode[0] = 0; - PreparedBlockProjection prepared = bridge.prepare(view, activation); - assertEquals(meta, prepared.getReverseDiff().getMeta()); - prepared.abort(); - } - } - - @Test - public void resolvesActivationBlockFromProposalSixtySixAndFeedsSharedBridge() { - BlockSnapshotMeta meta = meta(5); - HistoryCommitMarker marker = marker(meta); - byte[] accountKey = accountKey(6); - byte[] physicalAsset = assetKey(accountKey, "1000005"); - Account old = optimizedAccount(accountKey); - Account post = old.toBuilder().putAssetV2("1000005", 50L).build(); - AccountAssetStore assetStore = mock(AccountAssetStore.class); - Map assets = new LinkedHashMap<>(); - assets.put(WrappedByteArray.copyOf(physicalAsset), Longs.toByteArray(40L)); - when(assetStore.prefixQuery(any(byte[].class))).thenReturn(assets); - AccountAssetBlockProjectionBridge bridge = bridge(assetStore); - AccountAssetTargetActivationResolver resolver = - new AccountAssetTargetActivationResolver(); - - try (Fixture fixture = new Fixture(participants())) { - fixture.rootPut("account", accountKey, old.toByteArray()); - fixture.rootPut("properties", proposal66Key(), ByteArray.fromLong(0L)); - BlockChangeView view = fixture.capture(meta, databases -> { - databases.get("properties").put(proposal66Key(), ByteArray.fromLong(1L)); - databases.get("properties").put(proposal53Key(), ByteArray.fromLong(0L)); - databases.get("account").put(accountKey, post.toByteArray()); - }); - - TargetAssetOptimization activation = resolver.resolve(meta, view); - PreparedBlockProjection result = bridge.prepare(view, activation); - assertTrue(activation.isEnabled()); - assertEquals(Phase.P66_ACTIVATION, activation.getPhase()); - ArchiveTargetMutationPlan plan = plan(marker, view, result.seal(marker)); - assertEquals(Phase.P66_ACTIVATION, plan.getTargetPhase()); - assertArrayEquals(Longs.toByteArray(50L), - plan.getMutations("account-asset").get(0).getValue()); - } - verify(assetStore, times(1)).prefixQuery(any(byte[].class)); - } - - @Test - public void inheritsUnchangedProposalSixtySixWithoutUsingProposalFiftyThree() { - BlockSnapshotMeta meta = meta(6); - HistoryCommitMarker marker = marker(meta); - byte[] accountKey = accountKey(7); - Account raw = Account.newBuilder() - .setAddress(ByteString.copyFrom(accountKey)) - .putAssetV2("1000006", 60L) - .build(); - AccountAssetStore assetStore = mock(AccountAssetStore.class); - AccountAssetBlockProjectionBridge bridge = bridge(assetStore); - AccountAssetTargetActivationResolver resolver = - new AccountAssetTargetActivationResolver(); - - try (Fixture fixture = new Fixture(participants())) { - fixture.rootPut("properties", proposal66Key(), ByteArray.fromLong(0L)); - fixture.rootPut("properties", proposal53Key(), ByteArray.fromLong(1L)); - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").put(accountKey, raw.toByteArray())); - - TargetAssetOptimization activation = resolver.resolve(meta, view); - PreparedBlockProjection result = bridge.prepare(view, activation); - assertFalse(activation.isEnabled()); - assertEquals(Phase.P66_OFF, activation.getPhase()); - ArchiveTargetMutationPlan plan = plan(marker, view, result.seal(marker)); - assertEquals(Phase.P66_OFF, plan.getTargetPhase()); - assertArrayEquals(raw.toByteArray(), plan.getMutations("account").get(0).getValue()); - assertEquals(0, plan.getMutations("account-asset").size()); - } - verify(assetStore, never()).prefixQuery(any(byte[].class)); - } - - @Test - public void rejectsMissingCorruptSubstitutedAndReorgActivationBeforePrefix() { - BlockSnapshotMeta meta = meta(7); - HistoryCommitMarker marker = marker(meta); - byte[] accountKey = accountKey(8); - Account old = optimizedAccount(accountKey); - AccountAssetStore assetStore = mock(AccountAssetStore.class); - AccountAssetBlockProjectionBridge bridge = bridge(assetStore); - AccountAssetTargetActivationResolver resolver = - new AccountAssetTargetActivationResolver(); - - try (Fixture missing = new Fixture(participants())) { - missing.rootPut("account", accountKey, old.toByteArray()); - missing.rootPut("properties", proposal53Key(), ByteArray.fromLong(1L)); - BlockChangeView view = missing.capture(meta, - databases -> databases.get("account").delete(accountKey)); - assertThrows(ArchivePersistenceException.class, - () -> bridge.prepare(view, resolver.resolve(meta, view))); - } - try (Fixture corrupt = new Fixture(participants())) { - corrupt.rootPut("account", accountKey, old.toByteArray()); - corrupt.rootPut("properties", proposal66Key(), new byte[] {1}); - BlockChangeView view = corrupt.capture(meta, - databases -> databases.get("account").delete(accountKey)); - assertThrows(ArchivePersistenceException.class, - () -> bridge.prepare(view, resolver.resolve(meta, view))); - } - try (Fixture noncanonical = new Fixture(participants())) { - noncanonical.rootPut("account", accountKey, old.toByteArray()); - noncanonical.rootPut("properties", proposal66Key(), ByteArray.fromLong(2L)); - BlockChangeView view = noncanonical.capture(meta, - databases -> databases.get("account").delete(accountKey)); - assertThrows(ArchivePersistenceException.class, - () -> bridge.prepare(view, resolver.resolve(meta, view))); - } - try (Fixture deleted = new Fixture(participants())) { - deleted.rootPut("account", accountKey, old.toByteArray()); - deleted.rootPut("properties", proposal66Key(), ByteArray.fromLong(1L)); - BlockChangeView view = deleted.capture(meta, databases -> { - databases.get("properties").delete(proposal66Key()); - databases.get("account").delete(accountKey); - }); - assertThrows(ArchivePersistenceException.class, - () -> bridge.prepare(view, resolver.resolve(meta, view))); - } - try (Fixture reorg = new Fixture(participants())) { - reorg.rootPut("account", accountKey, old.toByteArray()); - reorg.rootPut("properties", proposal66Key(), ByteArray.fromLong(1L)); - BlockChangeView view = reorg.capture(meta, - databases -> databases.get("account").delete(accountKey)); - assertThrows(ArchivePersistenceException.class, - () -> bridge.prepare(view, resolver.resolve(meta(8), view))); - } - try (Fixture regressed = new Fixture(participants())) { - regressed.rootPut("properties", proposal66Key(), ByteArray.fromLong(1L)); - BlockChangeView view = regressed.capture(meta, - databases -> databases.get("properties").put( - proposal66Key(), ByteArray.fromLong(0L))); - assertThrows(ArchivePersistenceException.class, - () -> resolver.resolve(meta, view)); - } - verify(assetStore, never()).prefixQuery(any(byte[].class)); - } - - @Test - public void rejectsWrongMarkerWithoutConsumingPreparedProjectionAndSealsExactlyOnce() { - BlockSnapshotMeta meta = meta(9); - HistoryCommitMarker marker = marker(meta); - byte[] accountKey = accountKey(9); - Account account = optimizedAccount(accountKey); - AccountAssetStore assetStore = mock(AccountAssetStore.class); - when(assetStore.prefixQuery(any(byte[].class))).thenReturn(Collections.emptyMap()); - AccountAssetBlockProjectionBridge bridge = bridge(assetStore); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").put(accountKey, account.toByteArray())); - PreparedBlockProjection prepared = bridge.prepare(view, - TargetAssetOptimization.forTarget(meta, true)); - - assertThrows(ArchivePersistenceException.class, - () -> prepared.seal(marker(meta(10)))); - assertThrows(ArchivePersistenceException.class, - () -> prepared.seal(marker(meta, Collections.singletonList("account")))); - assertEquals(meta, prepared.getReverseDiff().getMeta()); - assertTrue(prepared.retainsCapturedView()); - - AccountAssetForwardMutationManifest manifest = prepared.seal(marker); - assertEquals(1, plan(marker, view, manifest).getMutations("account").size()); - assertFalse(prepared.retainsCapturedView()); - assertThrows(ArchivePersistenceException.class, () -> prepared.seal(marker)); - assertThrows(ArchivePersistenceException.class, prepared::abort); - } - } - - @Test - public void sealsEmptyBlockAndAbortReleasesPreparedPayload() { - BlockSnapshotMeta meta = meta(11); - HistoryCommitMarker marker = marker(meta); - AccountAssetStore assetStore = mock(AccountAssetStore.class); - AccountAssetBlockProjectionBridge bridge = bridge(assetStore); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView view = fixture.capture(meta, databases -> { }); - TargetAssetOptimization activation = TargetAssetOptimization.forTarget(meta, false); - PreparedBlockProjection sealed = bridge.prepare(view, activation); - assertTrue(sealed.getReverseDiff().getGroups().isEmpty()); - assertTrue(plan(marker, view, sealed.seal(marker)).getMutations().values().stream() - .allMatch(List::isEmpty)); - - PreparedBlockProjection aborted = bridge.prepare(view, activation); - assertTrue(aborted.retainsCapturedView()); - aborted.abort(); - assertFalse(aborted.retainsCapturedView()); - assertThrows(ArchivePersistenceException.class, aborted::getReverseDiff); - assertThrows(ArchivePersistenceException.class, () -> aborted.seal(marker)); - assertThrows(ArchivePersistenceException.class, aborted::abort); - } - verify(assetStore, never()).prefixQuery(any(byte[].class)); - } - - @Test - public void layerOwnersTransferContiguousPayloadsAndBatchSealExactlyOnce() { - BlockSnapshotMeta firstMeta = meta(12); - BlockSnapshotMeta secondMeta = meta(13); - HistoryCommitMarker firstMarker = marker(firstMeta); - HistoryCommitMarker secondMarker = marker(secondMeta); - AccountAssetBlockProjectionBridge bridge = emptyBridge(); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView firstView = fixture.capture(firstMeta, databases -> { }); - BlockChangeView secondView = fixture.capture(secondMeta, databases -> { }); - PreparedBlockProjection first = bridge.prepare(firstView, - TargetAssetOptimization.forTarget(firstMeta, false)); - PreparedBlockProjection second = bridge.prepare(secondView, - TargetAssetOptimization.forTarget(secondMeta, false)); - AccountAssetPreparedBlockPayloadOwner firstOwner = - new AccountAssetPreparedBlockPayloadOwner(firstMeta); - AccountAssetPreparedBlockPayloadOwner secondOwner = - new AccountAssetPreparedBlockPayloadOwner(secondMeta); - firstOwner.attach(first); - secondOwner.attach(second); - assertEquals(firstMeta, firstOwner.getReverseDiff().getMeta()); - - FrozenBatch batch = AccountAssetPreparedBlockPayloadOwner.freezeContiguous( - Arrays.asList(firstOwner, secondOwner)); - assertThrows(ArchivePersistenceException.class, firstOwner::getReverseDiff); - assertThrows(ArchivePersistenceException.class, firstOwner::discard); - assertThrows(ArchivePersistenceException.class, - () -> AccountAssetPreparedBlockPayloadOwner.freezeContiguous( - Collections.singletonList(secondOwner))); - - assertThrows(ArchivePersistenceException.class, - () -> batch.seal(Arrays.asList(firstMarker, marker(meta(14))))); - assertTrue(first.retainsCapturedView()); - assertTrue(second.retainsCapturedView()); - List payloads = batch.seal( - Arrays.asList(firstMarker, secondMarker)); - assertEquals(2, payloads.size()); - assertEquals(firstMeta, payloads.get(0).getMeta()); - assertSame(firstView, payloads.get(0).getView()); - assertEquals(secondMeta, payloads.get(1).getMeta()); - assertSame(secondView, payloads.get(1).getView()); - assertTrue(plan(firstMarker, payloads.get(0).getView(), - payloads.get(0).getAccountAssetManifest()).getMutations().values().stream() - .allMatch(List::isEmpty)); - assertFalse(first.retainsCapturedView()); - assertFalse(second.retainsCapturedView()); - assertThrows(ArchivePersistenceException.class, - () -> batch.seal(Arrays.asList(firstMarker, secondMarker))); - assertThrows(ArchivePersistenceException.class, batch::abort); - } - } - - @Test - public void sealedForwardPayloadCarriesExactViewAndRejectsMixedIdentity() { - BlockSnapshotMeta firstMeta = meta(14); - BlockSnapshotMeta secondMeta = meta(15); - AccountAssetBlockProjectionBridge bridge = emptyBridge(); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView firstView = fixture.capture(firstMeta, databases -> { }); - BlockChangeView secondView = fixture.capture(secondMeta, databases -> { }); - PreparedBlockProjection prepared = bridge.prepare(firstView, - TargetAssetOptimization.forTarget(firstMeta, false)); - - ArchiveBlockForwardPayload payload = prepared.sealPayload(marker(firstMeta)); - assertEquals(firstMeta, payload.getMeta()); - assertSame(firstView, payload.getView()); - assertFalse(prepared.retainsCapturedView()); - assertThrows(ArchivePersistenceException.class, - () -> prepared.sealPayload(marker(firstMeta))); - - AccountAssetForwardMutationManifest manifest = - new AccountAssetForwardMutationManifest(marker(firstMeta), Phase.P66_OFF, - Collections.emptyList()); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveBlockForwardPayload(marker(firstMeta), secondView, manifest)); - } - } - - @Test - public void layerOwnerRejectsWrongTargetAndDoubleAttachWithoutConsumingCallerPayload() { - BlockSnapshotMeta firstMeta = meta(15); - BlockSnapshotMeta secondMeta = meta(16); - AccountAssetBlockProjectionBridge bridge = emptyBridge(); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView firstView = fixture.capture(firstMeta, databases -> { }); - BlockChangeView secondView = fixture.capture(secondMeta, databases -> { }); - PreparedBlockProjection first = bridge.prepare(firstView, - TargetAssetOptimization.forTarget(firstMeta, false)); - PreparedBlockProjection second = bridge.prepare(secondView, - TargetAssetOptimization.forTarget(secondMeta, false)); - PreparedBlockProjection sealed = bridge.prepare(firstView, - TargetAssetOptimization.forTarget(firstMeta, false)); - sealed.seal(marker(firstMeta)); - PreparedBlockProjection aborted = bridge.prepare(firstView, - TargetAssetOptimization.forTarget(firstMeta, false)); - aborted.abort(); - AccountAssetPreparedBlockPayloadOwner owner = - new AccountAssetPreparedBlockPayloadOwner(firstMeta); - - assertThrows(ArchivePersistenceException.class, () -> owner.attach(second)); - assertEquals(secondMeta, second.getReverseDiff().getMeta()); - assertThrows(ArchivePersistenceException.class, () -> owner.attach(sealed)); - assertThrows(ArchivePersistenceException.class, () -> owner.attach(aborted)); - owner.attach(first); - assertThrows(ArchivePersistenceException.class, () -> owner.attach(second)); - assertEquals(secondMeta, second.getReverseDiff().getMeta()); - owner.discard(); - second.abort(); - } - } - - @Test - public void fastPopDiscardAndFrozenShutdownAbortReleaseEveryPayload() { - BlockSnapshotMeta firstMeta = meta(17); - BlockSnapshotMeta secondMeta = meta(18); - AccountAssetBlockProjectionBridge bridge = emptyBridge(); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView firstView = fixture.capture(firstMeta, databases -> { }); - BlockChangeView secondView = fixture.capture(secondMeta, databases -> { }); - PreparedBlockProjection discarded = bridge.prepare(firstView, - TargetAssetOptimization.forTarget(firstMeta, false)); - AccountAssetPreparedBlockPayloadOwner discardedOwner = - new AccountAssetPreparedBlockPayloadOwner(firstMeta); - discardedOwner.attach(discarded); - discardedOwner.discard(); - assertFalse(discarded.retainsCapturedView()); - assertThrows(ArchivePersistenceException.class, discarded::getReverseDiff); - assertThrows(ArchivePersistenceException.class, discardedOwner::discard); - - PreparedBlockProjection frozen = bridge.prepare(secondView, - TargetAssetOptimization.forTarget(secondMeta, false)); - AccountAssetPreparedBlockPayloadOwner frozenOwner = - new AccountAssetPreparedBlockPayloadOwner(secondMeta); - frozenOwner.attach(frozen); - FrozenBatch batch = AccountAssetPreparedBlockPayloadOwner.freezeContiguous( - Collections.singletonList(frozenOwner)); - batch.abort(); - assertFalse(frozen.retainsCapturedView()); - assertThrows(ArchivePersistenceException.class, frozen::getReverseDiff); - assertThrows(ArchivePersistenceException.class, batch::abort); - assertThrows(ArchivePersistenceException.class, - () -> batch.seal(Collections.singletonList(marker(secondMeta)))); - } - } - - @Test - public void nonContiguousAndDuplicateFreezeFailureLeavesLayerOwnersAttached() { - BlockSnapshotMeta firstMeta = meta(19); - BlockSnapshotMeta thirdMeta = meta(21); - AccountAssetBlockProjectionBridge bridge = emptyBridge(); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView firstView = fixture.capture(firstMeta, databases -> { }); - BlockChangeView thirdView = fixture.capture(thirdMeta, databases -> { }); - AccountAssetPreparedBlockPayloadOwner firstOwner = - new AccountAssetPreparedBlockPayloadOwner(firstMeta); - AccountAssetPreparedBlockPayloadOwner thirdOwner = - new AccountAssetPreparedBlockPayloadOwner(thirdMeta); - firstOwner.attach(bridge.prepare(firstView, - TargetAssetOptimization.forTarget(firstMeta, false))); - thirdOwner.attach(bridge.prepare(thirdView, - TargetAssetOptimization.forTarget(thirdMeta, false))); - - assertThrows(ArchivePersistenceException.class, - () -> AccountAssetPreparedBlockPayloadOwner.freezeContiguous( - Arrays.asList(firstOwner, thirdOwner))); - assertThrows(ArchivePersistenceException.class, - () -> AccountAssetPreparedBlockPayloadOwner.freezeContiguous( - Arrays.asList(firstOwner, firstOwner))); - assertEquals(firstMeta, firstOwner.getReverseDiff().getMeta()); - assertEquals(thirdMeta, thirdOwner.getReverseDiff().getMeta()); - firstOwner.discard(); - thirdOwner.discard(); - } - } - - @Test - public void durableMarkerEvidenceFailureLeavesFrozenBatchRetryable() { - BlockSnapshotMeta meta = meta(22); - HistoryCommitMarker marker = marker(meta); - AccountAssetBlockProjectionBridge bridge = emptyBridge(); - boolean[] substitute = {true}; - DurableHistoryMarkerRangeEvidence.Source source = - new DurableHistoryMarkerRangeEvidence.Source() { - @Override - public HistoryCommitMarker marker(long epoch) { - return substitute[0] ? AccountAssetBlockProjectionBridgeTest.marker(meta(23)) : marker; - } - - @Override - public BlockReverseDiff readCommitted(long epoch) { - return new BlockReverseDiff(meta, Collections.emptyList()); - } - }; - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView view = fixture.capture(meta, databases -> { }); - AccountAssetPreparedBlockPayloadOwner owner = - new AccountAssetPreparedBlockPayloadOwner(meta); - PreparedBlockProjection prepared = bridge.prepare(view, - TargetAssetOptimization.forTarget(meta, false)); - owner.attach(prepared); - FrozenBatch batch = AccountAssetPreparedBlockPayloadOwner.freezeContiguous( - Collections.singletonList(owner)); - DurableHistoryMarkerRangeEvidence evidence = - new DurableHistoryMarkerRangeEvidence(source, 1); - - assertThrows(ArchivePersistenceException.class, () -> evidence.seal(batch)); - assertEquals(meta, batch.getExpectedMetas().get(0)); - assertTrue(prepared.retainsCapturedView()); - substitute[0] = false; - List payloads = evidence.seal(batch); - assertEquals(1, payloads.size()); - assertSame(view, payloads.get(0).getView()); - assertFalse(prepared.retainsCapturedView()); - assertTrue(plan(payloads.get(0).getMarker(), payloads.get(0).getView(), - payloads.get(0).getAccountAssetManifest()).getMutations().values().stream() - .allMatch(List::isEmpty)); - assertThrows(ArchivePersistenceException.class, () -> evidence.seal(batch)); - } - } - - private static AccountAssetBlockProjectionBridge emptyBridge() { - return new AccountAssetBlockProjectionBridge(new AccountAssetArchiveProjector(), - accountKey -> Collections.emptyMap()); - } - - private static AccountAssetBlockProjectionBridge bridge(AccountAssetStore assetStore) { - return new AccountAssetBlockProjectionBridge(new AccountAssetArchiveProjector(), - accountKey -> assetStore.prefixQuery(accountKey)); - } - - private static ArchiveTargetMutationPlan plan(HistoryCommitMarker marker, BlockChangeView view, - AccountAssetForwardMutationManifest manifest) { - ArchiveParticipantMutationBatch batch = new ArchiveParticipantMutationBatchCollector( - manifest).collect(marker, view); - return new ArchiveTargetMutationPlanBuilder().build(marker, batch); - } - - private static BlockReverseDiff.DbGroup group(BlockReverseDiff diff, String dbName) { - return diff.getGroups().stream() - .filter(group -> dbName.equals(group.getDbName())) - .findFirst() - .orElseThrow(AssertionError::new); - } - - private static Account optimizedAccount(byte[] accountKey) { - return Account.newBuilder() - .setAddress(ByteString.copyFrom(accountKey)) - .setAssetOptimized(true) - .build(); - } - - private static byte[] accountKey(int suffix) { - byte[] key = new byte[21]; - key[0] = 0x41; - key[20] = (byte) suffix; - return key; - } - - private static byte[] assetKey(byte[] accountKey, String token) { - byte[] tokenBytes = token.getBytes(java.nio.charset.StandardCharsets.UTF_8); - byte[] key = Arrays.copyOf(accountKey, accountKey.length + tokenBytes.length); - System.arraycopy(tokenBytes, 0, key, accountKey.length, tokenBytes.length); - return key; - } - - private static byte[] proposal66Key() { - return AccountAssetTargetActivationResolver.proposal66PhysicalKey(); - } - - private static byte[] proposal53Key() { - return AccountAssetTargetActivationResolver.PROPOSAL_53_KEY.getBytes( - java.nio.charset.StandardCharsets.UTF_8); - } - - private static List participants() { - List participants = archiveParticipants(); - participants.remove(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB); - return participants; - } - - private static List archiveParticipants() { - List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); - Collections.sort(participants); - return participants; - } - - private static BlockSnapshotMeta meta(int epoch) { - return BlockSnapshotMeta.forBlock(epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L); - } - - private static HistoryCommitMarker marker(BlockSnapshotMeta meta) { - return marker(meta, archiveParticipants()); - } - - private static HistoryCommitMarker marker(BlockSnapshotMeta meta, - List markerParticipants) { - int epoch = (int) meta.getEpoch(); - return new HistoryCommitMarker(meta, epoch - 1, - new HistoryLocation(0, epoch * 100L, 100, epoch, bytes(32, epoch + 20)), - new HistoryIndexLocation(epoch * 50L, 50, bytes(32, epoch + 30)), - bytes(16, epoch + 40), markerParticipants); - } - - private static byte[] hash(int suffix) { - byte[] hash = new byte[32]; - hash[31] = (byte) suffix; - return hash; - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } - - @FunctionalInterface - private interface Mutator { - void mutate(Map databases); - } - - private static final class Fixture implements AutoCloseable { - private final SnapshotManager manager = new SnapshotManager(""); - private final Map roots = new LinkedHashMap<>(); - private final Map databases = new LinkedHashMap<>(); - private final List ordered = new ArrayList<>(); - - private Fixture(List participants) { - for (String participant : participants) { - MemoryDb root = new MemoryDb(participant); - Chainbase database = new Chainbase(new SnapshotRoot(root)); - roots.put(participant, root); - databases.put(participant, database); - ordered.add(database); - manager.add(database); - } - manager.enable(); - } - - private void rootPut(String dbName, byte[] key, byte[] value) { - roots.get(dbName).put(key, value); - } - - private BlockChangeView capture(BlockSnapshotMeta meta, Mutator mutator) { - try (ISession session = manager.buildSession()) { - mutator.mutate(databases); - return BlockChangeView.capture(meta, ordered); - } - } - - @Override - public void close() { - manager.shutdown(); - } - } - - private static final class MemoryDb implements DB, Flusher { - private final String name; - private final Map values = new LinkedHashMap<>(); - - private MemoryDb(String name) { - this.name = name; - } - - @Override - public byte[] get(byte[] key) { - byte[] value = values.get(WrappedByteArray.copyOf(key)); - return value == null ? null : Arrays.copyOf(value, value.length); - } - - @Override - public void put(byte[] key, byte[] value) { - values.put(WrappedByteArray.copyOf(key), Arrays.copyOf(value, value.length)); - } - - @Override - public long size() { - return values.size(); - } - - @Override - public boolean isEmpty() { - return values.isEmpty(); - } - - @Override - public void remove(byte[] key) { - values.remove(WrappedByteArray.copyOf(key)); - } - - @Override - public Iterator> iterator() { - List> entries = new ArrayList<>(); - values.forEach((key, value) -> entries.add(new AbstractMap.SimpleImmutableEntry<>( - key.getBytes(), Arrays.copyOf(value, value.length)))); - return entries.iterator(); - } - - @Override - public void flush(Map rows) { - rows.forEach((key, value) -> { - if (value == null || value.getBytes() == null) { - remove(key.getBytes()); - } else { - put(key.getBytes(), value.getBytes()); - } - }); - } - - @Override - public void close() { - values.clear(); - } - - @Override - public void reset() { - values.clear(); - } - - @Override - public String getDbName() { - return name; - } - - @Override - public void stat() { - } - - @Override - public DB newInstance() { - return new MemoryDb(name); - } - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java deleted file mode 100644 index 167ae5250bd..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBlockForwardMutationRecoveryTest.java +++ /dev/null @@ -1,851 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -import com.google.protobuf.ByteString; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Test; -import org.tron.common.BaseMethodTest; -import org.tron.core.db2.ISession; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; -import org.tron.core.db2.archive.ArchiveTargetApplyCoordinator.Stage; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; -import org.tron.core.db2.common.DB; -import org.tron.core.db2.common.Flusher; -import org.tron.core.db2.common.WrappedByteArray; -import org.tron.core.db2.core.Chainbase; -import org.tron.core.db2.core.SnapshotManager; -import org.tron.core.db2.core.SnapshotRoot; -import org.tron.protos.Protocol.Account; - -/** End-to-end ownership and recovery test from block capture to durable mixed participants. */ -public class ArchiveBlockForwardMutationRecoveryTest extends BaseMethodTest { - - private static final List PARTICIPANTS = participants(); - - @Test - public void captureBatchRecoversOnlyRemainingParticipantsFromDurablePlan() throws Exception { - Path archive = temporaryFolder.newFolder("capture-recovery").toPath(); - List markers = initializeHistory(archive, 1); - HistoryCommitMarker initial = markers.get(0); - HistoryCommitMarker target = markers.get(1); - Path checkpointPath = archive.resolve("progress/checkpoint.progress"); - Path readerPath = archive.resolve("progress/reader.progress"); - ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); - new ArchiveProgressFile(checkpointPath, progressCodec).store(global(Kind.APPLY_CHECKPOINT, - initial)); - new ArchiveProgressFile(readerPath, progressCodec).store(global(Kind.READER_VISIBLE, initial)); - - byte[] accountKey = bytes(2, 1); - byte[] rawAccount = bytes(3, 2); - byte[] canonicalAccount = bytes(3, 3); - byte[] assetKey = append(accountKey, 4); - byte[] assetValue = bytes(3, 5); - byte[] proposalKey = bytes(2, 6); - byte[] proposalValue = bytes(3, 7); - byte[] expectedAccountKey = copy(accountKey); - byte[] expectedCanonicalAccount = copy(canonicalAccount); - byte[] expectedAssetKey = copy(assetKey); - byte[] expectedAssetValue = copy(assetValue); - byte[] expectedProposalKey = copy(proposalKey); - byte[] expectedProposalValue = copy(proposalValue); - - LevelDbArchiveParticipant account = new LevelDbArchiveParticipant( - archive.resolve("participants/account"), "account", PARTICIPANTS); - RocksDbArchiveParticipant accountAsset = new RocksDbArchiveParticipant( - archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS); - Map counted = new LinkedHashMap<>(); - Map memory = new LinkedHashMap<>(); - Map engines = new LinkedHashMap<>(); - try { - for (String participant : PARTICIPANTS) { - ArchiveParticipant delegate; - if ("account".equals(participant)) { - delegate = account; - } else if ("account-asset".equals(participant)) { - delegate = accountAsset; - } else { - MemoryParticipant inMemory = new MemoryParticipant(); - memory.put(participant, inMemory); - delegate = inMemory; - } - CountingParticipant engine = new CountingParticipant(delegate); - engine.apply(Collections.emptyList(), participant(participant, initial)); - counted.put(participant, engine); - engines.put(participant, engine); - } - - ArchiveParticipantMutationBatch batch; - try (ViewFixture viewFixture = new ViewFixture()) { - ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture( - target.getMeta(), Phase.P66_ON, - new ArchiveBlockForwardMutationLimits(10, 10, 1024, 1024, 1024 * 1024)); - capture.recordAccount(target.getMeta(), accountKey, - BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(canonicalAccount)); - capture.recordAssetPut(target.getMeta(), accountKey, assetKey, assetValue); - BlockChangeView view = viewFixture.capture(target.getMeta(), databases -> { - databases.get("account").put(accountKey, rawAccount); - databases.get("proposal").put(proposalKey, proposalValue); - }); - capture.attach(view); - batch = capture.seal(target); - } - - Arrays.fill(accountKey, (byte) 9); - Arrays.fill(rawAccount, (byte) 9); - Arrays.fill(canonicalAccount, (byte) 9); - Arrays.fill(assetKey, (byte) 9); - Arrays.fill(assetValue, (byte) 9); - Arrays.fill(proposalKey, (byte) 9); - Arrays.fill(proposalValue, (byte) 9); - - String firstParticipant = PARTICIPANTS.get(0); - String secondParticipant = PARTICIPANTS.get(1); - try (HistoryCommitStore history = new HistoryCommitStore( - archive, new HistoryCommitMarkerCodec())) { - ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, - checkpointPath, engines, readerPath, PARTICIPANTS, action -> action.run(), - (stage, participant) -> { - if (stage == Stage.AFTER_PARTICIPANT - && firstParticipant.equals(participant)) { - throw new IOException("injected after first participant"); - } - }, temporary -> { }); - assertThrows(IOException.class, () -> coordinator.apply(batch, () -> { })); - } - - assertEquals(2, counted.get(firstParticipant).getApplyCount()); - assertEquals(1, counted.get(secondParticipant).getApplyCount()); - AtomicInteger refreshes = new AtomicInteger(); - try (ArchiveParticipantRecoveryStorage recovery = - new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, engines, - readerPath, PARTICIPANTS, action -> action.run(), refreshes::incrementAndGet)) { - new ArchiveRecoveryExecutor(recovery).recover(); - } - - assertEquals(2, counted.get(firstParticipant).getApplyCount()); - for (String participant : PARTICIPANTS) { - assertEquals(2, counted.get(participant).getApplyCount()); - } - assertEquals(1, refreshes.get()); - assertArrayEquals(expectedCanonicalAccount, account.get(expectedAccountKey)); - assertArrayEquals(expectedAssetValue, accountAsset.get(expectedAssetKey)); - assertArrayEquals(expectedProposalValue, - memory.get("proposal").get(expectedProposalKey)); - - ArchiveProgressEnvelope checkpoint = - new ArchiveProgressFile(checkpointPath, progressCodec).load(); - ArchiveProgressEnvelope reader = - new ArchiveProgressFile(readerPath, progressCodec).load(); - byte[] planDigest = checkpoint.getMutationPlanDigest(); - assertArrayEquals(planDigest, account.loadProgress().getMutationPlanDigest()); - assertArrayEquals(planDigest, accountAsset.loadProgress().getMutationPlanDigest()); - assertArrayEquals(planDigest, reader.getMutationPlanDigest()); - assertEquals(target.getMeta().getEpoch(), reader.getEpoch()); - assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); - - try (ArchiveParticipantRecoveryStorage fixed = - new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, engines, - readerPath, PARTICIPANTS)) { - assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); - } - } finally { - accountAsset.close(); - account.close(); - } - } - - @Test - public void p66ActivationCaptureRecoversExactParticipantSetAfterNativeReopen() - throws Exception { - Path archive = temporaryFolder.newFolder("p66-activation-exact-capture").toPath(); - List markers = initializeHistory(archive, 1); - HistoryCommitMarker initial = markers.get(0); - HistoryCommitMarker target = markers.get(1); - Path checkpointPath = archive.resolve("progress/checkpoint.progress"); - Path readerPath = archive.resolve("progress/reader.progress"); - ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); - new ArchiveProgressFile(checkpointPath, progressCodec).store(global(Kind.APPLY_CHECKPOINT, - initial)); - new ArchiveProgressFile(readerPath, progressCodec).store(global(Kind.READER_VISIBLE, initial)); - - byte[] address = accountAddress(7); - String tokenId = "1000007"; - Account raw = Account.newBuilder().setAddress(ByteString.copyFrom(address)) - .putAsset("asset-name", 30L).putAssetV2(tokenId, 30L).build(); - P66AccountAssetCodec codec = new P66AccountAssetCodec(); - byte[] canonicalAccount = codec.canonicalizeAccount( - Phase.P66_ACTIVATION, address, raw.toByteArray()); - P66AccountAssetCodec.AssetRow direct = codec.encodeAssetRow( - Phase.P66_ACTIVATION, address, tokenId, 30L); - byte[] assetKey = direct.getPhysicalRawKey(); - byte[] assetValue = direct.getPostValue().getValue(); - byte[] proposalKey = bytes(2, 61); - byte[] proposalValue = bytes(3, 71); - - ArchiveParticipantMutationBatch batch; - try (ViewFixture viewFixture = new ViewFixture()) { - ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture( - target.getMeta(), Phase.P66_ACTIVATION, - new ArchiveBlockForwardMutationLimits(10, 10, 1024, 1024, 1024 * 1024)); - capture.recordAccount(target.getMeta(), address, - BlockChangeView.PostValue.present(raw.toByteArray()), - BlockChangeView.PostValue.present(canonicalAccount)); - capture.recordAssetPut(target.getMeta(), address, assetKey, assetValue); - BlockChangeView view = viewFixture.capture(target.getMeta(), databases -> { - databases.get("account").put(address, raw.toByteArray()); - databases.get("proposal").put(proposalKey, proposalValue); - }); - capture.attach(view); - batch = capture.seal(target); - } - assertEquals(Phase.P66_ACTIVATION, batch.getTargetPhase()); - assertEquals(P66AccountAssetCodec.FORMAT_ID, batch.getAccountAssetFormatId()); - assertEquals(PARTICIPANTS, batch.getParticipants()); - String firstParticipant = PARTICIPANTS.get(0); - - try (ParticipantFixture participants = new ParticipantFixture(archive, initial)) { - try (HistoryCommitStore history = new HistoryCommitStore( - archive, new HistoryCommitMarkerCodec())) { - ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, - checkpointPath, participants.engines, readerPath, PARTICIPANTS, - action -> action.run(), - (stage, participant) -> failAfter(stage, participant, firstParticipant), - temporary -> { }); - assertThrows(IOException.class, () -> coordinator.apply(batch, () -> { })); - } - - ArchiveTargetMutationPlan durablePlan = - new ArchiveTargetMutationPlanFile(checkpointPath).loadRequired(); - byte[] planDigest = durablePlan.digest(); - assertEquals(Phase.P66_ACTIVATION, durablePlan.getTargetPhase()); - assertEquals(target.getMeta().getEpoch(), - participants.engines.get(firstParticipant).loadProgress().getEpoch()); - assertNull(participants.account.get(address)); - assertNull(participants.accountAsset.get(assetKey)); - assertNull(participants.memory.get("proposal").get(proposalKey)); - - participants.reopenNativeParticipants(); - AtomicInteger refreshes = new AtomicInteger(); - RecoveryPlan recoveryPlan; - try (ArchiveParticipantRecoveryStorage recovery = - new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, - participants.engines, readerPath, PARTICIPANTS, action -> action.run(), - refreshes::incrementAndGet)) { - recoveryPlan = new ArchiveRecoveryExecutor(recovery).recover(); - } - - List replayed = new ArrayList<>(); - recoveryPlan.getActions().stream() - .filter(action -> action.getType() == ActionType.REPLAY_PARTICIPANT) - .forEach(action -> replayed.add(action.getParticipant())); - assertEquals(PARTICIPANTS.subList(1, PARTICIPANTS.size()), replayed); - assertEquals(ActionType.PUBLISH_READER_HEAD, - recoveryPlan.getActions().get(recoveryPlan.getActions().size() - 1).getType()); - assertEquals(1, refreshes.get()); - assertArrayEquals(canonicalAccount, participants.account.get(address)); - assertArrayEquals(assetValue, participants.accountAsset.get(assetKey)); - assertArrayEquals(proposalValue, - participants.memory.get("proposal").get(proposalKey)); - - ArchiveProgressEnvelope checkpoint = - new ArchiveProgressFile(checkpointPath, progressCodec).load(); - ArchiveProgressEnvelope reader = - new ArchiveProgressFile(readerPath, progressCodec).load(); - assertArrayEquals(planDigest, checkpoint.getMutationPlanDigest()); - assertArrayEquals(target.getMeta().getBlockHash(), checkpoint.getBlockHash()); - assertEquals(target.getMeta().getEpoch(), reader.getEpoch()); - assertArrayEquals(planDigest, reader.getMutationPlanDigest()); - for (String participant : PARTICIPANTS) { - ArchiveProgressEnvelope progress = participants.engines.get(participant).loadProgress(); - assertEquals(PARTICIPANTS, progress.getParticipants()); - assertEquals(target.getMeta().getEpoch(), progress.getEpoch()); - assertArrayEquals(target.getMeta().getBlockHash(), progress.getBlockHash()); - assertArrayEquals(planDigest, progress.getMutationPlanDigest()); - } - assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); - - participants.reopenNativeParticipants(); - try (ArchiveParticipantRecoveryStorage fixed = - new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, - participants.engines, readerPath, PARTICIPANTS)) { - assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); - } - assertArrayEquals(canonicalAccount, participants.account.get(address)); - assertArrayEquals(assetValue, participants.accountAsset.get(assetKey)); - assertArrayEquals(proposalValue, - participants.memory.get("proposal").get(proposalKey)); - } - } - - @Test - public void consecutiveCaptureTargetsReplaceDigestAndRecoverPutDelete() throws Exception { - Path archive = temporaryFolder.newFolder("consecutive-capture-recovery").toPath(); - List markers = initializeHistory(archive, 2); - HistoryCommitMarker initial = markers.get(0); - HistoryCommitMarker firstTarget = markers.get(1); - HistoryCommitMarker secondTarget = markers.get(2); - Path checkpointPath = archive.resolve("progress/checkpoint.progress"); - Path readerPath = archive.resolve("progress/reader.progress"); - ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); - new ArchiveProgressFile(checkpointPath, progressCodec).store(global(Kind.APPLY_CHECKPOINT, - initial)); - new ArchiveProgressFile(readerPath, progressCodec).store(global(Kind.READER_VISIBLE, initial)); - - byte[] accountKey = bytes(2, 1); - byte[] assetKey = append(accountKey, 4); - byte[] proposalKey = bytes(2, 6); - byte[] firstCanonical = bytes(3, 11); - byte[] firstAsset = bytes(3, 12); - byte[] firstProposal = bytes(3, 13); - byte[] secondCanonical = bytes(3, 21); - byte[] secondProposal = bytes(3, 23); - - try (ParticipantFixture participants = new ParticipantFixture(archive, initial)) { - ArchiveParticipantMutationBatch firstBatch = capture(firstTarget, accountKey, - bytes(3, 10), firstCanonical, assetKey, firstAsset, false, - proposalKey, firstProposal); - try (HistoryCommitStore history = new HistoryCommitStore( - archive, new HistoryCommitMarkerCodec())) { - new ArchiveTargetApplyCoordinator(history, checkpointPath, participants.engines, - readerPath, PARTICIPANTS, action -> action.run()).apply(firstBatch, () -> { }); - } - - ArchiveProgressEnvelope firstCheckpoint = - new ArchiveProgressFile(checkpointPath, progressCodec).load(); - byte[] firstDigest = firstCheckpoint.getMutationPlanDigest(); - assertArrayEquals(firstCanonical, participants.account.get(accountKey)); - assertArrayEquals(firstAsset, participants.accountAsset.get(assetKey)); - assertArrayEquals(firstProposal, - participants.memory.get("proposal").get(proposalKey)); - assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); - for (String participant : PARTICIPANTS) { - assertEquals(2, participants.counted.get(participant).getApplyCount()); - } - - ArchiveParticipantMutationBatch secondBatch = capture(secondTarget, accountKey, - bytes(3, 20), secondCanonical, assetKey, null, true, - proposalKey, secondProposal); - String failureParticipant = "account-asset"; - int failureIndex = PARTICIPANTS.indexOf(failureParticipant); - try (HistoryCommitStore history = new HistoryCommitStore( - archive, new HistoryCommitMarkerCodec())) { - ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, - checkpointPath, participants.engines, readerPath, PARTICIPANTS, - action -> action.run(), - (stage, participant) -> failAfter(stage, participant, failureParticipant), - temporary -> { }); - assertThrows(IOException.class, () -> coordinator.apply(secondBatch, () -> { })); - } - - for (int index = 0; index < PARTICIPANTS.size(); index++) { - int expected = index <= failureIndex ? 3 : 2; - assertEquals(expected, - participants.counted.get(PARTICIPANTS.get(index)).getApplyCount()); - } - AtomicInteger refreshes = new AtomicInteger(); - try (ArchiveParticipantRecoveryStorage recovery = - new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, - participants.engines, readerPath, PARTICIPANTS, action -> action.run(), - refreshes::incrementAndGet)) { - new ArchiveRecoveryExecutor(recovery).recover(); - } - - for (String participant : PARTICIPANTS) { - assertEquals(3, participants.counted.get(participant).getApplyCount()); - } - assertEquals(1, refreshes.get()); - assertArrayEquals(secondCanonical, participants.account.get(accountKey)); - assertNull(participants.accountAsset.get(assetKey)); - assertArrayEquals(secondProposal, - participants.memory.get("proposal").get(proposalKey)); - - ArchiveProgressEnvelope secondCheckpoint = - new ArchiveProgressFile(checkpointPath, progressCodec).load(); - ArchiveProgressEnvelope reader = - new ArchiveProgressFile(readerPath, progressCodec).load(); - byte[] secondDigest = secondCheckpoint.getMutationPlanDigest(); - assertFalse(Arrays.equals(firstDigest, secondDigest)); - assertArrayEquals(secondDigest, - participants.account.loadProgress().getMutationPlanDigest()); - assertArrayEquals(secondDigest, - participants.accountAsset.loadProgress().getMutationPlanDigest()); - assertArrayEquals(secondDigest, reader.getMutationPlanDigest()); - assertEquals(secondTarget.getMeta().getEpoch(), reader.getEpoch()); - assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); - - try (ArchiveParticipantRecoveryStorage fixed = - new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, - participants.engines, readerPath, PARTICIPANTS)) { - assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); - } - } - } - - @Test - public void emptyCaptureTargetAdvancesProgressWithoutChangingBusinessData() throws Exception { - Path archive = temporaryFolder.newFolder("empty-capture-recovery").toPath(); - List markers = initializeHistory(archive, 2); - HistoryCommitMarker initial = markers.get(0); - HistoryCommitMarker firstTarget = markers.get(1); - HistoryCommitMarker emptyTarget = markers.get(2); - Path checkpointPath = archive.resolve("progress/checkpoint.progress"); - Path readerPath = archive.resolve("progress/reader.progress"); - ArchiveProgressEnvelopeCodec progressCodec = new ArchiveProgressEnvelopeCodec(); - new ArchiveProgressFile(checkpointPath, progressCodec).store(global(Kind.APPLY_CHECKPOINT, - initial)); - new ArchiveProgressFile(readerPath, progressCodec).store(global(Kind.READER_VISIBLE, initial)); - - byte[] accountKey = bytes(2, 1); - byte[] assetKey = append(accountKey, 4); - byte[] proposalKey = bytes(2, 6); - byte[] accountValue = bytes(3, 11); - byte[] assetValue = bytes(3, 12); - byte[] proposalValue = bytes(3, 13); - - try (ParticipantFixture participants = new ParticipantFixture(archive, initial)) { - ArchiveParticipantMutationBatch firstBatch = capture(firstTarget, accountKey, - bytes(3, 10), accountValue, assetKey, assetValue, false, - proposalKey, proposalValue); - try (HistoryCommitStore history = new HistoryCommitStore( - archive, new HistoryCommitMarkerCodec())) { - new ArchiveTargetApplyCoordinator(history, checkpointPath, participants.engines, - readerPath, PARTICIPANTS, action -> action.run()).apply(firstBatch, () -> { }); - } - byte[] firstDigest = new ArchiveProgressFile(checkpointPath, progressCodec) - .load().getMutationPlanDigest(); - - ArchiveParticipantMutationBatch emptyBatch = captureEmpty(emptyTarget); - assertTrue(emptyBatch.getMutations().isEmpty()); - assertEquals(PARTICIPANTS, emptyBatch.getParticipants()); - try (HistoryCommitStore history = new HistoryCommitStore( - archive, new HistoryCommitMarkerCodec())) { - ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, - checkpointPath, participants.engines, readerPath, PARTICIPANTS, - action -> action.run(), - (stage, participant) -> failAfterEmptyCheckpoint(stage), temporary -> { }); - assertThrows(IOException.class, () -> coordinator.apply(emptyBatch, () -> { })); - } - - for (String participant : PARTICIPANTS) { - assertEquals(2, participants.counted.get(participant).getApplyCount()); - } - assertArrayEquals(accountValue, participants.account.get(accountKey)); - assertArrayEquals(assetValue, participants.accountAsset.get(assetKey)); - assertArrayEquals(proposalValue, - participants.memory.get("proposal").get(proposalKey)); - - AtomicInteger refreshes = new AtomicInteger(); - try (ArchiveParticipantRecoveryStorage recovery = - new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, - participants.engines, readerPath, PARTICIPANTS, action -> action.run(), - refreshes::incrementAndGet)) { - new ArchiveRecoveryExecutor(recovery).recover(); - } - - for (String participant : PARTICIPANTS) { - assertEquals(3, participants.counted.get(participant).getApplyCount()); - } - assertEquals(1, refreshes.get()); - assertArrayEquals(accountValue, participants.account.get(accountKey)); - assertArrayEquals(assetValue, participants.accountAsset.get(assetKey)); - assertArrayEquals(proposalValue, - participants.memory.get("proposal").get(proposalKey)); - - ArchiveProgressEnvelope checkpoint = - new ArchiveProgressFile(checkpointPath, progressCodec).load(); - ArchiveProgressEnvelope reader = - new ArchiveProgressFile(readerPath, progressCodec).load(); - byte[] emptyDigest = checkpoint.getMutationPlanDigest(); - assertFalse(Arrays.equals(firstDigest, emptyDigest)); - for (String participant : PARTICIPANTS) { - assertArrayEquals(emptyDigest, - participants.engines.get(participant).loadProgress().getMutationPlanDigest()); - } - assertArrayEquals(emptyDigest, reader.getMutationPlanDigest()); - assertEquals(emptyTarget.getMeta().getEpoch(), reader.getEpoch()); - assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); - - try (ArchiveParticipantRecoveryStorage fixed = - new ArchiveParticipantRecoveryStorage(archive, 4096, checkpointPath, - participants.engines, readerPath, PARTICIPANTS)) { - assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); - } - } - } - - private static ArchiveParticipantMutationBatch capture(HistoryCommitMarker target, - byte[] accountKey, byte[] rawAccount, byte[] canonicalAccount, byte[] assetKey, - byte[] assetValue, boolean deleteAsset, byte[] proposalKey, byte[] proposalValue) { - try (ViewFixture viewFixture = new ViewFixture()) { - ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture( - target.getMeta(), Phase.P66_ON, - new ArchiveBlockForwardMutationLimits(10, 10, 1024, 1024, 1024 * 1024)); - capture.recordAccount(target.getMeta(), accountKey, - BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(canonicalAccount)); - if (deleteAsset) { - capture.recordAssetDelete(target.getMeta(), accountKey, assetKey); - } else { - capture.recordAssetPut(target.getMeta(), accountKey, assetKey, assetValue); - } - BlockChangeView view = viewFixture.capture(target.getMeta(), databases -> { - databases.get("account").put(accountKey, rawAccount); - databases.get("proposal").put(proposalKey, proposalValue); - }); - capture.attach(view); - return capture.seal(target); - } - } - - private static ArchiveParticipantMutationBatch captureEmpty(HistoryCommitMarker target) { - try (ViewFixture viewFixture = new ViewFixture()) { - ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture( - target.getMeta(), Phase.P66_ON, - new ArchiveBlockForwardMutationLimits(0, 0, 0, 0, 0)); - BlockChangeView view = viewFixture.capture(target.getMeta(), databases -> { }); - capture.attach(view); - return capture.seal(target); - } - } - - private static void failAfter(Stage stage, String participant, String failureParticipant) - throws IOException { - if (stage == Stage.AFTER_PARTICIPANT && failureParticipant.equals(participant)) { - throw new IOException("injected during second target"); - } - } - - private static void failAfterEmptyCheckpoint(Stage stage) throws IOException { - if (stage == Stage.AFTER_CHECKPOINT) { - throw new IOException("injected after empty checkpoint"); - } - } - - private static List initializeHistory(Path archive, int lastEpoch) - throws Exception { - List markers = new ArrayList<>(); - try (HistorySegmentStore bodies = new HistorySegmentStore( - archive, new BlockHistoryCodec(), 4096); - HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); - HistoryCommitStore commits = new HistoryCommitStore( - archive, new HistoryCommitMarkerCodec())) { - for (int epoch = 0; epoch <= lastEpoch; epoch++) { - BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, hash(epoch), - hash(epoch - 1), epoch * 1_000L); - BlockReverseDiff diff = new BlockReverseDiff(meta, - Collections.singletonList(new BlockReverseDiff.DbGroup("account", - Collections.singletonList(new BlockReverseDiff.Entry(bytes(2, epoch + 10), - OldValue.absent()))))); - HistoryLocation body = bodies.append(diff); - HistoryIndexLocation location = index.append(HistoryIndexRecord.from(diff, body)); - markers.add(new HistoryCommitMarker(meta, epoch - 1L, body, location, - bytes(16, epoch + 40), PARTICIPANTS)); - } - bodies.sync(); - index.sync(); - commits.commitAll(markers); - ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), - commits.getRecordLength(), commits.head(), new HistoryCommitMarkerCodec()); - } - return markers; - } - - private static ArchiveProgressEnvelope participant(String participant, - HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, participant, - marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - } - - private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - } - - private static List participants() { - List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); - Collections.sort(participants); - return Collections.unmodifiableList(participants); - } - - private static byte[] hash(int suffix) { - byte[] hash = new byte[32]; - hash[31] = (byte) suffix; - return hash; - } - - private static byte[] accountAddress(int suffix) { - byte[] address = new byte[21]; - address[0] = 0x41; - address[20] = (byte) suffix; - return address; - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } - - private static byte[] append(byte[] prefix, int suffix) { - byte[] result = Arrays.copyOf(prefix, prefix.length + 1); - result[result.length - 1] = (byte) suffix; - return result; - } - - private static byte[] copy(byte[] value) { - return Arrays.copyOf(value, value.length); - } - - @FunctionalInterface - private interface Mutator { - void mutate(Map databases); - } - - private static final class ViewFixture implements AutoCloseable { - private final SnapshotManager manager = new SnapshotManager(""); - private final Map databases = new LinkedHashMap<>(); - private final List ordered = new ArrayList<>(); - - private ViewFixture() { - for (String participant : PARTICIPANTS) { - Chainbase database = new Chainbase(new SnapshotRoot(new ViewMemoryDb(participant))); - databases.put(participant, database); - ordered.add(database); - manager.add(database); - } - manager.enable(); - } - - private BlockChangeView capture(BlockSnapshotMeta meta, Mutator mutator) { - try (ISession session = manager.buildSession()) { - mutator.mutate(databases); - return BlockChangeView.capture(meta, ordered); - } - } - - @Override - public void close() { - manager.shutdown(); - } - } - - private static final class ParticipantFixture implements AutoCloseable { - private final Path archive; - private LevelDbArchiveParticipant account; - private RocksDbArchiveParticipant accountAsset; - private final Map counted = new LinkedHashMap<>(); - private final Map memory = new LinkedHashMap<>(); - private final Map engines = new LinkedHashMap<>(); - - private ParticipantFixture(Path archive, HistoryCommitMarker initial) throws IOException { - this.archive = archive; - openNativeParticipants(); - for (String participant : PARTICIPANTS) { - ArchiveParticipant delegate; - if ("account".equals(participant)) { - delegate = account; - } else if ("account-asset".equals(participant)) { - delegate = accountAsset; - } else { - MemoryParticipant inMemory = new MemoryParticipant(); - memory.put(participant, inMemory); - delegate = inMemory; - } - CountingParticipant engine = new CountingParticipant(delegate); - engine.apply(Collections.emptyList(), participant(participant, initial)); - counted.put(participant, engine); - engines.put(participant, engine); - } - } - - private void reopenNativeParticipants() throws IOException { - closeNativeParticipants(); - openNativeParticipants(); - replaceNativeParticipant("account", account); - replaceNativeParticipant("account-asset", accountAsset); - } - - private void openNativeParticipants() throws IOException { - account = new LevelDbArchiveParticipant( - archive.resolve("participants/account"), "account", PARTICIPANTS); - try { - accountAsset = new RocksDbArchiveParticipant( - archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS); - } catch (IOException | RuntimeException failure) { - account.close(); - throw failure; - } - } - - private void replaceNativeParticipant(String participant, ArchiveParticipant delegate) { - CountingParticipant engine = new CountingParticipant(delegate); - counted.put(participant, engine); - engines.put(participant, engine); - } - - private void closeNativeParticipants() throws IOException { - accountAsset.close(); - account.close(); - } - - @Override - public void close() throws IOException { - closeNativeParticipants(); - } - } - - private static final class CountingParticipant implements ArchiveParticipant { - private final ArchiveParticipant delegate; - private int applyCount; - - private CountingParticipant(ArchiveParticipant delegate) { - this.delegate = delegate; - } - - @Override - public void apply(List mutations, - ArchiveProgressEnvelope progress) throws IOException { - delegate.apply(mutations, progress); - applyCount++; - } - - @Override - public ArchiveProgressEnvelope loadProgress() throws IOException { - return delegate.loadProgress(); - } - - private int getApplyCount() { - return applyCount; - } - } - - private static final class MemoryParticipant implements ArchiveParticipant { - private final Map values = new LinkedHashMap<>(); - private ArchiveProgressEnvelope progress; - - @Override - public void apply(List mutations, - ArchiveProgressEnvelope progress) { - for (ArchiveParticipantMutation mutation : mutations) { - byte[] value = mutation.getValue(); - WrappedByteArray key = WrappedByteArray.copyOf(mutation.getKey()); - if (value == null) { - values.remove(key); - } else { - values.put(key, copy(value)); - } - } - this.progress = progress; - } - - @Override - public ArchiveProgressEnvelope loadProgress() { - return progress; - } - - private byte[] get(byte[] key) { - byte[] value = values.get(WrappedByteArray.of(key)); - return value == null ? null : copy(value); - } - } - - private static final class ViewMemoryDb implements DB, Flusher { - private final String name; - private final Map values = new LinkedHashMap<>(); - - private ViewMemoryDb(String name) { - this.name = name; - } - - @Override - public byte[] get(byte[] key) { - byte[] value = values.get(WrappedByteArray.of(key)); - return value == null ? null : copy(value); - } - - @Override - public void put(byte[] key, byte[] value) { - values.put(WrappedByteArray.copyOf(key), copy(value)); - } - - @Override - public long size() { - return values.size(); - } - - @Override - public boolean isEmpty() { - return values.isEmpty(); - } - - @Override - public void remove(byte[] key) { - values.remove(WrappedByteArray.of(key)); - } - - @Override - public Iterator> iterator() { - List> entries = new ArrayList<>(); - values.forEach((key, value) -> entries.add(new AbstractMap.SimpleImmutableEntry<>( - key.getBytes(), copy(value)))); - return entries.iterator(); - } - - @Override - public void close() { - values.clear(); - } - - @Override - public void flush(Map batch) { - batch.forEach((key, value) -> { - if (value == null || value.getBytes() == null) { - values.remove(key); - } else { - values.put(WrappedByteArray.copyOf(key.getBytes()), value.getBytes()); - } - }); - } - - @Override - public void reset() { - values.clear(); - } - - @Override - public String getDbName() { - return name; - } - - @Override - public void stat() { - } - - @Override - public DB newInstance() { - return new ViewMemoryDb(name); - } - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveMixedEngineProgressSourceTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveMixedEngineProgressSourceTest.java deleted file mode 100644 index a5843628ccd..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveMixedEngineProgressSourceTest.java +++ /dev/null @@ -1,195 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; - -public class ArchiveMixedEngineProgressSourceTest { - - private static final List PARTICIPANTS = - Arrays.asList("account", "account-asset"); - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void mixedEnginesDriveFreshRecoveryAndReaderPublication() throws Exception { - try (Fixture fixture = fixture()) { - fixture.apply(fixture.marker(1)); - RecoverySnapshot snapshot = fixture.scanner(fixture.sources()).scan(); - assertEquals(1, snapshot.getHistoryHead()); - assertEquals(1, snapshot.getCheckpointHead()); - assertEquals(Long.valueOf(1), snapshot.getParticipantHeads().get("account")); - assertEquals(Long.valueOf(1), snapshot.getParticipantHeads().get("account-asset")); - assertEquals(0, snapshot.getReaderVisibleHead()); - - fixture.gate(fixture.sources()).publish(1); - assertEquals(1, fixture.reader().getEpoch()); - } - } - - @Test - public void sourceSetMismatchFailsBeforeReadingEitherEngine() throws Exception { - try (Fixture fixture = fixture()) { - AtomicInteger reads = new AtomicInteger(); - Map missing = new LinkedHashMap<>(); - missing.put("account", () -> { - reads.incrementAndGet(); - return fixture.progress("account", fixture.marker(1)); - }); - - assertThrows(IllegalArgumentException.class, () -> fixture.scanner(missing)); - assertThrows(IllegalArgumentException.class, () -> fixture.gate(missing)); - assertEquals(0, reads.get()); - assertEquals(0, fixture.reader().getEpoch()); - } - } - - @Test - public void identityAndPartialReadFailureNeverAdvanceReader() throws Exception { - try (Fixture fixture = fixture()) { - HistoryCommitMarker target = fixture.marker(1); - ArchiveProgressEnvelope wrongHash = new ArchiveProgressEnvelope( - Kind.PARTICIPANT_PROGRESS, "account", 1, bytes(32, 99), target.getBatchId(), - target.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - fixture.level.apply(Collections.emptyList(), wrongHash); - fixture.rocks.apply(Collections.emptyList(), fixture.progress("account-asset", target)); - assertThrows(ArchivePersistenceException.class, - () -> fixture.scanner(fixture.sources()).scan()); - assertThrows(ArchivePersistenceException.class, - () -> fixture.gate(fixture.sources()).publish(1)); - assertEquals(0, fixture.reader().getEpoch()); - - fixture.apply(target); - Map partial = fixture.sources(); - partial.put("account-asset", () -> { - throw new IOException("injected mixed-engine progress read failure"); - }); - assertThrows(IOException.class, () -> fixture.scanner(partial).scan()); - assertThrows(IOException.class, () -> fixture.gate(partial).publish(1)); - assertEquals(0, fixture.reader().getEpoch()); - } - } - - private Fixture fixture() throws Exception { - return new Fixture(temporaryFolder.newFolder().toPath()); - } - - private static final class Fixture implements AutoCloseable { - private final HistoryCommitStore history; - private final Path checkpointPath; - private final Path readerPath; - private final LevelDbArchiveParticipant level; - private final RocksDbArchiveParticipant rocks; - private final ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - - private Fixture(Path directory) throws Exception { - history = new HistoryCommitStore(directory, new HistoryCommitMarkerCodec()); - history.commitAll(Arrays.asList(marker(0), marker(1))); - checkpointPath = directory.resolve("progress/checkpoint.progress"); - readerPath = directory.resolve("progress/reader.progress"); - new ArchiveProgressFile(checkpointPath, codec).store( - progress(Kind.APPLY_CHECKPOINT, null, marker(1))); - new ArchiveProgressFile(readerPath, codec).store( - progress(Kind.READER_VISIBLE, null, marker(0))); - level = new LevelDbArchiveParticipant( - directory.resolve("account-level"), "account", PARTICIPANTS); - rocks = new RocksDbArchiveParticipant( - directory.resolve("asset-rocks"), "account-asset", PARTICIPANTS); - } - - private void apply(HistoryCommitMarker marker) throws IOException { - level.apply(Collections.emptyList(), progress("account", marker)); - rocks.apply(Collections.emptyList(), progress("account-asset", marker)); - } - - private Map sources() { - Map sources = new LinkedHashMap<>(); - sources.put("account", level); - sources.put("account-asset", rocks); - return sources; - } - - private ArchiveRecoveryAuthorityScanner scanner( - Map sources) { - return ArchiveRecoveryAuthorityScanner.forParticipants(history, checkpointPath, sources, - readerPath, PARTICIPANTS); - } - - private ArchiveReaderPublicationGate gate( - Map sources) { - return new ArchiveReaderPublicationGate(history, - () -> new ArchiveProgressFile(checkpointPath, codec).load(), sources, - readerPath, PARTICIPANTS, action -> action.run()); - } - - private ArchiveProgressEnvelope reader() throws IOException { - return new ArchiveProgressFile(readerPath, codec).load(); - } - - private ArchiveProgressEnvelope progress(String participant, HistoryCommitMarker marker) { - return progress(Kind.PARTICIPANT_PROGRESS, participant, marker); - } - - private ArchiveProgressEnvelope progress(Kind kind, String participant, - HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - } - - private HistoryCommitMarker marker(long epoch) { - BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, bytes(32, (int) epoch), - bytes(32, (int) epoch - 1), epoch * 1_000); - HistoryLocation body = new HistoryLocation(0, epoch * 100, 100, (int) epoch, - bytes(32, (int) epoch + 20)); - HistoryIndexLocation index = new HistoryIndexLocation(epoch * 50, 50, - bytes(32, (int) epoch + 30)); - return new HistoryCommitMarker(meta, epoch - 1, body, index, - bytes(16, (int) epoch + 40), new ArrayList<>(PARTICIPANTS)); - } - - @Override - public void close() throws IOException { - IOException failure = null; - try { - rocks.close(); - } catch (RuntimeException closeFailure) { - failure = new IOException("Failed to close RocksDB participant", closeFailure); - } - try { - level.close(); - } catch (IOException closeFailure) { - if (failure == null) { - failure = closeFailure; - } else { - failure.addSuppressed(closeFailure); - } - } - history.close(); - if (failure != null) { - throw failure; - } - } - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantBatchFileTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantBatchFileTest.java deleted file mode 100644 index 00e5652e3ba..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantBatchFileTest.java +++ /dev/null @@ -1,225 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.ArchiveParticipantBatchFile.Snapshot; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoveryStorage; - -public class ArchiveParticipantBatchFileTest { - - private static final List PARTICIPANTS = Arrays.asList("account", "account-asset"); - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void crashKeepsBusinessPayloadAndProgressOnTheSameOldVersion() throws Exception { - Path path = temporaryFolder.newFolder().toPath().resolve("account-asset.batch"); - ArchiveParticipantBatchFile normal = new ArchiveParticipantBatchFile(path, - "account-asset", PARTICIPANTS); - normal.store(bytes(12, 8), envelope("account-asset", marker(8))); - - ArchiveParticipantBatchFile failing = new ArchiveParticipantBatchFile(path, - "account-asset", PARTICIPANTS, temporary -> { - throw new IOException("injected after participant temporary force"); - }); - assertThrows(IOException.class, - () -> failing.store(bytes(12, 10), envelope("account-asset", marker(10)))); - Snapshot old = normal.load(); - assertArrayEquals(bytes(12, 8), old.getBusinessPayload()); - assertEquals(8, old.getProgress().getEpoch()); - - normal.store(bytes(12, 10), envelope("account-asset", marker(10))); - Snapshot current = normal.load(); - assertArrayEquals(bytes(12, 10), current.getBusinessPayload()); - assertEquals(10, current.getProgress().getEpoch()); - - byte[] corrupt = Files.readAllBytes(path); - corrupt[corrupt.length - 1] ^= 1; - Files.write(path, corrupt); - assertThrows(ArchivePersistenceException.class, normal::load); - } - - @Test - public void replayCrashLeavesOldBatchAndSecondRestartReplaysOnce() throws Exception { - try (Fixture fixture = fixture()) { - ArchiveParticipantBatchFile failingAsset = new ArchiveParticipantBatchFile( - fixture.assetPath, "account-asset", PARTICIPANTS, temporary -> { - throw new IOException("injected replay batch crash"); - }); - DurableBatchStorage first = new DurableBatchStorage(fixture, failingAsset); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveRecoveryExecutor(first).recover()); - assertEquals(Arrays.asList("truncate:10"), first.actions); - Snapshot afterCrash = fixture.batches.get("account-asset").load(); - assertArrayEquals(bytes(12, 8), afterCrash.getBusinessPayload()); - assertEquals(8, afterCrash.getProgress().getEpoch()); - - RecoverySnapshot restart = fixture.scanner.scan(); - assertEquals(10, restart.getHistoryHead()); - assertEquals(Long.valueOf(8), restart.getParticipantHeads().get("account-asset")); - assertEquals(8, restart.getReaderVisibleHead()); - - DurableBatchStorage second = new DurableBatchStorage(fixture, - fixture.batches.get("account-asset")); - new ArchiveRecoveryExecutor(second).recover(); - assertEquals(Arrays.asList("replay:account-asset:9-10", "publish:10"), second.actions); - Snapshot recovered = fixture.batches.get("account-asset").load(); - assertArrayEquals(bytes(12, 10), recovered.getBusinessPayload()); - assertEquals(10, recovered.getProgress().getEpoch()); - assertEquals(10, fixture.scanner.scan().getReaderVisibleHead()); - - DurableBatchStorage third = new DurableBatchStorage(fixture, - fixture.batches.get("account-asset")); - assertEquals(0, new ArchiveRecoveryExecutor(third).recover().getActions().size()); - assertEquals(0, third.actions.size()); - } - } - - private Fixture fixture() throws Exception { - Path directory = temporaryFolder.newFolder().toPath(); - HistoryCommitStore history = new HistoryCommitStore(directory, - new HistoryCommitMarkerCodec()); - List markers = new ArrayList<>(); - for (long epoch = 8; epoch <= 12; epoch++) { - markers.add(marker(epoch)); - } - history.commitAll(markers); - - ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - Path checkpointPath = directory.resolve("progress/checkpoint.progress"); - Path readerPath = directory.resolve("progress/reader.progress"); - new ArchiveProgressFile(checkpointPath, codec).store(globalEnvelope( - Kind.APPLY_CHECKPOINT, history.get(10))); - new ArchiveProgressFile(readerPath, codec).store(globalEnvelope( - Kind.READER_VISIBLE, history.get(8))); - - Path accountPath = directory.resolve("participants/account.batch"); - Path assetPath = directory.resolve("participants/account-asset.batch"); - Map batches = new LinkedHashMap<>(); - batches.put("account", new ArchiveParticipantBatchFile(accountPath, - "account", PARTICIPANTS)); - batches.put("account-asset", new ArchiveParticipantBatchFile(assetPath, - "account-asset", PARTICIPANTS)); - batches.get("account").store(bytes(12, 10), envelope("account", history.get(10))); - batches.get("account-asset").store(bytes(12, 8), - envelope("account-asset", history.get(8))); - - ArchiveRecoveryAuthorityScanner scanner = - ArchiveRecoveryAuthorityScanner.forParticipantBatches(history, checkpointPath, - batches, readerPath, PARTICIPANTS); - return new Fixture(history, scanner, batches, assetPath, readerPath); - } - - private static ArchiveProgressEnvelope envelope(String participant, - HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, participant, - marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); - } - - private static ArchiveProgressEnvelope globalEnvelope(Kind kind, - HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); - } - - private static HistoryCommitMarker marker(long epoch) { - BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, bytes(32, (int) epoch), - bytes(32, (int) epoch - 1), epoch * 1_000); - HistoryLocation body = new HistoryLocation(0, epoch * 100, 100, (int) epoch, - bytes(32, (int) epoch + 20)); - HistoryIndexLocation index = new HistoryIndexLocation(epoch * 50, 50, - bytes(32, (int) epoch + 30)); - return new HistoryCommitMarker(meta, epoch - 1, body, index, - bytes(16, (int) epoch + 40), PARTICIPANTS); - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } - - private static final class DurableBatchStorage implements RecoveryStorage { - private final Fixture fixture; - private final ArchiveParticipantBatchFile assetReplayBatch; - private final List actions = new ArrayList<>(); - - private DurableBatchStorage(Fixture fixture, - ArchiveParticipantBatchFile assetReplayBatch) { - this.fixture = fixture; - this.assetReplayBatch = assetReplayBatch; - } - - @Override - public RecoverySnapshot scan() throws IOException { - return fixture.scanner.scan(); - } - - @Override - public void truncateHistoryAndSync(long historyHead) throws IOException { - HistoryCommitMarker head = fixture.history.head(); - while (head != null && head.getMeta().getEpoch() > historyHead) { - fixture.history.removeHead(head.getMeta()); - head = fixture.history.head(); - } - actions.add("truncate:" + historyHead); - } - - @Override - public void replayParticipantAndSyncProgress(String participant, long firstEpoch, - long lastEpoch) throws IOException { - ArchiveParticipantBatchFile batch = "account-asset".equals(participant) - ? assetReplayBatch : fixture.batches.get(participant); - batch.store(bytes(12, (int) lastEpoch), - envelope(participant, fixture.history.get(lastEpoch))); - actions.add("replay:" + participant + ":" + firstEpoch + "-" + lastEpoch); - } - - @Override - public void publishReaderHeadAndSync(long readerVisibleHead) throws IOException { - new ArchiveReaderHeadPublisher(fixture.history, fixture.readerPath, PARTICIPANTS) - .publish(readerVisibleHead); - actions.add("publish:" + readerVisibleHead); - } - } - - private static final class Fixture implements AutoCloseable { - private final HistoryCommitStore history; - private final ArchiveRecoveryAuthorityScanner scanner; - private final Map batches; - private final Path assetPath; - private final Path readerPath; - - private Fixture(HistoryCommitStore history, ArchiveRecoveryAuthorityScanner scanner, - Map batches, Path assetPath, Path readerPath) { - this.history = history; - this.scanner = scanner; - this.batches = batches; - this.assetPath = assetPath; - this.readerPath = readerPath; - } - - @Override - public void close() throws IOException { - history.close(); - } - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java deleted file mode 100644 index 98e9268c930..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantMutationBatchCollectorTest.java +++ /dev/null @@ -1,875 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.Test; -import org.tron.common.BaseMethodTest; -import org.tron.core.db2.ISession; -import org.tron.core.db2.archive.AccountAssetForwardMutationManifest.Entry; -import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; -import org.tron.core.db2.archive.AccountAssetForwardProjector.Projection; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; -import org.tron.core.db2.common.DB; -import org.tron.core.db2.common.Flusher; -import org.tron.core.db2.common.WrappedByteArray; -import org.tron.core.db2.core.Chainbase; -import org.tron.core.db2.core.SnapshotManager; -import org.tron.core.db2.core.SnapshotRoot; - -public class ArchiveParticipantMutationBatchCollectorTest extends BaseMethodTest { - - @Test - public void collectsExactPostPutDeleteAndEmptyDeterministically() { - BlockSnapshotMeta meta = meta(1); - HistoryCommitMarker marker = marker(meta, participants()); - byte[] deleted = bytes(2, 2); - try (Fixture first = new Fixture(participants()); - Fixture second = new Fixture(participants())) { - first.rootPut("storage-row", deleted, bytes(1, 8)); - second.rootPut("storage-row", deleted, bytes(1, 8)); - BlockChangeView firstView = first.capture(meta, databases -> { - databases.get("proposal").put(bytes(2, 3), bytes(1, 3)); - databases.get("code").put(bytes(2, 1), new byte[0]); - databases.get("storage-row").delete(deleted); - }); - BlockChangeView secondView = second.capture(meta, databases -> { - databases.get("storage-row").delete(deleted); - databases.get("code").put(bytes(2, 1), new byte[0]); - databases.get("proposal").put(bytes(2, 3), bytes(1, 3)); - }); - ArchiveParticipantMutationBatchCollector collector = - new ArchiveParticipantMutationBatchCollector(Phase.P66_ON); - ArchiveTargetMutationPlan firstPlan = new ArchiveTargetMutationPlanBuilder().build(marker, - collector.collect(marker, firstView)); - ArchiveTargetMutationPlan secondPlan = new ArchiveTargetMutationPlanBuilder().build(marker, - collector.collect(marker, secondView)); - - assertArrayEquals(new byte[0], firstPlan.getMutations("code").get(0).getValue()); - assertNull(firstPlan.getMutations("storage-row").get(0).getValue()); - assertArrayEquals(bytes(1, 3), - firstPlan.getMutations("proposal").get(0).getValue()); - assertArrayEquals(firstPlan.digest(), secondPlan.digest()); - } - } - - @Test - public void accountMutationRequiresExplicitNoScanForwardProjection() { - BlockSnapshotMeta meta = meta(1); - HistoryCommitMarker marker = marker(meta, participants()); - byte[] accountKey = bytes(3, 1); - byte[] rawAccount = bytes(3, 2); - byte[] canonicalAccount = bytes(3, 3); - byte[] assetPut = bytes(3, 4); - byte[] assetDelete = bytes(3, 5); - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").put(accountKey, rawAccount)); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector(Phase.P66_ON).collect(marker, view)); - AccountAssetForwardProjector projector = (key, post) -> { - assertArrayEquals(accountKey, key); - assertArrayEquals(rawAccount, post.getValue()); - return new Projection(BlockChangeView.PostValue.present(canonicalAccount), Arrays.asList( - new AssetMutation(assetDelete, BlockChangeView.PostValue.absent()), - new AssetMutation(assetPut, BlockChangeView.PostValue.present(new byte[0])))); - }; - ArchiveParticipantMutationBatch batch = - new ArchiveParticipantMutationBatchCollector(Phase.P66_ON, projector) - .collect(marker, view); - ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, batch); - - assertArrayEquals(canonicalAccount, - plan.getMutations("account").get(0).getValue()); - assertArrayEquals(assetPut, - plan.getMutations("account-asset").get(0).getKey()); - assertEquals(0, plan.getMutations("account-asset").get(0).getValue().length); - assertArrayEquals(assetDelete, - plan.getMutations("account-asset").get(1).getKey()); - assertNull(plan.getMutations("account-asset").get(1).getValue()); - } - } - - @Test - public void rejectsViewIdentityCoverageAndMissingProjectionResult() { - BlockSnapshotMeta meta = meta(1); - HistoryCommitMarker marker = marker(meta, participants()); - try (Fixture exact = new Fixture(participants())) { - BlockChangeView view = exact.capture(meta, - databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector(Phase.P66_ON).collect( - marker(meta(2), participants()), view)); - } - - try (Fixture incomplete = new Fixture(Collections.singletonList("code"))) { - BlockChangeView view = incomplete.capture(meta, - databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector(Phase.P66_ON).collect(marker, view)); - } - - try (Fixture account = new Fixture(participants())) { - BlockChangeView view = account.capture(meta, - databases -> databases.get("account").put(bytes(1, 1), bytes(1, 2))); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector(Phase.P66_ON, (key, post) -> null) - .collect(marker, view)); - } - } - - @Test - public void manifestCollectsAccountCreateUpdateDeleteAndExactAssetStates() { - BlockSnapshotMeta meta = meta(3); - HistoryCommitMarker marker = marker(meta, participants()); - byte[] createKey = bytes(2, 1); - byte[] updateKey = bytes(2, 2); - byte[] deleteKey = bytes(2, 3); - byte[] rawCreate = bytes(3, 11); - byte[] rawUpdate = bytes(3, 12); - byte[] canonicalCreate = bytes(3, 21); - byte[] canonicalUpdate = bytes(3, 22); - byte[] createAsset = assetKey(createKey, 1); - byte[] updateAsset = assetKey(updateKey, 1); - byte[] updateDeletedAsset = assetKey(updateKey, 2); - byte[] deleteAsset = assetKey(deleteKey, 1); - - try (Fixture fixture = new Fixture(participants())) { - fixture.rootPut("account", updateKey, bytes(3, 31)); - fixture.rootPut("account", deleteKey, bytes(3, 32)); - BlockChangeView view = fixture.capture(meta, databases -> { - databases.get("account").put(createKey, rawCreate); - databases.get("account").put(updateKey, rawUpdate); - databases.get("account").delete(deleteKey); - }); - AccountAssetForwardMutationManifest manifest = - new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, Arrays.asList( - entry(createKey, rawCreate, canonicalCreate, - new AssetMutation(createAsset, - BlockChangeView.PostValue.present(new byte[0]))), - entry(updateKey, rawUpdate, canonicalUpdate, - new AssetMutation(updateAsset, - BlockChangeView.PostValue.present(bytes(2, 41))), - new AssetMutation(updateDeletedAsset, BlockChangeView.PostValue.absent())), - new Entry(deleteKey, BlockChangeView.PostValue.absent(), - BlockChangeView.PostValue.absent(), Collections.singletonList( - new AssetMutation(deleteAsset, BlockChangeView.PostValue.absent()))))); - - ArchiveParticipantMutationBatch batch = - new ArchiveParticipantMutationBatchCollector(manifest).collect(marker, view); - ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, batch); - - assertArrayEquals(canonicalCreate, plan.getMutations("account").get(0).getValue()); - assertArrayEquals(canonicalUpdate, plan.getMutations("account").get(1).getValue()); - assertNull(plan.getMutations("account").get(2).getValue()); - assertEquals(0, plan.getMutations("account-asset").get(0).getValue().length); - assertArrayEquals(bytes(2, 41), - plan.getMutations("account-asset").get(1).getValue()); - assertNull(plan.getMutations("account-asset").get(2).getValue()); - assertNull(plan.getMutations("account-asset").get(3).getValue()); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector(manifest).collect(marker, view)); - } - } - - @Test - public void manifestRejectsMissingExtraTargetAndRawValueMismatch() { - BlockSnapshotMeta meta = meta(4); - HistoryCommitMarker marker = marker(meta, participants()); - byte[] accountKey = bytes(2, 1); - byte[] rawAccount = bytes(3, 2); - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").put(accountKey, rawAccount)); - AccountAssetForwardMutationManifest missing = - new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, Collections.emptyList()); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector(missing).collect(marker, view)); - - AccountAssetForwardMutationManifest extra = - new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, - Arrays.asList(entry(accountKey, rawAccount, rawAccount), - entry(bytes(2, 9), bytes(3, 9), bytes(3, 9)))); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector(extra).collect(marker, view)); - - AccountAssetForwardMutationManifest wrongRaw = - new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, - Collections.singletonList(entry(accountKey, bytes(3, 8), rawAccount))); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector(wrongRaw).collect(marker, view)); - } - - BlockSnapshotMeta otherMeta = meta(5); - HistoryCommitMarker otherMarker = marker(otherMeta, participants()); - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView otherView = fixture.capture(otherMeta, - databases -> databases.get("account").put(accountKey, rawAccount)); - AccountAssetForwardMutationManifest wrongTarget = - new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, - Collections.singletonList(entry(accountKey, rawAccount, rawAccount))); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveParticipantMutationBatchCollector(wrongTarget) - .collect(otherMarker, otherView)); - } - } - - @Test - public void manifestRejectsDuplicateCrossAccountAndUnusedEntries() { - HistoryCommitMarker marker = marker(meta(6), participants()); - byte[] accountKey = bytes(2, 1); - byte[] rawAccount = bytes(3, 2); - Entry entry = entry(accountKey, rawAccount, rawAccount); - assertThrows(IllegalArgumentException.class, - () -> new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, - Arrays.asList(entry, entry))); - assertThrows(IllegalArgumentException.class, - () -> new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, - Collections.singletonList(null))); - assertThrows(IllegalArgumentException.class, - () -> entry(accountKey, rawAccount, rawAccount, - new AssetMutation(assetKey(accountKey, 1), BlockChangeView.PostValue.absent()), - new AssetMutation(assetKey(accountKey, 1), BlockChangeView.PostValue.absent()))); - assertThrows(IllegalArgumentException.class, - () -> entry(accountKey, rawAccount, rawAccount, - new AssetMutation(bytes(3, 7), BlockChangeView.PostValue.absent()))); - - AccountAssetForwardMutationManifest singleUse = - new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, - Collections.singletonList(entry)); - singleUse.begin(marker, Collections.singletonList(accountKey)); - singleUse.project(accountKey, BlockChangeView.PostValue.present(rawAccount)); - assertThrows(ArchivePersistenceException.class, - () -> singleUse.project(accountKey, BlockChangeView.PostValue.present(rawAccount))); - singleUse.complete(); - - AccountAssetForwardMutationManifest unused = - new AccountAssetForwardMutationManifest(marker, Phase.P66_ON, - Collections.singletonList(entry)); - unused.begin(marker, Collections.singletonList(accountKey)); - assertThrows(ArchivePersistenceException.class, unused::complete); - } - - @Test - public void recorderSealsUnorderedEventsIntoExactAccountAndAssetMutations() { - BlockSnapshotMeta meta = meta(7); - byte[] updateKey = bytes(2, 1); - byte[] deleteKey = bytes(2, 2); - byte[] rawUpdate = bytes(3, 3); - byte[] canonicalUpdate = bytes(3, 4); - byte[] emptyAsset = assetKey(updateKey, 1); - byte[] deletedAsset = assetKey(updateKey, 2); - byte[] removedAccountAsset = assetKey(deleteKey, 1); - AccountAssetForwardMutationRecorder recorder = - new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); - - recorder.recordAssetDelete(meta, deleteKey, removedAccountAsset); - recorder.recordAssetPut(meta, updateKey, emptyAsset, new byte[0]); - recorder.recordAccount(meta, deleteKey, BlockChangeView.PostValue.absent(), - BlockChangeView.PostValue.absent()); - recorder.recordAssetDelete(meta, updateKey, deletedAsset); - recorder.recordAccount(meta, updateKey, BlockChangeView.PostValue.present(rawUpdate), - BlockChangeView.PostValue.present(canonicalUpdate)); - HistoryCommitMarker marker = marker(meta, participants()); - AccountAssetForwardMutationManifest manifest = recorder.seal(marker); - - try (Fixture fixture = new Fixture(participants())) { - fixture.rootPut("account", updateKey, bytes(3, 8)); - fixture.rootPut("account", deleteKey, bytes(3, 9)); - BlockChangeView view = fixture.capture(meta, databases -> { - databases.get("account").put(updateKey, rawUpdate); - databases.get("account").delete(deleteKey); - }); - ArchiveParticipantMutationBatch batch = - new ArchiveParticipantMutationBatchCollector(manifest).collect(marker, view); - ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, batch); - - assertArrayEquals(canonicalUpdate, plan.getMutations("account").get(0).getValue()); - assertNull(plan.getMutations("account").get(1).getValue()); - assertEquals(0, plan.getMutations("account-asset").get(0).getValue().length); - assertNull(plan.getMutations("account-asset").get(1).getValue()); - assertNull(plan.getMutations("account-asset").get(2).getValue()); - } - } - - @Test - public void recorderCanonicalizesDifferentEventOrders() { - BlockSnapshotMeta meta = meta(8); - HistoryCommitMarker marker = marker(meta, participants()); - byte[] accountKey = bytes(2, 1); - byte[] rawAccount = bytes(3, 2); - byte[] canonicalAccount = bytes(3, 3); - byte[] firstAsset = assetKey(accountKey, 1); - byte[] secondAsset = assetKey(accountKey, 2); - AccountAssetForwardMutationRecorder first = - new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); - AccountAssetForwardMutationRecorder second = - new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); - - first.recordAssetDelete(meta, accountKey, secondAsset); - first.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(canonicalAccount)); - first.recordAssetPut(meta, accountKey, firstAsset, bytes(2, 4)); - second.recordAssetPut(meta, accountKey, firstAsset, bytes(2, 4)); - second.recordAssetDelete(meta, accountKey, secondAsset); - second.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(canonicalAccount)); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").put(accountKey, rawAccount)); - ArchiveTargetMutationPlan firstPlan = new ArchiveTargetMutationPlanBuilder().build(marker, - new ArchiveParticipantMutationBatchCollector(first.seal(marker)).collect(marker, view)); - ArchiveTargetMutationPlan secondPlan = new ArchiveTargetMutationPlanBuilder().build(marker, - new ArchiveParticipantMutationBatchCollector(second.seal(marker)).collect(marker, view)); - assertArrayEquals(firstPlan.digest(), secondPlan.digest()); - } - } - - @Test - public void recorderRejectsTargetDuplicatesIncompleteAndPostSealWrites() { - BlockSnapshotMeta meta = meta(9); - BlockSnapshotMeta otherMeta = meta(10); - HistoryCommitMarker marker = marker(meta, participants()); - HistoryCommitMarker otherMarker = marker(otherMeta, participants()); - byte[] accountKey = bytes(2, 1); - byte[] rawAccount = bytes(3, 2); - byte[] assetKey = assetKey(accountKey, 1); - - AccountAssetForwardMutationRecorder wrongTarget = - new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); - assertThrows(ArchivePersistenceException.class, - () -> wrongTarget.recordAccount(otherMeta, accountKey, - BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(rawAccount))); - assertThrows(ArchivePersistenceException.class, () -> wrongTarget.seal(otherMarker)); - - AccountAssetForwardMutationRecorder duplicates = - new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); - duplicates.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(rawAccount)); - assertThrows(ArchivePersistenceException.class, - () -> duplicates.recordAccount(meta, accountKey, - BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(rawAccount))); - duplicates.recordAssetPut(meta, accountKey, assetKey, bytes(1, 3)); - assertThrows(ArchivePersistenceException.class, - () -> duplicates.recordAssetDelete(meta, accountKey, assetKey)); - assertThrows(ArchivePersistenceException.class, - () -> duplicates.recordAssetPut(meta, accountKey, bytes(3, 7), bytes(1, 3))); - - AccountAssetForwardMutationRecorder incomplete = - new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); - incomplete.recordAssetDelete(meta, accountKey, assetKey); - assertThrows(ArchivePersistenceException.class, () -> incomplete.seal(marker)); - - AccountAssetForwardMutationRecorder sealed = - new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); - sealed.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(rawAccount)); - sealed.seal(marker); - assertThrows(ArchivePersistenceException.class, () -> sealed.seal(marker)); - assertThrows(ArchivePersistenceException.class, - () -> sealed.recordAssetDelete(meta, accountKey, assetKey)); - } - - @Test - public void recorderDefensivelyTransfersPayloadBeforeCommittedMarkerExists() { - BlockSnapshotMeta meta = meta(11); - byte[] accountKey = bytes(2, 1); - byte[] rawAccount = bytes(3, 2); - byte[] canonicalAccount = bytes(3, 3); - byte[] assetKey = assetKey(accountKey, 1); - byte[] assetValue = bytes(2, 4); - byte[] expectedAccountKey = Arrays.copyOf(accountKey, accountKey.length); - byte[] expectedRaw = Arrays.copyOf(rawAccount, rawAccount.length); - byte[] expectedCanonical = Arrays.copyOf(canonicalAccount, canonicalAccount.length); - byte[] expectedAssetKey = Arrays.copyOf(assetKey, assetKey.length); - byte[] expectedAssetValue = Arrays.copyOf(assetValue, assetValue.length); - AccountAssetForwardMutationRecorder recorder = - new AccountAssetForwardMutationRecorder(meta, Phase.P66_ON, limits()); - - recorder.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(canonicalAccount)); - recorder.recordAssetPut(meta, accountKey, assetKey, assetValue); - Arrays.fill(accountKey, (byte) 9); - Arrays.fill(rawAccount, (byte) 9); - Arrays.fill(canonicalAccount, (byte) 9); - Arrays.fill(assetKey, (byte) 9); - Arrays.fill(assetValue, (byte) 9); - - HistoryCommitMarker marker = marker(meta, participants()); - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").put(expectedAccountKey, expectedRaw)); - ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, - new ArchiveParticipantMutationBatchCollector(recorder.seal(marker)) - .collect(marker, view)); - assertArrayEquals(expectedCanonical, plan.getMutations("account").get(0).getValue()); - assertArrayEquals(expectedAssetKey, - plan.getMutations("account-asset").get(0).getKey()); - assertArrayEquals(expectedAssetValue, - plan.getMutations("account-asset").get(0).getValue()); - } - } - - @Test - public void blockCaptureOwnsViewRecorderAndBatchAsOneShot() { - BlockSnapshotMeta meta = meta(12); - HistoryCommitMarker marker = marker(meta, participants()); - byte[] accountKey = bytes(2, 1); - byte[] rawAccount = bytes(3, 2); - byte[] canonicalAccount = bytes(3, 3); - byte[] assetKey = assetKey(accountKey, 1); - ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, limits()); - capture.recordAssetPut(meta, accountKey, assetKey, new byte[0]); - capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(canonicalAccount)); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").put(accountKey, rawAccount)); - capture.attach(view); - assertTrue(capture.hasAttachedView()); - assertFalse(capture.isPayloadReleased()); - ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, - capture.seal(marker)); - assertArrayEquals(canonicalAccount, plan.getMutations("account").get(0).getValue()); - assertEquals(0, plan.getMutations("account-asset").get(0).getValue().length); - assertFalse(capture.hasAttachedView()); - assertTrue(capture.isPayloadReleased()); - assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); - assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); - assertThrows(ArchivePersistenceException.class, - () -> capture.recordAssetDelete(meta, accountKey, assetKey)); - assertThrows(ArchivePersistenceException.class, capture::abort); - } - } - - @Test - public void blockCapturePreconditionFailuresRemainRetryableBeforeManifestConsumption() { - BlockSnapshotMeta meta = meta(13); - BlockSnapshotMeta otherMeta = meta(14); - HistoryCommitMarker marker = marker(meta, participants()); - HistoryCommitMarker otherMarker = marker(otherMeta, participants()); - ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, limits()); - assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); - - try (Fixture exact = new Fixture(participants()); - Fixture other = new Fixture(participants())) { - BlockChangeView wrongView = other.capture(otherMeta, - databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); - assertThrows(ArchivePersistenceException.class, () -> capture.attach(wrongView)); - BlockChangeView view = exact.capture(meta, - databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); - capture.attach(view); - assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); - assertThrows(ArchivePersistenceException.class, () -> capture.seal(otherMarker)); - ArchiveParticipantMutationBatch batch = capture.seal(marker); - assertEquals(meta.getEpoch(), batch.getTargetEpoch()); - } - } - - @Test - public void blockCaptureCoverageFailureConsumesOwnershipAndBecomesTerminal() { - BlockSnapshotMeta meta = meta(15); - HistoryCommitMarker marker = marker(meta, participants()); - byte[] accountKey = bytes(2, 1); - byte[] rawAccount = bytes(3, 2); - ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, limits()); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").put(accountKey, rawAccount)); - capture.attach(view); - assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); - assertFalse(capture.hasAttachedView()); - assertTrue(capture.isPayloadReleased()); - assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); - assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); - assertThrows(ArchivePersistenceException.class, - () -> capture.recordAccount(meta, accountKey, - BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(rawAccount))); - assertThrows(ArchivePersistenceException.class, capture::abort); - } - } - - @Test - public void blockCaptureAbortBeforeAttachReleasesPayloadAndRejectsEveryTerminalAction() { - BlockSnapshotMeta meta = meta(20); - HistoryCommitMarker marker = marker(meta, participants()); - byte[] accountKey = bytes(2, 1); - byte[] assetKey = assetKey(accountKey, 1); - ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, limits()); - capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(bytes(3, 2)), - BlockChangeView.PostValue.present(bytes(3, 3))); - capture.recordAssetPut(meta, accountKey, assetKey, bytes(3, 4)); - assertFalse(capture.hasAttachedView()); - assertFalse(capture.isPayloadReleased()); - - capture.abort(); - - assertFalse(capture.hasAttachedView()); - assertTrue(capture.isPayloadReleased()); - assertThrows(ArchivePersistenceException.class, capture::abort); - assertThrows(ArchivePersistenceException.class, - () -> capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), - BlockChangeView.PostValue.absent())); - assertThrows(ArchivePersistenceException.class, - () -> capture.recordAssetDelete(meta, accountKey, assetKey)); - assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); - assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); - } - } - - @Test - public void blockCaptureAbortAfterAttachReleasesViewAndPayload() { - BlockSnapshotMeta meta = meta(21); - HistoryCommitMarker marker = marker(meta, participants()); - byte[] accountKey = bytes(2, 1); - ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, limits()); - capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), - BlockChangeView.PostValue.absent()); - - try (Fixture fixture = new Fixture(participants())) { - fixture.rootPut("account", accountKey, bytes(1, 9)); - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").delete(accountKey)); - capture.attach(view); - assertTrue(capture.hasAttachedView()); - assertFalse(capture.isPayloadReleased()); - - capture.abort(); - - assertFalse(capture.hasAttachedView()); - assertTrue(capture.isPayloadReleased()); - assertThrows(ArchivePersistenceException.class, capture::abort); - assertThrows(ArchivePersistenceException.class, () -> capture.attach(view)); - assertThrows(ArchivePersistenceException.class, () -> capture.seal(marker)); - } - } - - @Test - public void captureLimitsAcceptExactBoundaryWithDeleteAndPresentEmpty() { - BlockSnapshotMeta meta = meta(16); - HistoryCommitMarker marker = marker(meta, participants()); - byte[] accountKey = bytes(2, 1); - byte[] rawAccount = bytes(1, 2); - byte[] canonicalAccount = bytes(1, 3); - byte[] emptyAsset = assetKey(accountKey, 1); - byte[] deletedAsset = assetKey(accountKey, 2); - ArchiveBlockForwardMutationLimits exact = - new ArchiveBlockForwardMutationLimits(1, 2, 3, 1, 13); - ArchiveBlockForwardMutationCapture capture = - new ArchiveBlockForwardMutationCapture(meta, Phase.P66_ON, exact); - capture.recordAssetPut(meta, accountKey, emptyAsset, new byte[0]); - capture.recordAssetDelete(meta, accountKey, deletedAsset); - capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(canonicalAccount)); - - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").put(accountKey, rawAccount)); - capture.attach(view); - ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, - capture.seal(marker)); - assertEquals(0, plan.getMutations("account-asset").get(0).getValue().length); - assertNull(plan.getMutations("account-asset").get(1).getValue()); - } - } - - @Test - public void captureLimitsRejectEveryDimensionAndNegativeConfiguration() { - BlockSnapshotMeta meta = meta(17); - byte[] accountKey = bytes(2, 1); - byte[] assetKey = assetKey(accountKey, 1); - assertThrows(IllegalArgumentException.class, - () -> new ArchiveBlockForwardMutationLimits(-1, 1, 1, 1, 1)); - - AccountAssetForwardMutationRecorder accounts = new AccountAssetForwardMutationRecorder(meta, - Phase.P66_ON, new ArchiveBlockForwardMutationLimits(0, 1, 3, 1, 10)); - assertThrows(ArchivePersistenceException.class, - () -> accounts.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), - BlockChangeView.PostValue.absent())); - - AccountAssetForwardMutationRecorder assets = new AccountAssetForwardMutationRecorder(meta, - Phase.P66_ON, new ArchiveBlockForwardMutationLimits(1, 0, 3, 1, 10)); - assets.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), - BlockChangeView.PostValue.absent()); - assertThrows(ArchivePersistenceException.class, - () -> assets.recordAssetDelete(meta, accountKey, assetKey)); - - AccountAssetForwardMutationRecorder keys = new AccountAssetForwardMutationRecorder(meta, - Phase.P66_ON, new ArchiveBlockForwardMutationLimits(1, 1, 1, 1, 10)); - assertThrows(ArchivePersistenceException.class, - () -> keys.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), - BlockChangeView.PostValue.absent())); - - AccountAssetForwardMutationRecorder values = new AccountAssetForwardMutationRecorder(meta, - Phase.P66_ON, new ArchiveBlockForwardMutationLimits(1, 1, 3, 0, 10)); - assertThrows(ArchivePersistenceException.class, - () -> values.recordAccount(meta, accountKey, - BlockChangeView.PostValue.present(bytes(1, 2)), - BlockChangeView.PostValue.present(bytes(1, 3)))); - - AccountAssetForwardMutationRecorder total = new AccountAssetForwardMutationRecorder(meta, - Phase.P66_ON, new ArchiveBlockForwardMutationLimits(1, 1, 3, 1, 3)); - assertThrows(ArchivePersistenceException.class, - () -> total.recordAccount(meta, accountKey, - BlockChangeView.PostValue.present(bytes(1, 2)), - BlockChangeView.PostValue.present(bytes(1, 3)))); - } - - @Test - public void captureLimitRejectionAndDuplicatesDoNotConsumeReservation() { - BlockSnapshotMeta meta = meta(18); - HistoryCommitMarker marker = marker(meta, participants()); - byte[] accountKey = bytes(2, 1); - byte[] firstAsset = assetKey(accountKey, 1); - byte[] secondAsset = assetKey(accountKey, 2); - ArchiveBlockForwardMutationCapture capture = new ArchiveBlockForwardMutationCapture(meta, - Phase.P66_ON, new ArchiveBlockForwardMutationLimits(1, 2, 3, 1, 10)); - - assertThrows(ArchivePersistenceException.class, - () -> capture.recordAccount(meta, accountKey, - BlockChangeView.PostValue.present(bytes(2, 2)), - BlockChangeView.PostValue.present(bytes(2, 3)))); - capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), - BlockChangeView.PostValue.absent()); - assertThrows(ArchivePersistenceException.class, - () -> capture.recordAccount(meta, accountKey, BlockChangeView.PostValue.absent(), - BlockChangeView.PostValue.absent())); - capture.recordAssetDelete(meta, accountKey, firstAsset); - assertThrows(ArchivePersistenceException.class, - () -> capture.recordAssetDelete(meta, accountKey, firstAsset)); - capture.recordAssetDelete(meta, accountKey, secondAsset); - - try (Fixture fixture = new Fixture(participants())) { - fixture.rootPut("account", accountKey, bytes(1, 9)); - BlockChangeView view = fixture.capture(meta, - databases -> databases.get("account").delete(accountKey)); - capture.attach(view); - ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(marker, - capture.seal(marker)); - assertNull(plan.getMutations("account").get(0).getValue()); - assertEquals(2, plan.getMutations("account-asset").size()); - } - } - - @Test - public void captureLimitsReserveAttachedViewAtomicallyAndAllowRetry() { - BlockSnapshotMeta meta = meta(19); - HistoryCommitMarker marker = marker(meta, participants()); - ArchiveBlockForwardMutationCapture total = new ArchiveBlockForwardMutationCapture(meta, - Phase.P66_ON, new ArchiveBlockForwardMutationLimits(0, 0, 3, 3, 3)); - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView tooLarge = fixture.capture(meta, - databases -> databases.get("code").put(bytes(2, 1), bytes(2, 2))); - assertThrows(ArchivePersistenceException.class, () -> total.attach(tooLarge)); - BlockChangeView exact = fixture.capture(meta, - databases -> databases.get("code").put(bytes(1, 1), bytes(1, 2))); - total.attach(exact); - assertEquals(meta.getEpoch(), total.seal(marker).getTargetEpoch()); - } - - ArchiveBlockForwardMutationCapture key = new ArchiveBlockForwardMutationCapture(meta, - Phase.P66_ON, new ArchiveBlockForwardMutationLimits(0, 0, 1, 2, 10)); - ArchiveBlockForwardMutationCapture value = new ArchiveBlockForwardMutationCapture(meta, - Phase.P66_ON, new ArchiveBlockForwardMutationLimits(0, 0, 2, 1, 10)); - try (Fixture fixture = new Fixture(participants())) { - BlockChangeView keyTooLarge = fixture.capture(meta, - databases -> databases.get("code").put(bytes(2, 1), new byte[0])); - assertThrows(ArchivePersistenceException.class, () -> key.attach(keyTooLarge)); - BlockChangeView valueTooLarge = fixture.capture(meta, - databases -> databases.get("code").put(bytes(1, 1), bytes(2, 2))); - assertThrows(ArchivePersistenceException.class, () -> value.attach(valueTooLarge)); - } - } - - private static Entry entry(byte[] accountKey, byte[] rawAccount, byte[] canonicalAccount, - AssetMutation... mutations) { - return new Entry(accountKey, BlockChangeView.PostValue.present(rawAccount), - BlockChangeView.PostValue.present(canonicalAccount), Arrays.asList(mutations)); - } - - private static byte[] assetKey(byte[] accountKey, int suffix) { - byte[] key = Arrays.copyOf(accountKey, accountKey.length + 1); - key[key.length - 1] = (byte) suffix; - return key; - } - - private static ArchiveBlockForwardMutationLimits limits() { - return new ArchiveBlockForwardMutationLimits(100, 1_000, 1_024, 1024 * 1024, - 10L * 1024 * 1024); - } - - private static List participants() { - List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); - Collections.sort(participants); - return participants; - } - - private static BlockSnapshotMeta meta(int epoch) { - return BlockSnapshotMeta.forBlock(epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L); - } - - private static HistoryCommitMarker marker(BlockSnapshotMeta meta, List participants) { - int epoch = (int) meta.getEpoch(); - return new HistoryCommitMarker(meta, epoch - 1, - new HistoryLocation(0, epoch * 100L, 100, epoch, bytes(32, epoch + 20)), - new HistoryIndexLocation(epoch * 50L, 50, bytes(32, epoch + 30)), - bytes(16, epoch + 40), participants); - } - - private static byte[] hash(int suffix) { - byte[] hash = new byte[32]; - hash[31] = (byte) suffix; - return hash; - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } - - @FunctionalInterface - private interface Mutator { - void mutate(Map databases); - } - - private static final class Fixture implements AutoCloseable { - private final SnapshotManager manager = new SnapshotManager(""); - private final Map roots = new LinkedHashMap<>(); - private final Map databases = new LinkedHashMap<>(); - private final List ordered = new ArrayList<>(); - - private Fixture(List participants) { - for (String participant : participants) { - MemoryDb root = new MemoryDb(participant); - Chainbase database = new Chainbase(new SnapshotRoot(root)); - roots.put(participant, root); - databases.put(participant, database); - ordered.add(database); - manager.add(database); - } - manager.enable(); - } - - private void rootPut(String dbName, byte[] key, byte[] value) { - roots.get(dbName).put(key, value); - } - - private BlockChangeView capture(BlockSnapshotMeta meta, Mutator mutator) { - try (ISession session = manager.buildSession()) { - mutator.mutate(databases); - return BlockChangeView.capture(meta, ordered); - } - } - - @Override - public void close() { - manager.shutdown(); - } - } - - private static final class MemoryDb implements DB, Flusher { - private final String name; - private final Map values = new LinkedHashMap<>(); - - private MemoryDb(String name) { - this.name = name; - } - - @Override - public byte[] get(byte[] key) { - byte[] value = values.get(WrappedByteArray.of(key)); - return value == null ? null : Arrays.copyOf(value, value.length); - } - - @Override - public void put(byte[] key, byte[] value) { - values.put(WrappedByteArray.copyOf(key), Arrays.copyOf(value, value.length)); - } - - @Override - public long size() { - return values.size(); - } - - @Override - public boolean isEmpty() { - return values.isEmpty(); - } - - @Override - public void remove(byte[] key) { - values.remove(WrappedByteArray.of(key)); - } - - @Override - public Iterator> iterator() { - List> entries = new ArrayList<>(); - values.forEach((key, value) -> entries.add(new AbstractMap.SimpleImmutableEntry<>( - key.getBytes(), Arrays.copyOf(value, value.length)))); - return entries.iterator(); - } - - @Override - public void close() { - values.clear(); - } - - @Override - public void flush(Map batch) { - batch.forEach((key, value) -> { - if (value == null || value.getBytes() == null) { - values.remove(key); - } else { - values.put(WrappedByteArray.copyOf(key.getBytes()), value.getBytes()); - } - }); - } - - @Override - public void reset() { - values.clear(); - } - - @Override - public String getDbName() { - return name; - } - - @Override - public void stat() { - } - - @Override - public DB newInstance() { - return new MemoryDb(name); - } - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java deleted file mode 100644 index 766271ca017..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantRecoveryStorageTest.java +++ /dev/null @@ -1,246 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; - -public class ArchiveParticipantRecoveryStorageTest { - - private static final List PARTICIPANTS = - Arrays.asList("account", "account-asset"); - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void secondRestartFinishesOnlyRemainingMixedEngineParticipant() throws Exception { - Path archive = temporaryFolder.newFolder("mixed-native-recovery").toPath(); - Path checkpointPath = archive.resolve("progress/checkpoint.progress"); - Path readerPath = archive.resolve("progress/reader.progress"); - List markers = initializeHistory(archive, 3); - new ArchiveProgressFile(checkpointPath, new ArchiveProgressEnvelopeCodec()) - .store(global(Kind.APPLY_CHECKPOINT, markers.get(1))); - new ArchiveProgressFile(readerPath, new ArchiveProgressEnvelopeCodec()) - .store(global(Kind.READER_VISIBLE, markers.get(0))); - - try (LevelDbArchiveParticipant account = new LevelDbArchiveParticipant( - archive.resolve("participants/account"), "account", PARTICIPANTS); - RocksDbArchiveParticipant asset = new RocksDbArchiveParticipant( - archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS)) { - account.apply(Collections.emptyList(), participant("account", markers.get(0))); - asset.apply(Collections.emptyList(), participant("account-asset", markers.get(0))); - Map engines = engines(account, asset); - ArchiveTargetMutationPlan activePlan = storePlan(checkpointPath, markers.get(1)); - new ArchiveProgressFile(checkpointPath, new ArchiveProgressEnvelopeCodec()) - .store(global(Kind.APPLY_CHECKPOINT, markers.get(1), activePlan.digest())); - - try (ArchiveParticipantRecoveryStorage first = new ArchiveParticipantRecoveryStorage( - archive, 4096, checkpointPath, failingEngines(account, asset), readerPath, - PARTICIPANTS)) { - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveRecoveryExecutor(first).recover()); - } - - assertEquals(2, account.loadProgress().getEpoch()); - assertArrayEquals(bytes("account:2-2"), account.get(bytes("replayed"))); - assertEquals(1, asset.loadProgress().getEpoch()); - assertNull(asset.get(bytes("replayed"))); - assertEquals(1, reader(readerPath).getEpoch()); - assertEquals(2, ArchiveRestartCheckpoint.load(archive, - new HistoryCommitMarkerCodec()).getMarker().getMeta().getEpoch()); - assertFalse(Files.exists(archive.resolve("truncation.intent"))); - - ArchiveTargetMutationPlanFile planFile = new ArchiveTargetMutationPlanFile(checkpointPath); - byte[] validPlan = Files.readAllBytes(planFile.getPath()); - Files.delete(planFile.getPath()); - assertRecoveryFails(archive, checkpointPath, engines, readerPath); - Files.write(planFile.getPath(), validPlan); - byte[] corruptPlan = Arrays.copyOf(validPlan, validPlan.length); - corruptPlan[corruptPlan.length - 1] ^= 1; - Files.write(planFile.getPath(), corruptPlan); - assertRecoveryFails(archive, checkpointPath, engines, readerPath); - storeSubstitutedPlan(checkpointPath, markers.get(1)); - assertRecoveryFails(archive, checkpointPath, engines, readerPath); - storePlan(checkpointPath, markers.get(0)); - assertRecoveryFails(archive, checkpointPath, engines, readerPath); - Files.write(planFile.getPath(), validPlan); - account.apply(Collections.emptyList(), participant("account", markers.get(1), - bytes(32, 99))); - assertRecoveryFails(archive, checkpointPath, engines, readerPath); - account.apply(Collections.emptyList(), participant("account", markers.get(1), - activePlan.digest())); - - try (ArchiveParticipantRecoveryStorage second = new ArchiveParticipantRecoveryStorage( - archive, 4096, checkpointPath, engines, readerPath, PARTICIPANTS)) { - assertEquals(2, new ArchiveRecoveryExecutor(second).recover().getActions().size()); - } - - assertEquals(2, account.loadProgress().getEpoch()); - assertEquals(2, asset.loadProgress().getEpoch()); - assertArrayEquals(bytes("account-asset:2-2"), asset.get(bytes("replayed"))); - assertEquals(2, reader(readerPath).getEpoch()); - assertFalse(Files.exists(planFile.getPath())); - - try (ArchiveParticipantRecoveryStorage third = new ArchiveParticipantRecoveryStorage( - archive, 4096, checkpointPath, engines, readerPath, PARTICIPANTS)) { - assertEquals(0, new ArchiveRecoveryExecutor(third).recover().getActions().size()); - } - } - } - - private static List mutation(String participant, long firstEpoch, - long lastEpoch) { - return Collections.singletonList(ArchiveParticipantMutation.put(bytes("replayed"), - bytes(participant + ":" + firstEpoch + "-" + lastEpoch))); - } - - private static ArchiveTargetMutationPlan storePlan(Path checkpointPath, - HistoryCommitMarker marker) - throws IOException { - Map> mutations = new LinkedHashMap<>(); - mutations.put("account", mutation("account", 2, 2)); - mutations.put("account-asset", mutation("account-asset", 2, 2)); - ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlan( - global(Kind.APPLY_CHECKPOINT, marker), P66AccountAssetCodec.FORMAT_ID, - Phase.P66_ON, mutations); - new ArchiveTargetMutationPlanFile(checkpointPath).store(plan); - return plan; - } - - private static void storeSubstitutedPlan(Path checkpointPath, HistoryCommitMarker marker) - throws IOException { - Map> mutations = new LinkedHashMap<>(); - mutations.put("account", Collections.singletonList( - ArchiveParticipantMutation.put(bytes("replayed"), bytes("substituted-account")))); - mutations.put("account-asset", Collections.singletonList( - ArchiveParticipantMutation.put(bytes("replayed"), bytes("substituted-asset")))); - new ArchiveTargetMutationPlanFile(checkpointPath).store(new ArchiveTargetMutationPlan( - global(Kind.APPLY_CHECKPOINT, marker), P66AccountAssetCodec.FORMAT_ID, - Phase.P66_ON, mutations)); - } - - private static void assertRecoveryFails(Path archive, Path checkpointPath, - Map engines, Path readerPath) throws IOException { - try (ArchiveParticipantRecoveryStorage storage = new ArchiveParticipantRecoveryStorage( - archive, 4096, checkpointPath, engines, readerPath, PARTICIPANTS)) { - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveRecoveryExecutor(storage).recover()); - } - } - - private static Map failingEngines( - ArchiveParticipant account, ArchiveParticipant asset) { - Map engines = new LinkedHashMap<>(); - engines.put("account", account); - engines.put("account-asset", new ArchiveParticipant() { - @Override - public void apply(List mutations, - ArchiveProgressEnvelope progress) throws IOException { - throw new IOException("injected second participant replay failure"); - } - - @Override - public ArchiveProgressEnvelope loadProgress() throws IOException { - return asset.loadProgress(); - } - }); - return engines; - } - - private static ArchiveProgressEnvelope reader(Path readerPath) throws IOException { - return new ArchiveProgressFile(readerPath, new ArchiveProgressEnvelopeCodec()).load(); - } - - private static List initializeHistory(Path archive, int lastEpoch) - throws Exception { - List markers = new ArrayList<>(); - try (HistorySegmentStore bodies = new HistorySegmentStore( - archive, new BlockHistoryCodec(), 4096); - HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); - HistoryCommitStore commits = new HistoryCommitStore( - archive, new HistoryCommitMarkerCodec())) { - for (int epoch = 1; epoch <= lastEpoch; epoch++) { - BlockReverseDiff diff = new BlockReverseDiff( - new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L), - Collections.singletonList(new BlockReverseDiff.DbGroup("account", - Collections.singletonList(new BlockReverseDiff.Entry(bytes("key-" + epoch), - OldValue.present(bytes("old-" + epoch))))))); - HistoryLocation body = bodies.append(diff); - HistoryIndexLocation location = index.append(HistoryIndexRecord.from(diff, body)); - markers.add(new HistoryCommitMarker(diff.getMeta(), epoch - 1L, body, location, - bytes(16, epoch + 40), PARTICIPANTS)); - } - bodies.sync(); - index.sync(); - commits.commitAll(markers); - ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), - commits.getRecordLength(), commits.head(), new HistoryCommitMarkerCodec()); - } - return markers; - } - - private static Map engines( - ArchiveParticipant account, ArchiveParticipant asset) { - Map engines = new LinkedHashMap<>(); - engines.put("account", account); - engines.put("account-asset", asset); - return engines; - } - - private static ArchiveProgressEnvelope participant(String name, - HistoryCommitMarker marker) { - return participant(name, marker, null); - } - - private static ArchiveProgressEnvelope participant(String name, - HistoryCommitMarker marker, byte[] mutationPlanDigest) { - return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, name, - marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, PARTICIPANTS); - } - - private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { - return global(kind, marker, null); - } - - private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker, - byte[] mutationPlanDigest) { - return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, PARTICIPANTS); - } - - private static byte[] hash(int suffix) { - byte[] hash = new byte[32]; - hash[31] = (byte) suffix; - return hash; - } - - private static byte[] bytes(String value) { - return value.getBytes(StandardCharsets.UTF_8); - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java deleted file mode 100644 index e039a5d455b..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReaderPublicationGateTest.java +++ /dev/null @@ -1,292 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.ISession; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.ArchiveReaderPublicationGate.ProgressSource; -import org.tron.core.db2.core.SnapshotManager; - -public class ArchiveReaderPublicationGateTest { - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void publishesExactAuthoritiesWhileMergeAndFlushAreBlocked() throws Exception { - try (Fixture fixture = fixture()) { - SnapshotManager manager = new SnapshotManager(""); - manager.enable(); - ISession parent = manager.buildSession(); - ISession child = manager.buildSession(); - CountDownLatch entered = new CountDownLatch(1); - CountDownLatch release = new CountDownLatch(1); - AtomicBoolean paused = new AtomicBoolean(); - Map sources = fixture.sources(); - String first = fixture.participants.get(0); - ArchiveParticipantProgressSource original = sources.get(first); - sources.put(first, () -> { - ArchiveProgressEnvelope loaded = original.loadProgress(); - if (paused.compareAndSet(false, true)) { - entered.countDown(); - try { - if (!release.await(5, TimeUnit.SECONDS)) { - throw new IOException("Timed out waiting to release publication gate"); - } - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - throw new IOException("Interrupted inside publication gate", interrupted); - } - } - return loaded; - }); - ArchiveReaderPublicationGate gate = new ArchiveReaderPublicationGate(fixture.history, - fixture.checkpointSource(), sources, fixture.readerPath, fixture.participants, - manager::withArchiveStateBarrier); - ExecutorService executor = Executors.newFixedThreadPool(3); - try { - Future publication = executor.submit(() -> { - gate.publish(1); - return null; - }); - assertTrue(entered.await(5, TimeUnit.SECONDS)); - Future merge = executor.submit(child::merge); - Future flush = executor.submit(manager::flush); - assertThrows(TimeoutException.class, - () -> merge.get(100, TimeUnit.MILLISECONDS)); - assertThrows(TimeoutException.class, - () -> flush.get(100, TimeUnit.MILLISECONDS)); - - release.countDown(); - publication.get(5, TimeUnit.SECONDS); - merge.get(5, TimeUnit.SECONDS); - flush.get(5, TimeUnit.SECONDS); - assertEquals(1, fixture.reader().getEpoch()); - } finally { - release.countDown(); - child.close(); - parent.close(); - executor.shutdownNow(); - } - } - } - - @Test - public void missingMismatchedOrNullParticipantNeverAdvancesReader() throws Exception { - try (Fixture missing = fixture()) { - Files.delete(missing.participantPaths.get(missing.participants.get(0))); - assertThrows(ArchivePersistenceException.class, - () -> missing.fileGate().publish(1)); - assertEquals(0, missing.reader().getEpoch()); - } - - try (Fixture mismatch = fixture()) { - String participant = mismatch.participants.get(0); - mismatch.store(mismatch.participantPaths.get(participant), - mismatch.envelope(Kind.PARTICIPANT_PROGRESS, participant, 0)); - assertThrows(ArchivePersistenceException.class, - () -> mismatch.fileGate().publish(1)); - assertEquals(0, mismatch.reader().getEpoch()); - } - - try (Fixture absentLevelDb = fixture()) { - Map sources = absentLevelDb.sources(); - sources.put("account", () -> null); - ArchiveReaderPublicationGate gate = new ArchiveReaderPublicationGate(absentLevelDb.history, - absentLevelDb.checkpointSource(), sources, absentLevelDb.readerPath, - absentLevelDb.participants, action -> action.run()); - assertThrows(ArchivePersistenceException.class, () -> gate.publish(1)); - assertEquals(0, absentLevelDb.reader().getEpoch()); - } - } - - @Test - public void secondScanDriftAndRegressionPreserveCurrentReader() throws Exception { - try (Fixture drift = fixture()) { - Map sources = drift.sources(); - String participant = drift.participants.get(0); - ArchiveParticipantProgressSource stable = sources.get(participant); - AtomicInteger reads = new AtomicInteger(); - sources.put(participant, () -> reads.getAndIncrement() == 0 - ? stable.loadProgress() : drift.envelope(Kind.PARTICIPANT_PROGRESS, participant, 0)); - ArchiveReaderPublicationGate gate = new ArchiveReaderPublicationGate(drift.history, - drift.checkpointSource(), sources, drift.readerPath, drift.participants, - action -> action.run()); - assertThrows(ArchivePersistenceException.class, () -> gate.publish(1)); - assertEquals(0, drift.reader().getEpoch()); - } - - try (Fixture regression = fixture()) { - regression.fileGate().publish(1); - regression.writeAuthorities(0); - assertThrows(ArchivePersistenceException.class, - () -> regression.fileGate().publish(0)); - assertEquals(1, regression.reader().getEpoch()); - } - } - - @Test - public void publicationFaultKeepsOldReaderAndRetryPublishesOnce() throws Exception { - try (Fixture fixture = fixture()) { - ArchiveReaderPublicationGate failing = new ArchiveReaderPublicationGate(fixture.history, - fixture.checkpointSource(), fixture.sources(), fixture.readerPath, - fixture.participants, action -> action.run(), temporary -> { - throw new IOException("injected after reader temporary force"); - }); - assertThrows(IOException.class, () -> failing.publish(1)); - assertEquals(0, fixture.reader().getEpoch()); - - fixture.fileGate().publish(1); - assertEquals(1, fixture.reader().getEpoch()); - } - } - - @Test - public void inheritsExactPlanDigestAndRejectsParticipantMismatch() throws Exception { - byte[] digest = bytes(32, 77); - try (Fixture inherited = fixture()) { - inherited.writeAuthorities(1, digest); - inherited.fileGate().publish(1); - assertArrayEquals(digest, inherited.reader().getMutationPlanDigest()); - } - - try (Fixture mismatch = fixture()) { - mismatch.writeAuthorities(1, digest); - String participant = mismatch.participants.get(0); - mismatch.store(mismatch.participantPaths.get(participant), - mismatch.envelope(Kind.PARTICIPANT_PROGRESS, participant, 1, bytes(32, 78))); - assertThrows(ArchivePersistenceException.class, - () -> mismatch.fileGate().publish(1)); - assertEquals(0, mismatch.reader().getEpoch()); - } - } - - private Fixture fixture() throws Exception { - return new Fixture(temporaryFolder.newFolder().toPath()); - } - - private static final class Fixture implements AutoCloseable { - private final List participants; - private final HistoryCommitStore history; - private final Path checkpointPath; - private final Map participantPaths = new LinkedHashMap<>(); - private final Path readerPath; - private final ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - - private Fixture(Path directory) throws Exception { - participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); - java.util.Collections.sort(participants); - history = new HistoryCommitStore(directory, new HistoryCommitMarkerCodec()); - history.commitAll(Arrays.asList(marker(0), marker(1))); - checkpointPath = directory.resolve("progress/checkpoint.progress"); - for (String participant : participants) { - participantPaths.put(participant, - directory.resolve("progress/participants/" + participant + ".progress")); - } - readerPath = directory.resolve("progress/reader.progress"); - writeAuthorities(1); - store(readerPath, envelope(Kind.READER_VISIBLE, null, 0)); - } - - private ArchiveReaderPublicationGate fileGate() { - return ArchiveReaderPublicationGate.forFiles(history, checkpointPath, participantPaths, - readerPath, participants, action -> action.run()); - } - - private ProgressSource checkpointSource() { - return () -> new ArchiveProgressFile(checkpointPath, codec).load(); - } - - private Map sources() { - Map sources = new TreeMap<>(); - participantPaths.forEach((participant, path) -> sources.put(participant, - () -> new ArchiveProgressFile(path, codec).load())); - return sources; - } - - private void writeAuthorities(int epoch) throws IOException { - writeAuthorities(epoch, null); - } - - private void writeAuthorities(int epoch, byte[] mutationPlanDigest) throws IOException { - store(checkpointPath, - envelope(Kind.APPLY_CHECKPOINT, null, epoch, mutationPlanDigest)); - for (Map.Entry entry : participantPaths.entrySet()) { - store(entry.getValue(), - envelope(Kind.PARTICIPANT_PROGRESS, entry.getKey(), epoch, - mutationPlanDigest)); - } - } - - private ArchiveProgressEnvelope reader() throws IOException { - return new ArchiveProgressFile(readerPath, codec).load(); - } - - private ArchiveProgressEnvelope envelope(Kind kind, String participant, int epoch) { - return envelope(kind, participant, epoch, null); - } - - private ArchiveProgressEnvelope envelope(Kind kind, String participant, int epoch, - byte[] mutationPlanDigest) { - HistoryCommitMarker marker = history.get(epoch); - return new ArchiveProgressEnvelope(kind, participant, epoch, - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), mutationPlanDigest, participants); - } - - private void store(Path path, ArchiveProgressEnvelope envelope) throws IOException { - new ArchiveProgressFile(path, codec).store(envelope); - } - - private HistoryCommitMarker marker(int epoch) { - BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), - epoch * 1_000L); - HistoryLocation body = new HistoryLocation(0, epoch * 100L, 100, epoch, - bytes(32, epoch + 20)); - HistoryIndexLocation index = new HistoryIndexLocation(epoch * 50L, 50, - bytes(32, epoch + 30)); - return new HistoryCommitMarker(meta, epoch - 1L, body, index, - bytes(16, epoch + 40), participants); - } - - @Override - public void close() throws IOException { - history.close(); - } - } - - private static byte[] hash(int suffix) { - byte[] hash = new byte[32]; - hash[31] = (byte) suffix; - return hash; - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScannerTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScannerTest.java deleted file mode 100644 index bde40c304a1..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryAuthorityScannerTest.java +++ /dev/null @@ -1,260 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoveryStorage; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; - -public class ArchiveRecoveryAuthorityScannerTest { - - private static final List PARTICIPANTS = Arrays.asList("account", "account-asset"); - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void composesFreshFileAuthoritiesWithExecutorScan() throws Exception { - try (Fixture fixture = fixture()) { - RecordingStorage storage = new RecordingStorage(fixture.scanner); - RecoveryPlan plan = new ArchiveRecoveryExecutor(storage).recover(); - - assertEquals(Arrays.asList("truncate:10", "replay:account-asset:9-10", "publish:10"), - storage.actions); - assertEquals(3, plan.getActions().size()); - assertEquals(8, plan.getSafeHeadBeforeRecovery()); - } - } - - @Test - public void corruptEnvelopeFailsBeforeFirstRecoveryAction() throws Exception { - try (Fixture fixture = fixture()) { - byte[] corrupt = Files.readAllBytes(fixture.participantPaths.get("account-asset")); - corrupt[corrupt.length - 1] ^= 1; - Files.write(fixture.participantPaths.get("account-asset"), corrupt); - RecordingStorage storage = new RecordingStorage(fixture.scanner); - - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveRecoveryExecutor(storage).recover()); - assertEquals(0, storage.actions.size()); - } - } - - @Test - public void missingOrMismatchedIdentityFailsBeforeFirstRecoveryAction() throws Exception { - try (Fixture missing = fixture()) { - Files.delete(missing.checkpointPath); - RecordingStorage storage = new RecordingStorage(missing.scanner); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveRecoveryExecutor(storage).recover()); - assertEquals(0, storage.actions.size()); - } - - try (Fixture mismatch = fixture()) { - HistoryCommitMarker marker = mismatch.history.get(8); - new ArchiveProgressFile(mismatch.participantPaths.get("account-asset"), - new ArchiveProgressEnvelopeCodec()).store(new ArchiveProgressEnvelope( - Kind.PARTICIPANT_PROGRESS, "account-asset", 8, - marker.getMeta().getBlockHash(), bytes(16, 99), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS)); - RecordingStorage storage = new RecordingStorage(mismatch.scanner); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveRecoveryExecutor(storage).recover()); - assertEquals(0, storage.actions.size()); - } - } - - @Test - public void readerPublishCrashPreservesOldAuthorityAndSecondRecoveryResumes() - throws Exception { - try (Fixture fixture = fixture()) { - ArchiveReaderHeadPublisher failingPublisher = new ArchiveReaderHeadPublisher( - fixture.history, fixture.readerVisiblePath, PARTICIPANTS, temporary -> { - throw new IOException("injected after reader temporary force"); - }); - DurableStorage first = new DurableStorage(fixture, failingPublisher); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveRecoveryExecutor(first).recover()); - assertEquals(Arrays.asList("truncate:10", "replay:account-asset:9-10"), first.actions); - assertEquals(8, new ArchiveProgressFile(fixture.readerVisiblePath, - new ArchiveProgressEnvelopeCodec()).load().getEpoch()); - - RecoverySnapshot afterCrash = fixture.scanner.scan(); - assertEquals(10, afterCrash.getHistoryHead()); - assertEquals(Long.valueOf(10), - afterCrash.getParticipantHeads().get("account-asset")); - assertEquals(8, afterCrash.getReaderVisibleHead()); - - ArchiveReaderHeadPublisher publisher = new ArchiveReaderHeadPublisher( - fixture.history, fixture.readerVisiblePath, PARTICIPANTS); - DurableStorage second = new DurableStorage(fixture, publisher); - new ArchiveRecoveryExecutor(second).recover(); - assertEquals(Arrays.asList("publish:10"), second.actions); - assertEquals(10, fixture.scanner.scan().getReaderVisibleHead()); - - DurableStorage third = new DurableStorage(fixture, publisher); - assertEquals(0, new ArchiveRecoveryExecutor(third).recover().getActions().size()); - assertEquals(0, third.actions.size()); - } - } - - private Fixture fixture() throws Exception { - Path directory = temporaryFolder.newFolder().toPath(); - HistoryCommitStore history = new HistoryCommitStore(directory, - new HistoryCommitMarkerCodec()); - List markers = new ArrayList<>(); - for (long epoch = 8; epoch <= 12; epoch++) { - markers.add(marker(epoch)); - } - history.commitAll(markers); - - Path checkpointPath = directory.resolve("progress/checkpoint.progress"); - Map participantPaths = new LinkedHashMap<>(); - participantPaths.put("account", directory.resolve("progress/account.progress")); - participantPaths.put("account-asset", - directory.resolve("progress/account-asset.progress")); - ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - new ArchiveProgressFile(checkpointPath, codec).store( - envelope(Kind.APPLY_CHECKPOINT, null, history.get(10))); - new ArchiveProgressFile(participantPaths.get("account"), codec).store( - envelope(Kind.PARTICIPANT_PROGRESS, "account", history.get(10))); - new ArchiveProgressFile(participantPaths.get("account-asset"), codec).store( - envelope(Kind.PARTICIPANT_PROGRESS, "account-asset", history.get(8))); - Path readerVisiblePath = directory.resolve("progress/reader-visible.progress"); - new ArchiveProgressFile(readerVisiblePath, codec).store( - envelope(Kind.READER_VISIBLE, null, history.get(8))); - ArchiveRecoveryAuthorityScanner scanner = new ArchiveRecoveryAuthorityScanner(history, - checkpointPath, participantPaths, readerVisiblePath, PARTICIPANTS); - return new Fixture(history, scanner, checkpointPath, participantPaths, readerVisiblePath); - } - - private static ArchiveProgressEnvelope envelope(Kind kind, String participant, - HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); - } - - private static HistoryCommitMarker marker(long epoch) { - BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, bytes(32, (int) epoch), - bytes(32, (int) epoch - 1), epoch * 1_000); - HistoryLocation body = new HistoryLocation(0, epoch * 100, 100, (int) epoch, - bytes(32, (int) epoch + 20)); - HistoryIndexLocation index = new HistoryIndexLocation(epoch * 50, 50, - bytes(32, (int) epoch + 30)); - return new HistoryCommitMarker(meta, epoch - 1, body, index, - bytes(16, (int) epoch + 40), PARTICIPANTS); - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } - - private static final class RecordingStorage implements RecoveryStorage { - private final ArchiveRecoveryAuthorityScanner scanner; - private final List actions = new ArrayList<>(); - - private RecordingStorage(ArchiveRecoveryAuthorityScanner scanner) { - this.scanner = scanner; - } - - @Override - public RecoverySnapshot scan() throws IOException { - return scanner.scan(); - } - - @Override - public void truncateHistoryAndSync(long historyHead) { - actions.add("truncate:" + historyHead); - } - - @Override - public void replayParticipantAndSyncProgress(String participant, long firstEpoch, - long lastEpoch) { - actions.add("replay:" + participant + ":" + firstEpoch + "-" + lastEpoch); - } - - @Override - public void publishReaderHeadAndSync(long readerVisibleHead) { - actions.add("publish:" + readerVisibleHead); - } - } - - private static final class DurableStorage implements RecoveryStorage { - private final Fixture fixture; - private final ArchiveReaderHeadPublisher publisher; - private final List actions = new ArrayList<>(); - - private DurableStorage(Fixture fixture, ArchiveReaderHeadPublisher publisher) { - this.fixture = fixture; - this.publisher = publisher; - } - - @Override - public RecoverySnapshot scan() throws IOException { - return fixture.scanner.scan(); - } - - @Override - public void truncateHistoryAndSync(long historyHead) throws IOException { - HistoryCommitMarker head = fixture.history.head(); - while (head != null && head.getMeta().getEpoch() > historyHead) { - fixture.history.removeHead(head.getMeta()); - head = fixture.history.head(); - } - actions.add("truncate:" + historyHead); - } - - @Override - public void replayParticipantAndSyncProgress(String participant, long firstEpoch, - long lastEpoch) throws IOException { - new ArchiveProgressFile(fixture.participantPaths.get(participant), - new ArchiveProgressEnvelopeCodec()).store(envelope( - Kind.PARTICIPANT_PROGRESS, participant, fixture.history.get(lastEpoch))); - actions.add("replay:" + participant + ":" + firstEpoch + "-" + lastEpoch); - } - - @Override - public void publishReaderHeadAndSync(long readerVisibleHead) throws IOException { - publisher.publish(readerVisibleHead); - actions.add("publish:" + readerVisibleHead); - } - } - - private static final class Fixture implements AutoCloseable { - private final HistoryCommitStore history; - private final ArchiveRecoveryAuthorityScanner scanner; - private final Path checkpointPath; - private final Map participantPaths; - private final Path readerVisiblePath; - - private Fixture(HistoryCommitStore history, ArchiveRecoveryAuthorityScanner scanner, - Path checkpointPath, Map participantPaths, Path readerVisiblePath) { - this.history = history; - this.scanner = scanner; - this.checkpointPath = checkpointPath; - this.participantPaths = participantPaths; - this.readerVisiblePath = readerVisiblePath; - } - - @Override - public void close() throws IOException { - history.close(); - } - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryExecutorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryExecutorTest.java deleted file mode 100644 index 7507b2fa3a0..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryExecutorTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoveryStorage; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; - -public class ArchiveRecoveryExecutorTest { - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void secondRestartReadsDurableProgressAndExecutesOnlyRemainingParticipant() - throws Exception { - Path directory = temporaryFolder.newFolder("second-crash").toPath(); - List participants = Arrays.asList("account", "account-asset", "storage-row"); - DurableTestStorage.initialize(directory, 12, 10, 7, - heads("account", 10L, "account-asset", 8L, "storage-row", 7L)); - - DurableTestStorage firstStorage = new DurableTestStorage(directory, participants); - ArchiveRecoveryExecutor first = new ArchiveRecoveryExecutor(firstStorage, action -> { - if (action.getType() == ActionType.REPLAY_PARTICIPANT - && "account-asset".equals(action.getParticipant())) { - throw new IOException("injected crash after participant progress force"); - } - }); - assertThrows(ArchivePersistenceException.class, first::recover); - assertEquals(Arrays.asList("account-asset:9-10"), firstStorage.getReplays()); - - RecoverySnapshot afterCrash = new DurableTestStorage(directory, participants).scan(); - assertEquals(10, afterCrash.getHistoryHead()); - assertEquals(10, afterCrash.getParticipantHeads().get("account-asset").longValue()); - assertEquals(7, afterCrash.getParticipantHeads().get("storage-row").longValue()); - assertEquals(7, afterCrash.getReaderVisibleHead()); - - DurableTestStorage secondStorage = new DurableTestStorage(directory, participants); - new ArchiveRecoveryExecutor(secondStorage).recover(); - assertEquals(Arrays.asList("storage-row:8-10"), secondStorage.getReplays()); - - RecoverySnapshot recovered = new DurableTestStorage(directory, participants).scan(); - assertEquals(10, recovered.getHistoryHead()); - assertEquals(10, recovered.getCheckpointHead()); - assertEquals(10, recovered.getReaderVisibleHead()); - recovered.getParticipantHeads().values().forEach(head -> assertEquals(10, head.longValue())); - - DurableTestStorage thirdStorage = new DurableTestStorage(directory, participants); - assertEquals(0, new ArchiveRecoveryExecutor(thirdStorage).recover().getActions().size()); - assertEquals(0, thirdStorage.getReplays().size()); - } - - private static Map heads(Object... values) { - Map heads = new LinkedHashMap<>(); - for (int index = 0; index < values.length; index += 2) { - heads.put((String) values[index], (Long) values[index + 1]); - } - return heads; - } - - private static final class DurableTestStorage implements RecoveryStorage { - private static final String HISTORY = "history.head"; - private static final String CHECKPOINT = "checkpoint.head"; - private static final String READER = "reader.head"; - - private final Path directory; - private final List participants; - private final List replays = new ArrayList<>(); - - private DurableTestStorage(Path directory, List participants) { - this.directory = directory; - this.participants = new ArrayList<>(participants); - } - - private static void initialize(Path directory, long historyHead, long checkpointHead, - long readerHead, Map participantHeads) throws IOException { - Files.createDirectories(directory); - writeLong(directory.resolve(HISTORY), historyHead); - writeLong(directory.resolve(CHECKPOINT), checkpointHead); - writeLong(directory.resolve(READER), readerHead); - for (Map.Entry entry : participantHeads.entrySet()) { - writeLong(participantPath(directory, entry.getKey()), entry.getValue()); - } - } - - @Override - public RecoverySnapshot scan() throws IOException { - Map participantHeads = new LinkedHashMap<>(); - for (String participant : participants) { - participantHeads.put(participant, readLong(participantPath(directory, participant))); - } - return new RecoverySnapshot(readLong(directory.resolve(HISTORY)), - readLong(directory.resolve(CHECKPOINT)), participantHeads, - readLong(directory.resolve(READER))); - } - - @Override - public void truncateHistoryAndSync(long historyHead) throws IOException { - writeLong(directory.resolve(HISTORY), historyHead); - } - - @Override - public void replayParticipantAndSyncProgress(String participant, long firstEpoch, - long lastEpoch) throws IOException { - replays.add(participant + ":" + firstEpoch + "-" + lastEpoch); - writeLong(participantPath(directory, participant), lastEpoch); - } - - @Override - public void publishReaderHeadAndSync(long readerVisibleHead) throws IOException { - writeLong(directory.resolve(READER), readerVisibleHead); - } - - private List getReplays() { - return replays; - } - - private static Path participantPath(Path directory, String participant) { - return directory.resolve("participant-" + participant + ".head"); - } - - private static long readLong(Path path) throws IOException { - byte[] encoded = Files.readAllBytes(path); - if (encoded.length != Long.BYTES) { - throw new IOException("Invalid durable test progress length"); - } - return ByteBuffer.wrap(encoded).getLong(); - } - - private static void writeLong(Path path, long value) throws IOException { - Path temporary = path.resolveSibling(path.getFileName() + ".tmp"); - try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { - ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES).putLong(value); - buffer.flip(); - while (buffer.hasRemaining()) { - channel.write(buffer); - } - channel.force(true); - } - Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE, - StandardCopyOption.REPLACE_EXISTING); - HistorySegmentStore.syncDirectory(path.getParent()); - } - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryPlannerTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryPlannerTest.java deleted file mode 100644 index e82adadbed1..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryPlannerTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.Test; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryAction; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; - -public class ArchiveRecoveryPlannerTest { - - @Test - public void resumesOnlyRemainingParticipantAfterASecondCrash() { - Map firstHeads = heads("storage-row", 7L, "account", 10L, - "account-asset", 8L); - RecoveryPlan first = ArchiveRecoveryPlanner.plan(12, 10, firstHeads, 7); - - assertEquals(7, first.getSafeHeadBeforeRecovery()); - assertActions(first.getActions(), - action(ActionType.TRUNCATE_HISTORY, null, 10, 10), - action(ActionType.REPLAY_PARTICIPANT, "account-asset", 9, 10), - action(ActionType.REPLAY_PARTICIPANT, "storage-row", 8, 10), - action(ActionType.PUBLISH_READER_HEAD, null, 10, 10)); - - // The process crashes after truncating H and durably advancing only account-asset D[i]. - Map secondHeads = heads("storage-row", 7L, "account", 10L, - "account-asset", 10L); - RecoveryPlan second = ArchiveRecoveryPlanner.plan(10, 10, secondHeads, 7); - - assertActions(second.getActions(), - action(ActionType.REPLAY_PARTICIPANT, "storage-row", 8, 10), - action(ActionType.PUBLISH_READER_HEAD, null, 10, 10)); - - Map recoveredHeads = heads("storage-row", 10L, "account", 10L, - "account-asset", 10L); - RecoveryPlan recovered = ArchiveRecoveryPlanner.plan(10, 10, recoveredHeads, 10); - assertEquals(10, recovered.getSafeHeadBeforeRecovery()); - assertEquals(0, recovered.getActions().size()); - } - - @Test - public void chunksEveryParticipantReplayRange() { - RecoveryPlan plan = ArchiveRecoveryPlanner.plan(2_050, 2_050, - heads("account", 0L), 0); - - assertActions(plan.getActions(), - action(ActionType.REPLAY_PARTICIPANT, "account", 1, 1_024), - action(ActionType.REPLAY_PARTICIPANT, "account", 1_025, 2_048), - action(ActionType.REPLAY_PARTICIPANT, "account", 2_049, 2_050), - action(ActionType.PUBLISH_READER_HEAD, null, 2_050, 2_050)); - } - - @Test - public void rejectsEveryAheadOrUnsafeStateBeforePlanningActions() { - assertThrows(ArchivePersistenceException.class, - () -> ArchiveRecoveryPlanner.plan(9, 10, heads("account", 9L), 9)); - assertThrows(ArchivePersistenceException.class, - () -> ArchiveRecoveryPlanner.plan(10, 10, heads("account", 11L), 10)); - assertThrows(ArchivePersistenceException.class, - () -> ArchiveRecoveryPlanner.plan(10, 10, heads("account", 8L), 9)); - assertThrows(ArchivePersistenceException.class, - () -> ArchiveRecoveryPlanner.plan(10, 10, java.util.Collections.emptyMap(), 10)); - } - - private static Map heads(Object... values) { - Map heads = new LinkedHashMap<>(); - for (int index = 0; index < values.length; index += 2) { - heads.put((String) values[index], (Long) values[index + 1]); - } - return heads; - } - - private static ExpectedAction action(ActionType type, String participant, long first, - long last) { - return new ExpectedAction(type, participant, first, last); - } - - private static void assertActions(List actual, ExpectedAction... expected) { - assertEquals(expected.length, actual.size()); - for (int index = 0; index < expected.length; index++) { - ExpectedAction left = expected[index]; - RecoveryAction right = actual.get(index); - assertEquals(left.type, right.getType()); - assertEquals(left.participant, right.getParticipant()); - assertEquals(left.firstEpoch, right.getFirstEpoch()); - assertEquals(left.lastEpoch, right.getLastEpoch()); - } - } - - private static final class ExpectedAction { - private final ActionType type; - private final String participant; - private final long firstEpoch; - private final long lastEpoch; - - private ExpectedAction(ActionType type, String participant, long firstEpoch, long lastEpoch) { - this.type = type; - this.participant = participant; - this.firstEpoch = firstEpoch; - this.lastEpoch = lastEpoch; - } - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryScannerTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryScannerTest.java deleted file mode 100644 index 58cc50fd2c7..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveRecoveryScannerTest.java +++ /dev/null @@ -1,224 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.Test; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.ArchiveRecoveryExecutor.RecoverySnapshot; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryAction; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; - -public class ArchiveRecoveryScannerTest { - - private static final List PARTICIPANTS = Arrays.asList("account", "account-asset"); - - @Test - public void resolvesLaggingParticipantAgainstItsOwnCommittedEpoch() throws Exception { - TestHistory history = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); - TestProgress progress = progress(envelope(Kind.APPLY_CHECKPOINT, null, - history.markers.get(10L)), envelope(Kind.READER_VISIBLE, null, - history.markers.get(8L))); - progress.participantProgress.put("account", - envelope(Kind.PARTICIPANT_PROGRESS, "account", history.markers.get(10L))); - progress.participantProgress.put("account-asset", - envelope(Kind.PARTICIPANT_PROGRESS, "account-asset", history.markers.get(8L))); - - RecoverySnapshot snapshot = scanner(history, progress).scan(); - assertEquals(12, snapshot.getHistoryHead()); - assertEquals(10, snapshot.getCheckpointHead()); - assertEquals(Long.valueOf(10), snapshot.getParticipantHeads().get("account")); - assertEquals(Long.valueOf(8), snapshot.getParticipantHeads().get("account-asset")); - assertEquals(8, snapshot.getReaderVisibleHead()); - - RecoveryPlan plan = ArchiveRecoveryPlanner.plan(snapshot.getHistoryHead(), - snapshot.getCheckpointHead(), snapshot.getParticipantHeads(), - snapshot.getReaderVisibleHead()); - assertAction(plan.getActions().get(0), ActionType.TRUNCATE_HISTORY, null, 10, 10); - assertAction(plan.getActions().get(1), ActionType.REPLAY_PARTICIPANT, - "account-asset", 9, 10); - assertAction(plan.getActions().get(2), ActionType.PUBLISH_READER_HEAD, null, 10, 10); - } - - @Test - public void rejectsMissingHistoryAndEveryProgressSourceGap() { - TestHistory missingHistory = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); - TestProgress valid = validProgress(missingHistory); - missingHistory.markers.remove(8L); - assertThrows(ArchivePersistenceException.class, - () -> scanner(missingHistory, valid).scan()); - - TestHistory completeHistory = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); - TestProgress missingCheckpoint = validProgress(completeHistory); - missingCheckpoint.checkpoint = null; - assertThrows(ArchivePersistenceException.class, - () -> scanner(completeHistory, missingCheckpoint).scan()); - - TestProgress missingParticipant = validProgress(completeHistory); - missingParticipant.participantProgress.remove("account-asset"); - assertThrows(ArchivePersistenceException.class, - () -> scanner(completeHistory, missingParticipant).scan()); - - TestProgress unexpectedParticipant = validProgress(completeHistory); - unexpectedParticipant.participantProgress.put("storage-row", - unexpectedParticipant.participantProgress.get("account")); - assertThrows(ArchivePersistenceException.class, - () -> scanner(completeHistory, unexpectedParticipant).scan()); - - TestProgress missingReader = validProgress(completeHistory); - missingReader.readerVisible = null; - assertThrows(ArchivePersistenceException.class, - () -> scanner(completeHistory, missingReader).scan()); - } - - @Test - public void rejectsEveryEnvelopeIdentityMismatch() { - TestHistory history = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); - HistoryCommitMarker marker = history.markers.get(8L); - List mismatches = Arrays.asList( - new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, "account-asset", 8, - bytes(32, 90), marker.getBatchId(), marker.getHistoryLocation().getBodyDigest(), - PARTICIPANTS), - new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, "account-asset", 8, - marker.getMeta().getBlockHash(), bytes(16, 91), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS), - new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, "account-asset", 8, - marker.getMeta().getBlockHash(), marker.getBatchId(), bytes(32, 92), PARTICIPANTS), - new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, "account", 8, - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS), - new ArchiveProgressEnvelope(Kind.APPLY_CHECKPOINT, null, 8, - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS), - new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, "account-asset", 8, - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), - Arrays.asList("account", "account-asset", "storage-row"))); - - for (ArchiveProgressEnvelope mismatch : mismatches) { - TestProgress progress = validProgress(history); - progress.participantProgress.put("account-asset", mismatch); - assertThrows(ArchivePersistenceException.class, () -> scanner(history, progress).scan()); - } - } - - @Test - public void rejectsCommittedMarkerEpochOrParticipantSetMismatch() { - TestHistory wrongEpoch = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); - TestProgress epochProgress = validProgress(wrongEpoch); - wrongEpoch.markers.put(8L, marker(7, PARTICIPANTS)); - assertThrows(ArchivePersistenceException.class, - () -> scanner(wrongEpoch, epochProgress).scan()); - - TestHistory wrongSet = history(marker(8, PARTICIPANTS), marker(10, PARTICIPANTS)); - TestProgress setProgress = validProgress(wrongSet); - wrongSet.markers.put(8L, - marker(8, Arrays.asList("account", "account-asset", "storage-row"))); - assertThrows(ArchivePersistenceException.class, - () -> scanner(wrongSet, setProgress).scan()); - } - - private static ArchiveRecoveryScanner scanner(TestHistory history, TestProgress progress) { - return new ArchiveRecoveryScanner(history, progress, PARTICIPANTS); - } - - private static TestProgress validProgress(TestHistory history) { - TestProgress progress = progress(envelope(Kind.APPLY_CHECKPOINT, null, - history.markers.get(10L)), envelope(Kind.READER_VISIBLE, null, - history.markers.get(8L))); - progress.participantProgress.put("account", - envelope(Kind.PARTICIPANT_PROGRESS, "account", history.markers.get(10L))); - progress.participantProgress.put("account-asset", - envelope(Kind.PARTICIPANT_PROGRESS, "account-asset", history.markers.get(8L))); - return progress; - } - - private static TestHistory history(HistoryCommitMarker... markers) { - TestHistory history = new TestHistory(); - for (HistoryCommitMarker marker : markers) { - history.markers.put(marker.getMeta().getEpoch(), marker); - } - return history; - } - - private static TestProgress progress(ArchiveProgressEnvelope checkpoint, - ArchiveProgressEnvelope readerVisible) { - TestProgress progress = new TestProgress(); - progress.checkpoint = checkpoint; - progress.readerVisible = readerVisible; - return progress; - } - - private static ArchiveProgressEnvelope envelope(Kind kind, String participant, - HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(kind, participant, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); - } - - private static HistoryCommitMarker marker(long epoch, List participants) { - BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, bytes(32, (int) epoch), - bytes(32, (int) epoch - 1), epoch * 1_000); - HistoryLocation body = new HistoryLocation(0, epoch * 100, 100, (int) epoch, - bytes(32, (int) epoch + 20)); - HistoryIndexLocation index = new HistoryIndexLocation(epoch * 50, 50, - bytes(32, (int) epoch + 30)); - return new HistoryCommitMarker(meta, epoch - 1, body, index, - bytes(16, (int) epoch + 40), participants); - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } - - private static void assertAction(RecoveryAction action, ActionType type, String participant, - long firstEpoch, long lastEpoch) { - assertEquals(type, action.getType()); - assertEquals(participant, action.getParticipant()); - assertEquals(firstEpoch, action.getFirstEpoch()); - assertEquals(lastEpoch, action.getLastEpoch()); - } - - private static final class TestHistory implements ArchiveRecoveryScanner.HistoryIdentitySource { - private final Map markers = new LinkedHashMap<>(); - - @Override - public long committedHeadEpoch() { - return 12; - } - - @Override - public HistoryCommitMarker committedMarker(long epoch) { - return markers.get(epoch); - } - } - - private static final class TestProgress implements ArchiveRecoveryScanner.ProgressIdentitySource { - private ArchiveProgressEnvelope checkpoint; - private final Map participantProgress = - new LinkedHashMap<>(); - private ArchiveProgressEnvelope readerVisible; - - @Override - public ArchiveProgressEnvelope loadCheckpoint() { - return checkpoint; - } - - @Override - public Map loadParticipantProgress() { - return participantProgress; - } - - @Override - public ArchiveProgressEnvelope loadReaderVisible() { - return readerVisible; - } - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java deleted file mode 100644 index 5d90202d149..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetApplyCoordinatorTest.java +++ /dev/null @@ -1,566 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -import com.google.protobuf.ByteString; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.ActionType; -import org.tron.core.db2.archive.ArchiveRecoveryPlanner.RecoveryPlan; -import org.tron.core.db2.archive.ArchiveTargetApplyCoordinator.Stage; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; -import org.tron.protos.Protocol.Account; - -public class ArchiveTargetApplyCoordinatorTest { - - private static final List PARTICIPANTS = - Arrays.asList("account", "account-asset"); - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void appliesCheckpointParticipantsRefreshAndReaderInOrder() throws Exception { - try (Fixture fixture = fixture("normal")) { - AtomicBoolean insideBarrier = new AtomicBoolean(); - ArchiveStateBarrier barrier = action -> { - assertTrue(insideBarrier.compareAndSet(false, true)); - try { - action.run(); - } finally { - insideBarrier.set(false); - } - }; - try (HistoryCommitStore history = fixture.openHistory()) { - ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, barrier); - coordinator.apply(1, Phase.P66_ON, plans(), () -> { - assertTrue(insideBarrier.get()); - assertEquals(1, fixture.account.loadProgress().getEpoch()); - assertEquals(1, fixture.asset.loadProgress().getEpoch()); - }); - } - - ArchiveProgressEnvelope checkpoint = fixture.checkpoint(); - ArchiveProgressEnvelope reader = fixture.reader(); - byte[] mutationPlanDigest = checkpoint.getMutationPlanDigest(); - assertEquals(1, checkpoint.getEpoch()); - assertEquals(1, reader.getEpoch()); - assertTrue(mutationPlanDigest != null); - assertArrayEquals(mutationPlanDigest, - fixture.account.loadProgress().getMutationPlanDigest()); - assertArrayEquals(mutationPlanDigest, - fixture.asset.loadProgress().getMutationPlanDigest()); - assertArrayEquals(mutationPlanDigest, reader.getMutationPlanDigest()); - assertArrayEquals(bytes("account"), fixture.account.get(bytes("normal"))); - assertArrayEquals(bytes("account-asset"), fixture.asset.get(bytes("normal"))); - assertFalse(Files.exists( - new ArchiveTargetMutationPlanFile(fixture.checkpointPath).getPath())); - } - } - - @Test - public void everyDurableStageFailureConvergesThroughFreshRecovery() throws Exception { - for (FailurePoint point : FailurePoint.values()) { - try (Fixture fixture = fixture(point.name().toLowerCase())) { - AtomicInteger refreshes = new AtomicInteger(); - try (HistoryCommitStore history = fixture.openHistory()) { - ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, - action -> action.run(), - (stage, participant) -> failAfterStage(point, stage, participant), temporary -> { - if (point == FailurePoint.DURING_PUBLICATION) { - throw new IOException("injected during publication"); - } - }, (stage, path) -> failPlanStage(point, stage)); - assertThrows(IOException.class, () -> coordinator.apply(1, Phase.P66_ON, plans(), () -> { - refreshes.incrementAndGet(); - if (point == FailurePoint.DURING_REFRESH) { - throw new IOException("injected during refresh"); - } - })); - } - - try (ArchiveParticipantRecoveryStorage recovery = - new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, - action -> action.run(), refreshes::incrementAndGet)) { - new ArchiveRecoveryExecutor(recovery).recover(); - } - - long expected = point.isPlanFailure() ? 0 : 1; - assertEquals(expected, fixture.checkpoint().getEpoch()); - assertEquals(expected, fixture.account.loadProgress().getEpoch()); - assertEquals(expected, fixture.asset.loadProgress().getEpoch()); - assertEquals(expected, fixture.reader().getEpoch()); - assertEquals(point.isPlanFailure() ? 0 : 1, - Math.min(refreshes.get(), 1)); - assertFalse(Files.exists( - new ArchiveTargetMutationPlanFile(fixture.checkpointPath).getPath())); - - try (ArchiveParticipantRecoveryStorage fixed = - new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { - assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); - } - } - } - } - - @Test - public void p66PlansRecoverOnlyRemainingNativeParticipantAfterFreshReopen() throws Exception { - for (Phase phase : Arrays.asList(Phase.P66_ACTIVATION, Phase.P66_ON)) { - for (boolean failAfterAccount : Arrays.asList(false, true)) { - String boundary = failAfterAccount ? "after-account" : "after-checkpoint"; - try (Fixture fixture = fixture("p66-" + phase.name().toLowerCase() + "-" + boundary)) { - byte[] address = accountAddress(7); - byte[] accountValue = canonicalAccount(address, - phase == Phase.P66_ACTIVATION ? 2_000L : 3_000L); - byte[] assetKey = new P66AccountAssetCodec().assetPhysicalKey(address, "1000007"); - byte[] assetValue = ByteBuffer.allocate(Long.BYTES) - .putLong(phase == Phase.P66_ACTIVATION ? 30L : 40L).array(); - Map> mutations = - p66Mutations(address, accountValue, assetKey, assetValue); - ArchiveTargetApplyCoordinator.FaultHook failure = (stage, participant) -> { - if (!failAfterAccount && stage == Stage.AFTER_CHECKPOINT - || failAfterAccount && stage == Stage.AFTER_PARTICIPANT - && "account".equals(participant)) { - throw new IOException("injected " + boundary); - } - }; - - try (HistoryCommitStore history = fixture.openHistory()) { - ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, - action -> action.run(), failure, temporary -> { }); - assertThrows(IOException.class, - () -> coordinator.apply(1, phase, mutations, () -> { })); - } - - ArchiveTargetMutationPlan durablePlan = - new ArchiveTargetMutationPlanFile(fixture.checkpointPath).loadRequired(); - byte[] planDigest = durablePlan.digest(); - assertEquals(phase, durablePlan.getTargetPhase()); - assertArrayEquals(hash(1), durablePlan.getTarget().getBlockHash()); - assertArrayEquals(planDigest, fixture.checkpoint().getMutationPlanDigest()); - - fixture.reopenParticipants(); - AtomicInteger refreshes = new AtomicInteger(); - RecoveryPlan recoveryPlan; - try (ArchiveParticipantRecoveryStorage recovery = - new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, - action -> action.run(), refreshes::incrementAndGet)) { - recoveryPlan = new ArchiveRecoveryExecutor(recovery).recover(); - } - - List replayed = new ArrayList<>(); - recoveryPlan.getActions().stream() - .filter(action -> action.getType() == ActionType.REPLAY_PARTICIPANT) - .forEach(action -> replayed.add(action.getParticipant())); - assertEquals(failAfterAccount - ? Collections.singletonList("account-asset") : PARTICIPANTS, replayed); - assertEquals(ActionType.PUBLISH_READER_HEAD, - recoveryPlan.getActions().get(recoveryPlan.getActions().size() - 1).getType()); - assertEquals(1, refreshes.get()); - assertArrayEquals(accountValue, fixture.account.get(address)); - assertArrayEquals(assetValue, fixture.asset.get(assetKey)); - assertP66Authority(fixture, planDigest); - assertFalse(Files.exists( - new ArchiveTargetMutationPlanFile(fixture.checkpointPath).getPath())); - - fixture.reopenParticipants(); - try (ArchiveParticipantRecoveryStorage fixed = - new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { - assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); - } - assertArrayEquals(accountValue, fixture.account.get(address)); - assertArrayEquals(assetValue, fixture.asset.get(assetKey)); - assertP66Authority(fixture, planDigest); - } - } - } - } - - @Test - public void p66ReaderDurableCrashReopenRetiresPlanWithoutBusinessReplay() throws Exception { - for (Phase phase : Arrays.asList(Phase.P66_ACTIVATION, Phase.P66_ON)) { - try (Fixture fixture = fixture("p66-reader-durable-" + phase.name().toLowerCase())) { - P66Vector vector = p66Vector(phase); - try (HistoryCommitStore history = fixture.openHistory()) { - ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, - action -> action.run(), failAt(Stage.AFTER_READER, - "injected after durable reader publication"), temporary -> { }); - assertThrows(IOException.class, - () -> coordinator.apply(1, phase, vector.mutations, () -> { })); - } - - ArchiveTargetMutationPlan durablePlan = - new ArchiveTargetMutationPlanFile(fixture.checkpointPath).loadRequired(); - byte[] planDigest = durablePlan.digest(); - assertEquals(phase, durablePlan.getTargetPhase()); - assertP66BusinessAndAuthority(fixture, vector, planDigest); - - fixture.reopenParticipants(); - AtomicInteger refreshes = new AtomicInteger(); - try (ArchiveParticipantRecoveryStorage recovery = - new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, - action -> action.run(), refreshes::incrementAndGet)) { - assertEquals(0, new ArchiveRecoveryExecutor(recovery).recover().getActions().size()); - } - assertEquals(0, refreshes.get()); - assertFalse(Files.exists( - new ArchiveTargetMutationPlanFile(fixture.checkpointPath).getPath())); - assertP66BusinessAndAuthority(fixture, vector, planDigest); - - fixture.reopenParticipants(); - try (ArchiveParticipantRecoveryStorage fixed = - new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { - assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); - } - assertP66BusinessAndAuthority(fixture, vector, planDigest); - } - } - } - - @Test - public void p66RecoveryCrashReopenReplaysOnlySecondNativeParticipant() throws Exception { - for (Phase phase : Arrays.asList(Phase.P66_ACTIVATION, Phase.P66_ON)) { - try (Fixture fixture = fixture("p66-recovery-crash-" + phase.name().toLowerCase())) { - P66Vector vector = p66Vector(phase); - try (HistoryCommitStore history = fixture.openHistory()) { - ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS, - action -> action.run(), failAt(Stage.AFTER_CHECKPOINT, - "injected after checkpoint"), temporary -> { }); - assertThrows(IOException.class, - () -> coordinator.apply(1, phase, vector.mutations, () -> { })); - } - ArchiveTargetMutationPlan durablePlan = - new ArchiveTargetMutationPlanFile(fixture.checkpointPath).loadRequired(); - byte[] planDigest = durablePlan.digest(); - assertEquals(phase, durablePlan.getTargetPhase()); - - fixture.reopenParticipants(); - try (ArchiveParticipantRecoveryStorage recovery = - new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveRecoveryExecutor(recovery, action -> { - if (action.getType() == ActionType.REPLAY_PARTICIPANT - && "account".equals(action.getParticipant())) { - throw new IOException("injected after recovered account"); - } - }).recover()); - } - assertArrayEquals(vector.accountValue, fixture.account.get(vector.address)); - assertNull(fixture.asset.get(vector.assetKey)); - assertEquals(1L, fixture.account.loadProgress().getEpoch()); - assertEquals(0L, fixture.asset.loadProgress().getEpoch()); - assertEquals(0L, fixture.reader().getEpoch()); - assertArrayEquals(planDigest, - fixture.account.loadProgress().getMutationPlanDigest()); - - fixture.reopenParticipants(); - RecoveryPlan recoveryPlan; - try (ArchiveParticipantRecoveryStorage recovery = - new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { - recoveryPlan = new ArchiveRecoveryExecutor(recovery).recover(); - } - assertEquals(2, recoveryPlan.getActions().size()); - assertEquals(ActionType.REPLAY_PARTICIPANT, - recoveryPlan.getActions().get(0).getType()); - assertEquals("account-asset", recoveryPlan.getActions().get(0).getParticipant()); - assertEquals(ActionType.PUBLISH_READER_HEAD, - recoveryPlan.getActions().get(1).getType()); - assertP66BusinessAndAuthority(fixture, vector, planDigest); - assertFalse(Files.exists( - new ArchiveTargetMutationPlanFile(fixture.checkpointPath).getPath())); - - fixture.reopenParticipants(); - try (ArchiveParticipantRecoveryStorage fixed = - new ArchiveParticipantRecoveryStorage(fixture.archive, 4096, - fixture.checkpointPath, fixture.engines(), fixture.readerPath, PARTICIPANTS)) { - assertEquals(0, new ArchiveRecoveryExecutor(fixed).recover().getActions().size()); - } - assertP66BusinessAndAuthority(fixture, vector, planDigest); - } - } - } - - private Fixture fixture(String name) throws Exception { - return new Fixture(temporaryFolder.newFolder(name).toPath()); - } - - private static Map> plans() { - Map> plans = new LinkedHashMap<>(); - plans.put("account", Collections.singletonList( - ArchiveParticipantMutation.put(bytes("normal"), bytes("account")))); - plans.put("account-asset", Collections.singletonList( - ArchiveParticipantMutation.put(bytes("normal"), bytes("account-asset")))); - return plans; - } - - private static Map> p66Mutations(byte[] address, - byte[] accountValue, byte[] assetKey, byte[] assetValue) { - Map> mutations = new LinkedHashMap<>(); - mutations.put("account", Collections.singletonList( - ArchiveParticipantMutation.put(address, accountValue))); - mutations.put("account-asset", Collections.singletonList( - ArchiveParticipantMutation.put(assetKey, assetValue))); - return mutations; - } - - private static byte[] accountAddress(int suffix) { - byte[] address = new byte[21]; - address[0] = 0x41; - address[20] = (byte) suffix; - return address; - } - - private static byte[] canonicalAccount(byte[] address, long balance) { - return Account.newBuilder().setAddress(ByteString.copyFrom(address)) - .setAssetOptimized(true).setBalance(balance).build().toByteArray(); - } - - private static P66Vector p66Vector(Phase phase) { - byte[] address = accountAddress(7); - byte[] accountValue = canonicalAccount(address, - phase == Phase.P66_ACTIVATION ? 2_000L : 3_000L); - byte[] assetKey = new P66AccountAssetCodec().assetPhysicalKey(address, "1000007"); - byte[] assetValue = ByteBuffer.allocate(Long.BYTES) - .putLong(phase == Phase.P66_ACTIVATION ? 30L : 40L).array(); - return new P66Vector(address, accountValue, assetKey, assetValue, - p66Mutations(address, accountValue, assetKey, assetValue)); - } - - private static void assertP66BusinessAndAuthority(Fixture fixture, P66Vector vector, - byte[] planDigest) throws IOException { - assertArrayEquals(vector.accountValue, fixture.account.get(vector.address)); - assertArrayEquals(vector.assetValue, fixture.asset.get(vector.assetKey)); - assertP66Authority(fixture, planDigest); - } - - private static void assertP66Authority(Fixture fixture, byte[] planDigest) throws IOException { - ArchiveProgressEnvelope checkpoint = fixture.checkpoint(); - ArchiveProgressEnvelope reader = fixture.reader(); - assertEquals(1L, checkpoint.getEpoch()); - assertEquals(1L, reader.getEpoch()); - assertArrayEquals(hash(1), checkpoint.getBlockHash()); - assertArrayEquals(hash(1), reader.getBlockHash()); - assertArrayEquals(planDigest, checkpoint.getMutationPlanDigest()); - assertArrayEquals(planDigest, fixture.account.loadProgress().getMutationPlanDigest()); - assertArrayEquals(planDigest, fixture.asset.loadProgress().getMutationPlanDigest()); - assertArrayEquals(planDigest, reader.getMutationPlanDigest()); - } - - private static ArchiveTargetApplyCoordinator.FaultHook failAt(Stage expected, - String message) { - return (stage, participant) -> { - if (stage == expected) { - throw new IOException(message); - } - }; - } - - private static void failAfterStage(FailurePoint point, Stage stage, String participant) - throws IOException { - if (point == FailurePoint.AFTER_CHECKPOINT && stage == Stage.AFTER_CHECKPOINT - || point == FailurePoint.AFTER_FIRST_PARTICIPANT - && stage == Stage.AFTER_PARTICIPANT && "account".equals(participant) - || point == FailurePoint.AFTER_READER && stage == Stage.AFTER_READER) { - throw new IOException("injected at " + point); - } - } - - private static void failPlanStage(FailurePoint point, - ArchiveTargetMutationPlanFile.Stage stage) throws IOException { - if (point == FailurePoint.AFTER_PLAN_TEMPORARY_FORCE - && stage == ArchiveTargetMutationPlanFile.Stage.AFTER_TEMPORARY_FORCE - || point == FailurePoint.AFTER_PLAN_REPLACE - && stage == ArchiveTargetMutationPlanFile.Stage.AFTER_REPLACE) { - throw new IOException("injected at " + point); - } - } - - private enum FailurePoint { - AFTER_PLAN_TEMPORARY_FORCE, - AFTER_PLAN_REPLACE, - AFTER_CHECKPOINT, - AFTER_FIRST_PARTICIPANT, - DURING_REFRESH, - DURING_PUBLICATION, - AFTER_READER; - - private boolean isPlanFailure() { - return this == AFTER_PLAN_TEMPORARY_FORCE || this == AFTER_PLAN_REPLACE; - } - } - - private static final class P66Vector { - private final byte[] address; - private final byte[] accountValue; - private final byte[] assetKey; - private final byte[] assetValue; - private final Map> mutations; - - private P66Vector(byte[] address, byte[] accountValue, byte[] assetKey, byte[] assetValue, - Map> mutations) { - this.address = address; - this.accountValue = accountValue; - this.assetKey = assetKey; - this.assetValue = assetValue; - this.mutations = mutations; - } - } - - private static final class Fixture implements AutoCloseable { - private final Path archive; - private final Path checkpointPath; - private final Path readerPath; - private LevelDbArchiveParticipant account; - private RocksDbArchiveParticipant asset; - private final ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - - private Fixture(Path archive) throws Exception { - this.archive = archive; - List markers = initializeHistory(archive); - checkpointPath = archive.resolve("progress/checkpoint.progress"); - readerPath = archive.resolve("progress/reader.progress"); - new ArchiveProgressFile(checkpointPath, codec).store( - global(Kind.APPLY_CHECKPOINT, markers.get(0))); - new ArchiveProgressFile(readerPath, codec).store( - global(Kind.READER_VISIBLE, markers.get(0))); - openParticipants(); - account.apply(Collections.emptyList(), participant("account", markers.get(0))); - asset.apply(Collections.emptyList(), participant("account-asset", markers.get(0))); - } - - private void reopenParticipants() throws IOException { - closeParticipants(); - openParticipants(); - } - - private void openParticipants() throws IOException { - account = new LevelDbArchiveParticipant( - archive.resolve("participants/account"), "account", PARTICIPANTS); - try { - asset = new RocksDbArchiveParticipant( - archive.resolve("participants/account-asset"), "account-asset", PARTICIPANTS); - } catch (IOException | RuntimeException failure) { - account.close(); - throw failure; - } - } - - private void closeParticipants() throws IOException { - asset.close(); - account.close(); - } - - private HistoryCommitStore openHistory() throws IOException { - return new HistoryCommitStore(archive, new HistoryCommitMarkerCodec()); - } - - private Map engines() { - Map engines = new LinkedHashMap<>(); - engines.put("account", account); - engines.put("account-asset", asset); - return engines; - } - - private ArchiveProgressEnvelope checkpoint() throws IOException { - return new ArchiveProgressFile(checkpointPath, codec).load(); - } - - private ArchiveProgressEnvelope reader() throws IOException { - return new ArchiveProgressFile(readerPath, codec).load(); - } - - @Override - public void close() throws IOException { - closeParticipants(); - } - } - - private static List initializeHistory(Path archive) throws Exception { - List markers = new ArrayList<>(); - try (HistorySegmentStore bodies = new HistorySegmentStore( - archive, new BlockHistoryCodec(), 4096); - HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); - HistoryCommitStore commits = new HistoryCommitStore( - archive, new HistoryCommitMarkerCodec())) { - for (int epoch = 0; epoch <= 1; epoch++) { - BlockReverseDiff diff = new BlockReverseDiff( - new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L), - Collections.singletonList(new BlockReverseDiff.DbGroup("account", - Collections.singletonList(new BlockReverseDiff.Entry(bytes("key-" + epoch), - OldValue.present(bytes("old-" + epoch))))))); - HistoryLocation body = bodies.append(diff); - HistoryIndexLocation location = index.append(HistoryIndexRecord.from(diff, body)); - markers.add(new HistoryCommitMarker(diff.getMeta(), epoch - 1L, body, location, - bytes(16, epoch + 40), PARTICIPANTS)); - } - bodies.sync(); - index.sync(); - commits.commitAll(markers); - ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), - commits.getRecordLength(), commits.head(), new HistoryCommitMarkerCodec()); - } - return markers; - } - - private static ArchiveProgressEnvelope participant(String participant, - HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, participant, - marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - } - - private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - } - - private static byte[] hash(int suffix) { - byte[] hash = new byte[32]; - hash[31] = (byte) suffix; - return hash; - } - - private static byte[] bytes(String value) { - return value.getBytes(StandardCharsets.UTF_8); - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java deleted file mode 100644 index 65e4c509dd3..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTargetMutationPlanBuilderTest.java +++ /dev/null @@ -1,231 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.ArchiveParticipantMutationBatch.Mutation; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; - -public class ArchiveTargetMutationPlanBuilderTest { - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void canonicalizesExactPhysicalMutationsAndOwnsInputBytes() { - HistoryCommitMarker target = marker(1, participants()); - byte[] key = bytes(3, 3); - byte[] value = bytes(2, 7); - ArchiveParticipantMutationBatch first = new ArchiveParticipantMutationBatch(target, - Phase.P66_ON, Arrays.asList(Mutation.delete("storage-row", bytes(3, 2)), - Mutation.put("account", key, value), - Mutation.put("account", bytes(3, 1), new byte[0]))); - key[0] = 99; - value[0] = 99; - ArchiveTargetMutationPlan plan = new ArchiveTargetMutationPlanBuilder().build(target, first); - - assertArrayEquals(bytes(3, 1), plan.getMutations("account").get(0).getKey()); - assertArrayEquals(new byte[0], plan.getMutations("account").get(0).getValue()); - assertArrayEquals(bytes(3, 3), plan.getMutations("account").get(1).getKey()); - assertArrayEquals(bytes(2, 7), plan.getMutations("account").get(1).getValue()); - assertNull(plan.getMutations("storage-row").get(0).getValue()); - assertEquals(participants(), new ArrayList<>(plan.getMutations().keySet())); - - ArchiveParticipantMutationBatch reordered = new ArchiveParticipantMutationBatch(target, - Phase.P66_ON, Arrays.asList(Mutation.put("account", bytes(3, 1), new byte[0]), - Mutation.put("account", bytes(3, 3), bytes(2, 7)), - Mutation.delete("storage-row", bytes(3, 2)))); - assertArrayEquals(plan.digest(), - new ArchiveTargetMutationPlanBuilder().build(target, reordered).digest()); - } - - @Test - public void rejectsUnknownDerivedAndDuplicatePhysicalKeys() { - HistoryCommitMarker target = marker(1, participants()); - assertBuildFails(target, Collections.singletonList( - Mutation.put("unknown-db", bytes(1, 1), bytes(1, 2)))); - assertBuildFails(target, Collections.singletonList( - Mutation.delete("accountTrie", bytes(1, 1)))); - assertThrows(IllegalArgumentException.class, - () -> new ArchiveTargetMutationPlanBuilder().build(target, - new ArchiveParticipantMutationBatch(target, Phase.P66_ON, Arrays.asList( - Mutation.put("account", bytes(1, 1), bytes(1, 2)), - Mutation.delete("account", bytes(1, 1)))))); - } - - @Test - public void rejectsTargetIdentityAndExactParticipantSetMismatch() { - HistoryCommitMarker target = marker(1, participants()); - ArchiveParticipantMutationBatch batch = new ArchiveParticipantMutationBatch(target, - Phase.P66_ON, Collections.emptyList()); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveTargetMutationPlanBuilder().build(marker(2, participants()), batch)); - - List incomplete = Arrays.asList("account", "account-asset"); - HistoryCommitMarker incompleteTarget = marker(1, incomplete); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveTargetMutationPlanBuilder().build(incompleteTarget, - new ArchiveParticipantMutationBatch(incompleteTarget, Phase.P66_ON, - Collections.emptyList()))); - - List abiExcludedExact26 = new ArrayList<>(participants()); - abiExcludedExact26.remove("abi"); - HistoryCommitMarker oldTarget = marker(1, abiExcludedExact26); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveTargetMutationPlanBuilder().build(oldTarget, - new ArchiveParticipantMutationBatch(oldTarget, Phase.P66_ON, - Collections.emptyList()))); - - List v2OnlyExact25 = new ArrayList<>(participants()); - v2OnlyExact25.remove("abi"); - v2OnlyExact25.remove("asset-issue"); - HistoryCommitMarker v2OnlyTarget = marker(1, v2OnlyExact25); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveTargetMutationPlanBuilder().build(v2OnlyTarget, - new ArchiveParticipantMutationBatch(v2OnlyTarget, Phase.P66_ON, - Collections.emptyList()))); - } - - @Test - public void bindsAccountAssetFormatAndPhaseIntoPlanDigest() { - HistoryCommitMarker target = marker(1, participants()); - List mutations = Collections.singletonList( - Mutation.put("account", bytes(3, 1), bytes(2, 2))); - ArchiveTargetMutationPlan activation = new ArchiveTargetMutationPlanBuilder().build(target, - new ArchiveParticipantMutationBatch(target, Phase.P66_ACTIVATION, mutations)); - ArchiveTargetMutationPlan enabled = new ArchiveTargetMutationPlanBuilder().build(target, - new ArchiveParticipantMutationBatch(target, Phase.P66_ON, mutations)); - - assertEquals(P66AccountAssetCodec.FORMAT_ID, activation.getAccountAssetFormatId()); - assertEquals(Phase.P66_ACTIVATION, activation.getTargetPhase()); - assertFalse(Arrays.equals(activation.digest(), enabled.digest())); - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveTargetMutationPlanBuilder().build(target, - new ArchiveParticipantMutationBatch(target, "legacy-format", Phase.P66_ON, - mutations))); - } - - @Test - public void coordinatorConsumesImmutableBatchAndPublishesExactDigest() throws Exception { - Path archive = temporaryFolder.newFolder("coordinator-producer").toPath(); - List participants = participants(); - HistoryCommitMarker zero = marker(0, participants); - HistoryCommitMarker one = marker(1, participants); - Path checkpointPath = archive.resolve("progress/checkpoint.progress"); - Path readerPath = archive.resolve("progress/reader.progress"); - ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - new ArchiveProgressFile(checkpointPath, codec).store(global(Kind.APPLY_CHECKPOINT, zero)); - new ArchiveProgressFile(readerPath, codec).store(global(Kind.READER_VISIBLE, zero)); - Map recording = new LinkedHashMap<>(); - Map engines = new LinkedHashMap<>(); - for (String participant : participants) { - RecordingParticipant engine = new RecordingParticipant( - progress(participant, zero)); - recording.put(participant, engine); - engines.put(participant, engine); - } - try (HistoryCommitStore history = new HistoryCommitStore( - archive, new HistoryCommitMarkerCodec())) { - history.commitAll(Arrays.asList(zero, one)); - ArchiveTargetApplyCoordinator coordinator = new ArchiveTargetApplyCoordinator(history, - checkpointPath, engines, readerPath, participants, action -> action.run()); - coordinator.apply(new ArchiveParticipantMutationBatch(one, Phase.P66_ON, Arrays.asList( - Mutation.put("account", bytes(2, 1), new byte[0]), - Mutation.delete("storage-row", bytes(2, 2)))), () -> { }); - } - - ArchiveProgressEnvelope checkpoint = new ArchiveProgressFile(checkpointPath, codec).load(); - ArchiveProgressEnvelope reader = new ArchiveProgressFile(readerPath, codec).load(); - assertArrayEquals(checkpoint.getMutationPlanDigest(), reader.getMutationPlanDigest()); - assertArrayEquals(checkpoint.getMutationPlanDigest(), - recording.get("account").progress.getMutationPlanDigest()); - assertEquals(1, recording.get("account").mutations.size()); - assertArrayEquals(new byte[0], recording.get("account").mutations.get(0).getValue()); - assertNull(recording.get("storage-row").mutations.get(0).getValue()); - assertEquals(0, recording.get("witness").mutations.size()); - assertFalse(Files.exists(new ArchiveTargetMutationPlanFile(checkpointPath).getPath())); - } - - private static void assertBuildFails(HistoryCommitMarker target, List mutations) { - assertThrows(ArchivePersistenceException.class, - () -> new ArchiveTargetMutationPlanBuilder().build(target, - new ArchiveParticipantMutationBatch(target, Phase.P66_ON, mutations))); - } - - private static List participants() { - List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); - Collections.sort(participants); - return participants; - } - - private static HistoryCommitMarker marker(long epoch, List participants) { - BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, hash((int) epoch), - hash((int) epoch - 1), epoch * 1_000L); - return new HistoryCommitMarker(meta, epoch - 1, - new HistoryLocation(0, epoch * 100, 100, (int) epoch, - bytes(32, (int) epoch + 20)), - new HistoryIndexLocation(epoch * 50, 50, bytes(32, (int) epoch + 30)), - bytes(16, (int) epoch + 40), participants); - } - - private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); - } - - private static ArchiveProgressEnvelope progress(String participant, - HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, participant, - marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), marker.getDatabases()); - } - - private static byte[] hash(int suffix) { - byte[] value = new byte[32]; - value[31] = (byte) suffix; - return value; - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } - - private static final class RecordingParticipant implements ArchiveParticipant { - private List mutations = Collections.emptyList(); - private ArchiveProgressEnvelope progress; - - private RecordingParticipant(ArchiveProgressEnvelope progress) { - this.progress = progress; - } - - @Override - public void apply(List mutations, - ArchiveProgressEnvelope progress) { - this.mutations = new ArrayList<>(mutations); - this.progress = progress; - } - - @Override - public ArchiveProgressEnvelope loadProgress() { - return progress; - } - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java b/framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java deleted file mode 100644 index 1a74a5e540e..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/LevelDbArchiveParticipantTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.LevelDbArchiveParticipant.Stage; - -public class LevelDbArchiveParticipantTest { - - private static final List PARTICIPANTS = - Arrays.asList("account", "account-asset"); - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void nativeBatchExposesOnlyOldOldOrNewNewAcrossFailureBoundaries() throws Exception { - for (Stage failedStage : Stage.values()) { - Path directory = temporaryFolder.newFolder("native-" + failedStage).toPath(); - try (LevelDbArchiveParticipant participant = new LevelDbArchiveParticipant( - directory, "account", PARTICIPANTS)) { - participant.apply(Collections.singletonList( - ArchiveParticipantMutation.put(bytes("key"), bytes("old"))), - progress(1)); - } - - try (LevelDbArchiveParticipant failing = new LevelDbArchiveParticipant( - directory, "account", PARTICIPANTS, stage -> failAt(failedStage, stage))) { - assertThrows(IOException.class, () -> failing.apply( - Collections.singletonList( - ArchiveParticipantMutation.put(bytes("key"), bytes("new"))), progress(2))); - } - - try (LevelDbArchiveParticipant reopened = new LevelDbArchiveParticipant( - directory, "account", PARTICIPANTS)) { - long expectedEpoch = failedStage == Stage.BEFORE_WRITE ? 1 : 2; - byte[] expectedValue = failedStage == Stage.BEFORE_WRITE ? bytes("old") : bytes("new"); - assertEquals(expectedEpoch, reopened.loadProgress().getEpoch()); - assertArrayEquals(expectedValue, reopened.get(bytes("key"))); - } - } - } - - @Test - public void deleteAndProgressShareTheSameNativeBatch() throws Exception { - Path directory = temporaryFolder.newFolder("native-delete").toPath(); - try (LevelDbArchiveParticipant participant = new LevelDbArchiveParticipant( - directory, "account", PARTICIPANTS)) { - participant.apply(Collections.singletonList( - ArchiveParticipantMutation.put(bytes("key"), bytes("value"))), - progress(1)); - participant.apply(Collections.singletonList( - ArchiveParticipantMutation.delete(bytes("key"))), progress(2)); - assertNull(participant.get(bytes("key"))); - assertEquals(2, participant.loadProgress().getEpoch()); - } - } - - @Test - public void resetClearsBusinessAndProgressBeforeAConsistentReapply() throws Exception { - Path directory = temporaryFolder.newFolder("native-reset").toPath(); - try (LevelDbArchiveParticipant participant = new LevelDbArchiveParticipant( - directory, "account", PARTICIPANTS)) { - participant.apply(Collections.singletonList( - ArchiveParticipantMutation.put(bytes("key"), bytes("old"))), - progress(1)); - participant.reset(); - assertNull(participant.get(bytes("key"))); - assertThrows(ArchivePersistenceException.class, participant::loadProgress); - - participant.apply(Collections.singletonList( - ArchiveParticipantMutation.put(bytes("key"), bytes("new"))), - progress(2)); - } - - try (LevelDbArchiveParticipant reopened = new LevelDbArchiveParticipant( - directory, "account", PARTICIPANTS)) { - assertArrayEquals(bytes("new"), reopened.get(bytes("key"))); - assertEquals(2, reopened.loadProgress().getEpoch()); - } - } - - @Test - public void rejectsProgressForAnotherParticipantBeforeNativeWrite() throws Exception { - Path directory = temporaryFolder.newFolder("native-identity").toPath(); - try (LevelDbArchiveParticipant participant = new LevelDbArchiveParticipant( - directory, "account", PARTICIPANTS)) { - ArchiveProgressEnvelope wrong = new ArchiveProgressEnvelope( - ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, "account-asset", 1, - bytes(32, 1), bytes(16, 2), bytes(32, 3), PARTICIPANTS); - assertThrows(IllegalArgumentException.class, () -> participant.apply( - Collections.singletonList( - ArchiveParticipantMutation.put(bytes("key"), bytes("value"))), wrong)); - assertNull(participant.get(bytes("key"))); - assertThrows(ArchivePersistenceException.class, participant::loadProgress); - } - } - - private static ArchiveProgressEnvelope progress(long epoch) { - return new ArchiveProgressEnvelope(ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, - "account", epoch, bytes(32, (int) epoch), bytes(16, (int) epoch + 10), - bytes(32, (int) epoch + 20), PARTICIPANTS); - } - - private static void failAt(Stage failedStage, Stage currentStage) throws IOException { - if (currentStage == failedStage) { - throw new IOException("injected at " + currentStage); - } - } - - private static byte[] bytes(String value) { - return value.getBytes(StandardCharsets.UTF_8); - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java b/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java deleted file mode 100644 index e66f8dcf97d..00000000000 --- a/framework/src/test/java/org/tron/core/db2/archive/RocksDbArchiveParticipantTest.java +++ /dev/null @@ -1,167 +0,0 @@ -package org.tron.core.db2.archive; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tron.core.db2.archive.RocksDbArchiveParticipant.Stage; - -public class RocksDbArchiveParticipantTest { - - private static final List PARTICIPANTS = - Arrays.asList("account", "account-asset"); - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void nativeBatchExposesOnlyOldOldOrNewNewAcrossFailureBoundaries() throws Exception { - for (Stage failedStage : Stage.values()) { - Path directory = temporaryFolder.newFolder("native-" + failedStage).toPath(); - try (RocksDbArchiveParticipant participant = new RocksDbArchiveParticipant( - directory, "account", PARTICIPANTS)) { - participant.apply(Collections.singletonList( - ArchiveParticipantMutation.put(bytes("key"), bytes("old"))), - progress(1)); - } - - try (RocksDbArchiveParticipant failing = new RocksDbArchiveParticipant( - directory, "account", PARTICIPANTS, stage -> failAt(failedStage, stage))) { - assertThrows(IOException.class, () -> failing.apply( - Collections.singletonList( - ArchiveParticipantMutation.put(bytes("key"), bytes("new"))), progress(2))); - } - - try (RocksDbArchiveParticipant reopened = new RocksDbArchiveParticipant( - directory, "account", PARTICIPANTS)) { - long expectedEpoch = failedStage == Stage.BEFORE_WRITE ? 1 : 2; - byte[] expectedValue = failedStage == Stage.BEFORE_WRITE ? bytes("old") : bytes("new"); - assertEquals(expectedEpoch, reopened.loadProgress().getEpoch()); - assertArrayEquals(expectedValue, reopened.get(bytes("key"))); - } - } - } - - @Test - public void deleteAndProgressShareTheSameNativeBatch() throws Exception { - Path directory = temporaryFolder.newFolder("native-delete").toPath(); - try (RocksDbArchiveParticipant participant = new RocksDbArchiveParticipant( - directory, "account", PARTICIPANTS)) { - participant.apply(Collections.singletonList( - ArchiveParticipantMutation.put(bytes("key"), bytes("value"))), - progress(1)); - participant.apply(Collections.singletonList( - ArchiveParticipantMutation.delete(bytes("key"))), progress(2)); - assertNull(participant.get(bytes("key"))); - assertEquals(2, participant.loadProgress().getEpoch()); - } - } - - @Test - public void rejectsProgressForAnotherParticipantBeforeNativeWrite() throws Exception { - Path directory = temporaryFolder.newFolder("native-identity").toPath(); - try (RocksDbArchiveParticipant participant = new RocksDbArchiveParticipant( - directory, "account", PARTICIPANTS)) { - ArchiveProgressEnvelope wrong = new ArchiveProgressEnvelope( - ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, "account-asset", 1, - bytes(32, 1), bytes(16, 2), bytes(32, 3), PARTICIPANTS); - assertThrows(IllegalArgumentException.class, () -> participant.apply( - Collections.singletonList( - ArchiveParticipantMutation.put(bytes("key"), bytes("value"))), wrong)); - assertNull(participant.get(bytes("key"))); - assertThrows(ArchivePersistenceException.class, participant::loadProgress); - } - } - - @Test - public void recoveryScannerReadsParticipantProgressFromNativeEngines() throws Exception { - Path directory = temporaryFolder.newFolder("native-scanner").toPath(); - Path checkpointPath = directory.resolve("progress/checkpoint.progress"); - Path readerPath = directory.resolve("progress/reader.progress"); - try (HistoryCommitStore history = new HistoryCommitStore(directory, - new HistoryCommitMarkerCodec()); - RocksDbArchiveParticipant account = new RocksDbArchiveParticipant( - directory.resolve("account-engine"), "account", PARTICIPANTS); - RocksDbArchiveParticipant asset = new RocksDbArchiveParticipant( - directory.resolve("asset-engine"), "account-asset", PARTICIPANTS)) { - HistoryCommitMarker first = marker(1); - HistoryCommitMarker second = marker(2); - history.commitAll(Arrays.asList(first, second)); - new ArchiveProgressFile(checkpointPath, new ArchiveProgressEnvelopeCodec()) - .store(globalProgress(ArchiveProgressEnvelope.Kind.APPLY_CHECKPOINT, second)); - new ArchiveProgressFile(readerPath, new ArchiveProgressEnvelopeCodec()) - .store(globalProgress(ArchiveProgressEnvelope.Kind.READER_VISIBLE, first)); - account.apply(Collections.emptyList(), participantProgress("account", second)); - asset.apply(Collections.emptyList(), participantProgress("account-asset", first)); - Map engines = new LinkedHashMap<>(); - engines.put("account", account); - engines.put("account-asset", asset); - - ArchiveRecoveryExecutor.RecoverySnapshot snapshot = - ArchiveRecoveryAuthorityScanner.forParticipants(history, checkpointPath, - engines, readerPath, PARTICIPANTS).scan(); - assertEquals(2, snapshot.getHistoryHead()); - assertEquals(2, snapshot.getCheckpointHead()); - assertEquals(Long.valueOf(2), snapshot.getParticipantHeads().get("account")); - assertEquals(Long.valueOf(1), snapshot.getParticipantHeads().get("account-asset")); - assertEquals(1, snapshot.getReaderVisibleHead()); - } - } - - private static ArchiveProgressEnvelope progress(long epoch) { - return new ArchiveProgressEnvelope(ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, - "account", epoch, bytes(32, (int) epoch), bytes(16, (int) epoch + 10), - bytes(32, (int) epoch + 20), PARTICIPANTS); - } - - private static HistoryCommitMarker marker(long epoch) { - BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, bytes(32, (int) epoch), - bytes(32, (int) epoch - 1), epoch * 1_000); - return new HistoryCommitMarker(meta, epoch - 1, - new HistoryLocation(0, epoch * 100, 80, (int) epoch, bytes(32, (int) epoch + 20)), - new HistoryIndexLocation(epoch * 50, 50, bytes(32, (int) epoch + 30)), - bytes(16, (int) epoch + 40), PARTICIPANTS); - } - - private static ArchiveProgressEnvelope participantProgress(String participant, - HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, - participant, marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), - marker.getBatchId(), marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - } - - private static ArchiveProgressEnvelope globalProgress(ArchiveProgressEnvelope.Kind kind, - HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - } - - private static void failAt(Stage failedStage, Stage currentStage) throws IOException { - if (currentStage == failedStage) { - throw new IOException("injected at " + currentStage); - } - } - - private static byte[] bytes(String value) { - return value.getBytes(StandardCharsets.UTF_8); - } - - private static byte[] bytes(int length, int value) { - byte[] bytes = new byte[length]; - Arrays.fill(bytes, (byte) value); - return bytes; - } -} From c9b183134fa9a4b24b639652ec9bb7782905bad8 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 25 Aug 2026 10:05:12 +0800 Subject: [PATCH 060/161] test(chainbase): cover native archive lifecycle Exercise checkpoint WAL recovery, bootstrap identity, all-Store serving publication, request-owned historical reads, and native supplemental AccountAsset behavior. --- .../ArchiveAuthorityHandleSourcesTest.java | 19 +- .../archive/ArchiveBootstrapAnchorTest.java | 116 ++ .../ArchiveCheckpointFileRecoveryTest.java | 205 ++++ .../db2/archive/ArchiveHistoryWriterTest.java | 40 +- .../ArchiveParticipantDescriptorTest.java | 6 +- .../ArchiveTruncationRecoveryTest.java | 10 +- .../db2/archive/ArchiveWalBindingTest.java | 64 + .../CommittedHistoryAuthorityTest.java | 14 - ...StateGenerationCoordinatorFactoryTest.java | 23 + .../SnapshotOldValueCollectorTest.java | 655 ++--------- ...eArchiveManagerStartupIntegrationTest.java | 1042 +++++++++++++++-- .../archive/StateArchiveRuntimeOwnerTest.java | 15 +- 12 files changed, 1438 insertions(+), 771 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveBootstrapAnchorTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveCheckpointFileRecoveryTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ArchiveWalBindingTest.java diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java index 41bee711bb0..1e424c58fe8 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java @@ -23,7 +23,7 @@ public class ArchiveAuthorityHandleSourcesTest { public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void persistentAndNativeHandlesProduceReadyAndReleaseEveryPin() throws Exception { + public void persistentAndReadOnlyHandlesProduceReadyAndReleaseEveryPin() throws Exception { try (Fixture fixture = new Fixture(temporaryFolder.newFolder("ready").toPath(), false)) { ArchiveAuthoritySnapshotCollector collector = new ArchiveAuthoritySnapshotCollector( fixture.sources, fixture.sources, fixture.sources, fixture.sources); @@ -59,8 +59,6 @@ private static final class Fixture implements AutoCloseable { private final HistorySegmentStore bodies; private final HistoryIndexStore index; private final HistoryCommitStore history; - private final LevelDbArchiveParticipant level; - private final RocksDbArchiveParticipant rocks; private final PersistentServingKeyIndexCatalog catalog; private final AtomicInteger latestOpened = new AtomicInteger(); private final AtomicInteger latestClosed = new AtomicInteger(); @@ -93,22 +91,11 @@ private Fixture(Path root, boolean wrongLatestHead) throws Exception { new ArchiveProgressFile(checkpointPath, progressCodec).store(checkpoint); new ArchiveProgressFile(readerPath, progressCodec).store(reader); - level = new LevelDbArchiveParticipant(root.resolve("level-abi"), "abi", participants); - rocks = new RocksDbArchiveParticipant(root.resolve("rocks-account"), "account", - participants); Map participantSources = new LinkedHashMap<>(); for (String participant : participants) { ArchiveProgressEnvelope participantProgress = progress( ArchiveProgressEnvelope.Kind.PARTICIPANT_PROGRESS, participant, marker); - if ("abi".equals(participant)) { - level.apply(Collections.emptyList(), participantProgress); - participantSources.put(participant, level); - } else if ("account".equals(participant)) { - rocks.apply(Collections.emptyList(), participantProgress); - participantSources.put(participant, rocks); - } else { - participantSources.put(participant, () -> participantProgress); - } + participantSources.put(participant, () -> participantProgress); } Path shadow = root.resolve("serving-shadow"); @@ -134,8 +121,6 @@ public void close() throws Exception { } catch (IOException closeFailure) { failure = closeFailure; } - rocks.close(); - level.close(); history.close(); index.close(); bodies.close(); diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveBootstrapAnchorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBootstrapAnchorTest.java new file mode 100644 index 00000000000..ece6a0385b4 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveBootstrapAnchorTest.java @@ -0,0 +1,116 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.core.SnapshotManager; + +public class ArchiveBootstrapAnchorTest { + + private static final int MAGIC = 0x54414241; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void independentAnchorSurvivesSecondRestartWithoutLegacyPaths() throws Exception { + Path archive = bootstrap("second-restart", 123); + byte[] encoded = Files.readAllBytes(archive.resolve("bootstrap.anchor")); + + assertEquals(MAGIC, ByteBuffer.wrap(encoded).getInt()); + assertFalse(Files.exists(archive.resolve("participants"))); + assertFalse(Files.exists(archive.resolve("progress"))); + assertNoAnchorTemporary(archive); + for (int restart = 0; restart < 2; restart++) { + try (StateArchiveRuntimeOwner owner = StateArchiveRuntimeOwner.recover( + new SnapshotManager(""), archive, 4096)) { + assertEquals(123, owner.getRecoveredHead().getEpoch()); + } + assertArrayEquals(encoded, Files.readAllBytes(archive.resolve("bootstrap.anchor"))); + } + } + + @Test + public void checksumCorruptionAndSubstitutedMarkerFailClosed() throws Exception { + Path corrupt = bootstrap("corrupt", 123); + byte[] bytes = Files.readAllBytes(corrupt.resolve("bootstrap.anchor")); + bytes[bytes.length - 1] ^= 1; + Files.write(corrupt.resolve("bootstrap.anchor"), bytes); + assertThrows(ArchivePersistenceException.class, + () -> openHistory(corrupt)); + + Path target = bootstrap("target", 123); + Path foreign = bootstrap("foreign", 124); + Files.copy(foreign.resolve("bootstrap.anchor"), target.resolve("bootstrap.anchor"), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + assertThrows(ArchivePersistenceException.class, + () -> openHistory(target)); + } + + @Test + public void rejectsLegacyProgressEnvelopeAndWrongStoreScope() throws Exception { + Path archive = bootstrap("legacy", 123); + HistoryCommitMarker marker; + try (ArchiveHistoryWriter writer = openHistory(archive)) { + marker = writer.committedHead(); + } + List stores = stores(); + ArchiveProgressEnvelope legacy = new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, + marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), + marker.getHistoryLocation().getBodyDigest(), new byte[32], stores); + Files.write(archive.resolve("bootstrap.anchor"), + new ArchiveProgressEnvelopeCodec().encode(legacy)); + assertThrows(ArchivePersistenceException.class, + () -> openHistory(archive)); + + List incomplete = new ArrayList<>(stores); + incomplete.remove(incomplete.size() - 1); + assertThrows(ArchivePersistenceException.class, + () -> ArchiveBootstrapAnchor.store(archive, marker, incomplete)); + } + + private Path bootstrap(String name, int epoch) throws Exception { + Path archive = temporaryFolder.newFolder(name).toPath().resolve("state-archive"); + BlockSnapshotMeta head = BlockSnapshotMeta.forBlock(epoch, hash(epoch), hash(epoch - 1), + epoch * 1_000L); + try (StateArchiveRuntimeOwner ignored = StateArchiveRuntimeOwner.bootstrapAndRecover( + new SnapshotManager(""), archive, 4096, head)) { + return archive; + } + } + + private static ArchiveHistoryWriter openHistory(Path archive) throws Exception { + return new ArchiveHistoryWriter(archive, 4096, ArchiveStoreScope.getStateDatabases()); + } + + private static void assertNoAnchorTemporary(Path archive) throws Exception { + try (java.util.stream.Stream entries = Files.list(archive)) { + assertFalse(entries.anyMatch(path -> path.getFileName().toString() + .startsWith(".bootstrap.anchor-"))); + } + } + + private static List stores() { + List stores = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(stores); + return stores; + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveCheckpointFileRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveCheckpointFileRecoveryTest.java new file mode 100644 index 00000000000..cb5f756a720 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveCheckpointFileRecoveryTest.java @@ -0,0 +1,205 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.context.ApplicationContext; +import org.tron.common.parameter.CommonParameter; +import org.tron.core.config.args.Storage; +import org.tron.core.config.args.StorageConfig; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.store.CheckPointV2Store; +import org.tron.core.store.CheckTmpStore; + +public class ArchiveCheckpointFileRecoveryTest { + + static { + org.rocksdb.RocksDB.loadLibrary(); + } + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void v1ReopensTheSameBindingAcrossSecondRestart() throws Exception { + Path output = temporaryFolder.newFolder("checkpoint-v1-restart").toPath(); + ArchiveWalBinding binding = binding(10); + withStorage(output, 1, () -> { + writeV1(bindingBytes(binding)); + assertRecoveredV1(binding); + assertRecoveredV1(binding); + }); + } + + @Test + public void v1CorruptBindingFailsDuringPreflight() throws Exception { + Path output = temporaryFolder.newFolder("checkpoint-v1-corrupt").toPath(); + withStorage(output, 1, () -> { + byte[] corrupt = bindingBytes(binding(10)); + corrupt[corrupt.length - 1] ^= 1; + writeV1(corrupt); + try (CheckTmpStore checkpoint = checkpointV1()) { + SnapshotManager snapshots = snapshots(1, checkpoint); + try { + assertThrows(ArchivePersistenceException.class, snapshots::check); + } finally { + snapshots.shutdown(); + } + } + }); + } + + @Test + public void v2SelectsLatestBindingAndIgnoresEmptyBoundariesAcrossRestart() throws Exception { + Path output = temporaryFolder.newFolder("checkpoint-v2-selection").toPath(); + ArchiveWalBinding older = binding(10); + ArchiveWalBinding latest = binding(11); + withStorage(output, 2, () -> { + writeV2("1000", null); // pre-enable/before-force boundary + writeV2("2000", bindingBytes(older)); + writeV2("3000", null); // empty boundary must not erase older authority + writeV2("4000", bindingBytes(latest)); + writeV2("5000", null); // after-force empty boundary must not erase latest authority + assertRecoveredV2(latest); + assertRecoveredV2(latest); + }); + } + + @Test + public void v2CorruptLatestBindingFailsInsteadOfFallingBackToOlderAuthority() + throws Exception { + Path output = temporaryFolder.newFolder("checkpoint-v2-corrupt").toPath(); + ArchiveWalBinding older = binding(10); + withStorage(output, 2, () -> { + writeV2("1000", bindingBytes(older)); + byte[] corrupt = bindingBytes(binding(11)); + corrupt[corrupt.length - 1] ^= 1; + writeV2("2000", corrupt); + try (CheckTmpStore checkpoint = checkpointV1()) { + SnapshotManager snapshots = snapshots(2, checkpoint); + try { + assertThrows(ArchivePersistenceException.class, snapshots::check); + assertArrayEquals(older.getBatchDigest(), + snapshots.getRecoveredArchiveWalBinding().getBatchDigest()); + } finally { + snapshots.shutdown(); + } + } + }); + } + + private static void assertRecoveredV1(ArchiveWalBinding expected) throws Exception { + try (CheckTmpStore checkpoint = checkpointV1()) { + SnapshotManager snapshots = snapshots(1, checkpoint); + try { + snapshots.check(); + assertArrayEquals(expected.getBatchDigest(), + snapshots.getRecoveredArchiveWalBinding().getBatchDigest()); + } finally { + snapshots.shutdown(); + } + } + } + + private static void assertRecoveredV2(ArchiveWalBinding expected) throws Exception { + try (CheckTmpStore checkpoint = checkpointV1()) { + SnapshotManager snapshots = snapshots(2, checkpoint); + try { + snapshots.check(); + assertArrayEquals(expected.getBatchDigest(), + snapshots.getRecoveredArchiveWalBinding().getBatchDigest()); + } finally { + snapshots.shutdown(); + } + } + } + + private static SnapshotManager snapshots(int version, CheckTmpStore checkpoint) { + CommonParameter.getInstance().getStorage().setCheckpointVersion(version); + SnapshotManager snapshots = new SnapshotManager(""); + snapshots.setCheckTmpStore(checkpoint); + snapshots.init(); + return snapshots; + } + + private static void writeV1(byte[] encoded) throws Exception { + try (CheckTmpStore checkpoint = checkpointV1()) { + checkpoint.updateByBatch(Collections.singletonMap( + ArchiveWalBinding.getCheckpointKey(), encoded)); + } + } + + private static CheckTmpStore checkpointV1() { + return new CheckTmpStore(mock(ApplicationContext.class)); + } + + private static void writeV2(String name, byte[] encoded) { + try (CheckPointV2Store checkpoint = new CheckPointV2Store("checkpoint/" + name)) { + Map batch = new HashMap<>(); + if (encoded != null) { + batch.put(ArchiveWalBinding.getCheckpointKey(), encoded); + } + checkpoint.updateByBatch(batch); + } + } + + private static byte[] bindingBytes(ArchiveWalBinding binding) { + return new ArchiveWalBindingCodec().encode(binding); + } + + private static ArchiveWalBinding binding(int epoch) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, epoch, hash(epoch), + hash(epoch - 1), epoch * 1_000L); + return new ArchiveWalBinding(meta, meta, epoch - 1L, hash(epoch - 1), + digest(epoch), digest(epoch + 10), digest(epoch + 20), digest(epoch + 30)); + } + + private static byte[] hash(int suffix) { + byte[] value = new byte[32]; + value[31] = (byte) suffix; + return value; + } + + private static byte[] digest(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + private static void withStorage(Path output, int version, ThrowingRunnable action) + throws Exception { + CommonParameter parameters = CommonParameter.getInstance(); + Storage oldStorage = parameters.getStorage(); + Storage storage = new Storage(); + storage.setDefaultDbOptions(new StorageConfig()); + String oldOutput = parameters.outputDirectory; + try { + parameters.storage = storage; + parameters.outputDirectory = output.toString(); + storage.setDbDirectory("database"); + storage.setDbEngine("LEVELDB"); + storage.setDbSync(true); + storage.setCheckpointVersion(version); + storage.setCheckpointSync(true); + action.run(); + } finally { + parameters.outputDirectory = oldOutput; + parameters.storage = oldStorage; + } + } + + @FunctionalInterface + private interface ThrowingRunnable { + void run() throws Exception; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java index cd7789358dc..0c13e8179c0 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java @@ -229,15 +229,15 @@ public void persistsBatchedPrefixWithoutPerBlockFilesAndResumes() throws Excepti } @Test - public void scansOnlyTailAfterAStaleRestartCheckpoint() throws Exception { + public void scansOnlyTailAfterAStaleHistoryScanAnchor() throws Exception { Path archive = temporaryFolder.newFolder("stale-checkpoint").toPath(); byte[] checkpointAtOne; try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { writer.accept(diff(1)); - checkpointAtOne = Files.readAllBytes(archive.resolve("restart.checkpoint")); + checkpointAtOne = Files.readAllBytes(archive.resolve("history.scan-anchor")); writer.accept(diff(2)); } - Files.write(archive.resolve("restart.checkpoint"), checkpointAtOne); + Files.write(archive.resolve("history.scan-anchor"), checkpointAtOne); try (ArchiveHistoryWriter reopened = new ArchiveHistoryWriter(archive, 4096, databases())) { assertEquals(2, reopened.committedHead().getMeta().getEpoch()); @@ -246,6 +246,28 @@ public void scansOnlyTailAfterAStaleRestartCheckpoint() throws Exception { } } + @Test + public void isolatesLegacyRestartCheckpointFromNewScanAnchor() throws Exception { + Path archive = temporaryFolder.newFolder("legacy-checkpoint-isolation").toPath(); + HistoryCommitMarkerCodec codec = new HistoryCommitMarkerCodec(); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + writer.accept(diff(1)); + } + ArchiveHistoryScanAnchor anchor = ArchiveHistoryScanAnchor.load(archive, codec); + ArchiveRestartCheckpoint.persist(archive, anchor.getFirstEpoch(), anchor.getRecordCount(), + anchor.getCommitRecordLength(), anchor.getMarker(), codec); + Files.delete(archive.resolve("history.scan-anchor")); + + assertTrue(Files.exists(archive.resolve("restart.checkpoint"))); + assertNull(ArchiveHistoryScanAnchor.load(archive, codec)); + + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { + assertEquals(1, writer.committedHead().getMeta().getEpoch()); + } + assertTrue(Files.exists(archive.resolve("restart.checkpoint"))); + assertTrue(Files.exists(archive.resolve("history.scan-anchor"))); + } + @Test public void truncatesInvalidBodyAndIndexTailWithoutRescanningPrefix() throws Exception { Path archive = temporaryFolder.newFolder("invalid-data-tail").toPath(); @@ -299,12 +321,12 @@ public void boundsPreparedTailAcrossAnOversizedFlushFailure() throws Exception { } @Test - public void failsClosedOnCorruptRestartCheckpoint() throws Exception { + public void failsClosedOnCorruptHistoryScanAnchor() throws Exception { Path archive = temporaryFolder.newFolder("corrupt-checkpoint").toPath(); try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { writer.accept(diff(1)); } - Path checkpoint = archive.resolve("restart.checkpoint"); + Path checkpoint = archive.resolve("history.scan-anchor"); byte[] encoded = Files.readAllBytes(checkpoint); encoded[encoded.length - 1] ^= 1; Files.write(checkpoint, encoded); @@ -314,7 +336,7 @@ public void failsClosedOnCorruptRestartCheckpoint() throws Exception { } @Test - public void completesPreparedTruncationBeforeLoadingRestartCheckpoint() throws Exception { + public void completesPreparedTruncationBeforeLoadingHistoryScanAnchor() throws Exception { Path archive = temporaryFolder.newFolder("writer-truncation-recovery").toPath(); initializeHistory(archive, 3); prepareTruncation(archive, 2); @@ -340,7 +362,7 @@ public void truncatesDerivedAccountIndexToRecoveredHistoryAuthority() throws Exc archive, 4096, databases())) { assertEquals(2, reopened.committedHead().getMeta().getEpoch()); } - ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archive, new HistoryCommitMarkerCodec()); assertEquals(2, checkpoint.getMarker().getMeta().getEpoch()); try (AccountChangeIndex index = new AccountChangeIndex( @@ -438,13 +460,13 @@ archive, new HistoryCommitMarkerCodec())) { bodies.sync(); index.sync(); commits.commitAll(markers); - ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), + ArchiveHistoryScanAnchor.persist(archive, commits.firstEpoch(), commits.size(), commits.getRecordLength(), commits.head(), new HistoryCommitMarkerCodec()); } } private static void prepareTruncation(Path archive, long targetEpoch) throws Exception { - ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archive, new HistoryCommitMarkerCodec()); try (HistorySegmentStore bodies = new HistorySegmentStore( archive, new BlockHistoryCodec(), 4096, checkpoint); diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java index 82dc84ee9d9..333e7250316 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveParticipantDescriptorTest.java @@ -54,14 +54,14 @@ public void rejectsAbiExcludedExact26AndV2OnlyExact25ParticipantSets() { } @Test - public void manifestBindsApprovedScopeAndRejectsLegacyVersion() throws Exception { + public void manifestBindsApprovedScopeAndRejectsLegacyVersions() throws Exception { List participants = ArchiveParticipantDescriptor.current().getParticipants(); Path archive = temporaryFolder.newFolder("exact-27-manifest").toPath(); ArchiveBaseManifest manifest = new ArchiveBaseManifest(archive, participants); manifest.ensureBase(meta(1)); byte[] encoded = Files.readAllBytes(archive.resolve("MANIFEST")); - assertEquals(2, ByteBuffer.wrap(encoded, Integer.BYTES, Short.BYTES).getShort()); + assertEquals(3, ByteBuffer.wrap(encoded, Integer.BYTES, Short.BYTES).getShort()); new ArchiveBaseManifest(archive, participants); List abiExcludedExact26 = new ArrayList<>(participants); @@ -69,7 +69,7 @@ public void manifestBindsApprovedScopeAndRejectsLegacyVersion() throws Exception assertThrows(ArchivePersistenceException.class, () -> new ArchiveBaseManifest(archive, abiExcludedExact26)); - ByteBuffer.wrap(encoded).putShort(Integer.BYTES, (short) 1); + ByteBuffer.wrap(encoded).putShort(Integer.BYTES, (short) 2); byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); ByteBuffer.wrap(encoded, encoded.length - Integer.BYTES, Integer.BYTES) .putInt(Hashing.crc32c().hashBytes(payload).asInt()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java index 2db364d4377..0ee797e487f 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java @@ -53,7 +53,7 @@ public void everyPostIntentCrashUsesIntentAndConvergesToTargetCheckpoint() throw }); assertThrows(IOException.class, recovery::recover); - ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archive, new HistoryCommitMarkerCodec()); long expectedCheckpoint = failedStage == Stage.COMMIT_SHRUNK ? 12 : 10; assertEquals(expectedCheckpoint, checkpoint.getMarker().getMeta().getEpoch()); @@ -67,7 +67,7 @@ public void everyPostIntentCrashUsesIntentAndConvergesToTargetCheckpoint() throw public void intentPreReplaceCrashNeverShrinksCommittedAuthority() throws Exception { Path archive = temporaryFolder.newFolder("intent-pre-replace").toPath(); initialize(archive); - ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archive, new HistoryCommitMarkerCodec()); try (HistorySegmentStore bodies = new HistorySegmentStore( archive, new BlockHistoryCodec(), 4096, checkpoint); @@ -119,13 +119,13 @@ archive, new HistoryCommitMarkerCodec())) { index.sync(); commits.commitAll(markers); head = commits.head(); - ArchiveRestartCheckpoint.persist(archive, commits.firstEpoch(), commits.size(), + ArchiveHistoryScanAnchor.persist(archive, commits.firstEpoch(), commits.size(), commits.getRecordLength(), head, new HistoryCommitMarkerCodec()); } } private static void prepare(Path archive) throws Exception { - ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archive, new HistoryCommitMarkerCodec()); try (HistorySegmentStore bodies = new HistorySegmentStore( archive, new BlockHistoryCodec(), 4096, checkpoint); @@ -145,7 +145,7 @@ private static void assertRecovered(Path archive) throws Exception { private static void assertHeads(Path archive, long checkpointEpoch, long commitEpoch, long indexEpoch, long bodyEpoch) throws Exception { - ArchiveRestartCheckpoint checkpoint = ArchiveRestartCheckpoint.load(archive, + ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archive, new HistoryCommitMarkerCodec()); assertEquals(checkpointEpoch, checkpoint.getMarker().getMeta().getEpoch()); try (HistorySegmentStore bodies = new HistorySegmentStore( diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveWalBindingTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveWalBindingTest.java new file mode 100644 index 00000000000..54eebf1dde5 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveWalBindingTest.java @@ -0,0 +1,64 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +public class ArchiveWalBindingTest { + + private final ArchiveWalBindingCodec codec = new ArchiveWalBindingCodec(); + + @Test + public void roundTripsContiguousHistoryIdentityAndCheckpointEntry() { + ArchiveWalBinding binding = ArchiveWalBinding.fromMarkers(Arrays.asList( + marker(7, 6), marker(8, 7))); + + ArchiveWalBinding decoded = codec.decode(codec.encode(binding)); + + assertEquals(7, decoded.getFirst().getEpoch()); + assertEquals(8, decoded.getLast().getEpoch()); + assertEquals(6, decoded.getPredecessorEpoch()); + assertArrayEquals(hash(6), decoded.getPredecessorHash()); + assertArrayEquals(binding.getBatchDigest(), decoded.getBatchDigest()); + assertArrayEquals(binding.getStoreScopeDigest(), decoded.getStoreScopeDigest()); + assertArrayEquals(binding.getHistoryRefsDigest(), decoded.getHistoryRefsDigest()); + assertArrayEquals(binding.getBlockIndexRefsDigest(), decoded.getBlockIndexRefsDigest()); + + Map checkpoint = new LinkedHashMap<>(); + checkpoint.put(ArchiveWalBinding.getCheckpointKey(), codec.encode(binding)); + assertNotNull(ArchiveWalBinding.fromCheckpointBatch(checkpoint)); + } + + @Test + public void rejectsCorruptionAndNonContiguousMarkers() { + byte[] encoded = codec.encode(ArchiveWalBinding.fromMarkers( + Collections.singletonList(marker(7, 6)))); + encoded[encoded.length - 5] ^= 1; + assertThrows(IllegalArgumentException.class, () -> codec.decode(encoded)); + + List gap = Arrays.asList(marker(7, 6), marker(9, 8)); + assertThrows(IllegalArgumentException.class, () -> ArchiveWalBinding.fromMarkers(gap)); + } + + private static HistoryCommitMarker marker(int epoch, int previousEpoch) { + return new HistoryCommitMarker( + new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(previousEpoch), epoch * 1_000L), + previousEpoch, new HistoryLocation(0, epoch * 100L, 80, epoch, hash(epoch + 20)), + new HistoryIndexLocation(epoch * 120L, 96, hash(epoch + 40)), + Arrays.copyOf(hash(epoch + 60), 16), Arrays.asList("account", "properties")); + } + + private static byte[] hash(int suffix) { + byte[] value = new byte[32]; + value[31] = (byte) suffix; + return value; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/CommittedHistoryAuthorityTest.java b/framework/src/test/java/org/tron/core/db2/archive/CommittedHistoryAuthorityTest.java index db962f601d0..1d5773419a1 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/CommittedHistoryAuthorityTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/CommittedHistoryAuthorityTest.java @@ -5,9 +5,7 @@ import static org.junit.Assert.assertNotNull; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -20,7 +18,6 @@ public class CommittedHistoryAuthorityTest { @Test public void writerAndStoreExposeTheSameReadOnlyCommittedAuthority() throws Exception { Path archive = temporaryFolder.newFolder("committed-authority").toPath(); - Path reader = archive.resolve("progress/reader.progress"); BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1_000L); try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( @@ -28,11 +25,6 @@ public void writerAndStoreExposeTheSameReadOnlyCommittedAuthority() throws Excep writer.accept(new BlockReverseDiff(meta, Collections.emptyList())); assertAuthority(writer, meta); - new ArchiveReaderHeadPublisher(writer, reader, participants()).publish(1); - ArchiveProgressEnvelope published = new ArchiveProgressFile(reader, - new ArchiveProgressEnvelopeCodec()).load(); - assertEquals(1L, published.getEpoch()); - assertArrayEquals(meta.getBlockHash(), published.getBlockHash()); } try (HistoryCommitStore store = new HistoryCommitStore( @@ -55,12 +47,6 @@ private static void assertAuthority(CommittedHistoryAuthority authority, assertArrayEquals(expected.getBlockHash(), coverage.getHeadHash()); } - private static List participants() { - List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); - Collections.sort(participants); - return participants; - } - private static byte[] hash(int suffix) { byte[] hash = new byte[32]; hash[31] = (byte) suffix; diff --git a/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactoryTest.java b/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactoryTest.java index 381c429002a..bb282f93410 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactoryTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactoryTest.java @@ -76,6 +76,29 @@ public void assemblesExactMixedEnginesAndIgnoresDerivedStore() throws Exception assertEquals(ArchiveStoreScope.getStateDatabases().size(), totalCloses(registry)); } + @Test + public void combinesSnapshotManagerStoresWithSupplementalAccountAsset() throws Exception { + Registry registry = registry(false); + DB accountAsset = registry.engines.get( + AccountAssetArchiveProjector.ACCOUNT_ASSET_DB); + registry.manager.getDbs().removeIf(database -> + AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(database.getDbName())); + ArchiveProgressEnvelope authority = new ArchiveProgressEnvelope(Kind.READER_VISIBLE, null, 1, + hash(1), new byte[16], new byte[32], registry.participants); + + try (LatestStateGenerationCoordinator coordinator = + LatestStateGenerationCoordinatorFactory.create(registry.manager, + java.util.Collections.singletonMap( + AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + (LatestStateGenerationAdapter.SnapshotCapableStore) accountAsset), + () -> authority); + LatestStateGenerationCoordinator.Candidate candidate = + coordinator.acquire("generation-supplemental")) { + assertEquals(ArchiveStoreScope.getStateDatabases().size(), totalPins(registry)); + } + assertEquals(ArchiveStoreScope.getStateDatabases().size(), totalCloses(registry)); + } + @Test public void rejectsMissingDuplicateAndNonCapableStateRoots() throws Exception { Path readerVisible = temporaryFolder.newFile("invalid-reader-visible").toPath(); diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index 9d0117e2f68..c4ee9bcc310 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -31,17 +31,12 @@ import java.util.Map; import java.util.Random; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.tron.common.BaseMethodTest; import org.tron.core.db.common.DbSourceInter; import org.tron.core.db2.ISession; -import org.tron.core.db2.archive.AccountAssetForwardProjector.AssetMutation; -import org.tron.core.db2.archive.AccountAssetPreparedBlockPayloadOwner.FrozenBatch; import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; import org.tron.core.db2.archive.BlockReverseDiff.Entry; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.Flusher; import org.tron.core.db2.common.WrappedByteArray; @@ -61,13 +56,12 @@ public void borrowedRuntimeAttachmentIsAtomicAndIdentityBound() throws Exception SnapshotManager manager = new SnapshotManager(""); manager.add(new Chainbase(new SnapshotRoot(new MemoryDb("code")))); OldValueCollector collector = mock(OldValueCollector.class); - ArchiveBlockProjectionPreparer preparer = mock(ArchiveBlockProjectionPreparer.class); DurableBlockReverseDiffSink sink = mock(DurableBlockReverseDiffSink.class, withSettings().extraInterfaces(Closeable.class)); ArchiveRuntimeAttachment attachment = - new ArchiveRuntimeAttachment(collector, preparer, sink); + new ArchiveRuntimeAttachment(collector, sink); ArchiveRuntimeAttachment foreign = - new ArchiveRuntimeAttachment(collector, preparer, sink); + new ArchiveRuntimeAttachment(collector, sink); manager.attachArchiveRuntime(attachment); @@ -75,8 +69,6 @@ public void borrowedRuntimeAttachmentIsAtomicAndIdentityBound() throws Exception assertThrows(IllegalStateException.class, () -> manager.detachArchiveRuntime(foreign)); assertThrows(IllegalStateException.class, () -> manager.installArchiveCollector(collector, sink)); - assertThrows(IllegalStateException.class, - () -> manager.installArchiveProjectionPreparer(preparer)); assertSame(attachment, manager.detachArchiveRuntime(attachment)); assertThrows(IllegalStateException.class, () -> manager.detachArchiveRuntime(attachment)); verify((Closeable) sink, never()).close(); @@ -84,76 +76,12 @@ public void borrowedRuntimeAttachmentIsAtomicAndIdentityBound() throws Exception BlockReverseDiffSink legacySink = mock(BlockReverseDiffSink.class, withSettings().extraInterfaces(Closeable.class)); manager.installArchiveCollector(collector, legacySink); - manager.installArchiveProjectionPreparer(preparer); manager.shutdown(); verify((Closeable) legacySink).close(); verify((Closeable) sink, never()).close(); } - @Test - public void detachAbortsLayerAndFrozenForwardOwnership() throws Exception { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); - AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = - preparedProjection(meta, mock(BlockReverseDiff.class)); - DurableBlockReverseDiffSink sink = mock(DurableBlockReverseDiffSink.class, - withSettings().extraInterfaces(Closeable.class)); - ArchiveRuntimeAttachment attachment = new ArchiveRuntimeAttachment( - new SnapshotOldValueCollector(), captured -> projection, sink); - manager.attachArchiveRuntime(attachment); - commitBlock(manager, database, meta, "key-1"); - setFlushCount(manager, 1); - manager.freezeArchiveForwardFlushRange(); - - assertTrue(manager.hasPendingArchiveForwardFlush()); - assertSame(attachment, manager.detachArchiveRuntime(attachment)); - - assertFalse(manager.hasPendingArchiveForwardFlush()); - assertEquals(0, manager.getArchiveForwardPayloadOwnerCount()); - assertThrows(IllegalStateException.class, manager::claimArchiveForwardFlushPayloads); - verify(projection).abort(); - verify((Closeable) sink, never()).close(); - manager.shutdown(); - verify((Closeable) sink, never()).close(); - } - - @Test - public void detachClearsSealedForwardOwnershipWithoutAbortingCompletedProjection() - throws Exception { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); - BlockChangeView view = mock(BlockChangeView.class); - when(view.getMeta()).thenReturn(meta); - AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = - sealReadyProjection(meta, view); - DurableBlockReverseDiffSink sink = mock(DurableBlockReverseDiffSink.class); - ArchiveRuntimeAttachment attachment = new ArchiveRuntimeAttachment( - new SnapshotOldValueCollector(), captured -> projection, sink); - manager.attachArchiveRuntime(attachment); - commitBlock(manager, database, meta, "key-1"); - setFlushCount(manager, 1); - manager.freezeArchiveForwardFlushRange(); - manager.sealPendingArchiveForwardFlush(Collections.singletonList(marker(meta))); - - assertTrue(manager.hasPendingArchiveForwardFlush()); - manager.detachArchiveRuntime(attachment); - - assertFalse(manager.hasPendingArchiveForwardFlush()); - assertThrows(IllegalStateException.class, manager::claimArchiveForwardFlushPayloads); - verify(projection).completeSeal(); - verify(projection, never()).abort(); - manager.shutdown(); - } - @Test public void collectsBlockPreStateAfterNestedSessionsFinish() { MemoryDb memoryDb = new MemoryDb("code"); @@ -482,48 +410,6 @@ public void projectsOldPhysicalAssetValueForOptimizedAccount() { manager.shutdown(); } - @Test - public void sharedAccountAssetProjectionUsesOneSnapshotAndStableForwardOrder() { - byte[] address = archiveAddress(3); - byte[] firstKey = Bytes.concat(address, bytes("1000001")); - byte[] secondKey = Bytes.concat(address, bytes("1000002")); - Account oldAccount = Account.newBuilder() - .setAddress(ByteString.copyFrom(address)) - .setAssetOptimized(true) - .build(); - Account postAccount = oldAccount.toBuilder() - .putAssetV2("1000001", 80L) - .putAssetV2("1000002", 0L) - .build(); - AccountAssetStore assetStore = mock(AccountAssetStore.class); - Map persisted = new LinkedHashMap<>(); - persisted.put(WrappedByteArray.copyOf(secondKey), Longs.toByteArray(200L)); - persisted.put(WrappedByteArray.copyOf(firstKey), Longs.toByteArray(100L)); - when(assetStore.prefixQuery(any(byte[].class))).thenReturn(persisted); - - AccountAssetArchiveProjector.Projection projection = - new AccountAssetArchiveProjector().project(address, oldAccount.toByteArray(), - BlockChangeView.PostValue.present(postAccount.toByteArray()), false, persisted); - - verify(assetStore, never()).prefixQuery(any(byte[].class)); - assertEquals(2, projection.reverseAssets.size()); - assertEquals(2, projection.forwardAssets.size()); - assertArrayEquals(firstKey, projection.reverseAssets.get(0).getKey()); - assertArrayEquals(secondKey, projection.reverseAssets.get(1).getKey()); - assertArrayEquals(firstKey, projection.forwardAssets.get(0).getPhysicalRawKey()); - assertArrayEquals(secondKey, projection.forwardAssets.get(1).getPhysicalRawKey()); - assertArrayEquals(Longs.toByteArray(100L), - projection.reverseAssets.get(0).getOldValue().getValue()); - assertArrayEquals(Longs.toByteArray(80L), - projection.forwardAssets.get(0).getPostValue().getValue()); - assertFalse(projection.forwardAssets.get(1).getPostValue().isPresent()); - assertThrows(UnsupportedOperationException.class, projection.reverseAssets::clear); - assertThrows(UnsupportedOperationException.class, projection.forwardAssets::clear); - Account canonicalPost = parseAccount(projection.postAccount.getValue()); - assertTrue(canonicalPost.getAssetOptimized()); - assertTrue(canonicalPost.getAssetV2Map().isEmpty()); - } - @Test public void pureProjectionRequiresAndCopiesExplicitOldPhysicalAssets() { byte[] address = archiveAddress(4); @@ -555,7 +441,6 @@ public void pureProjectionRequiresAndCopiesExplicitOldPhysicalAssets() { assertArrayEquals(assetKey, projection.reverseAssets.get(0).getKey()); assertArrayEquals(Longs.toByteArray(900L), projection.reverseAssets.get(0).getOldValue().getValue()); - assertFalse(projection.forwardAssets.get(0).getPostValue().isPresent()); } @Test @@ -571,17 +456,12 @@ public void targetAssetOptimizationOverridesLegacySupplierAndCoversDelete() { AccountAssetArchiveProjector.Projection enabled = projector.project(address, null, BlockChangeView.PostValue.present(rawPost.toByteArray()), true, Collections.emptyMap()); assertTrue(parseAccount(enabled.postAccount.getValue()).getAssetOptimized()); - assertEquals(1, enabled.forwardAssets.size()); - assertArrayEquals(assetKey, enabled.forwardAssets.get(0).getPhysicalRawKey()); - assertArrayEquals(Longs.toByteArray(300L), - enabled.forwardAssets.get(0).getPostValue().getValue()); AccountAssetArchiveProjector.Projection disabled = new AccountAssetArchiveProjector().project(address, null, BlockChangeView.PostValue.present(rawPost.toByteArray()), false, Collections.emptyMap()); assertArrayEquals(rawPost.toByteArray(), disabled.postAccount.getValue()); - assertTrue(disabled.forwardAssets.isEmpty()); Map mixedPhysical = new HashMap<>(); mixedPhysical.put(WrappedByteArray.copyOf(assetKey), Longs.toByteArray(300L)); assertThrows(ArchivePersistenceException.class, @@ -598,49 +478,67 @@ public void targetAssetOptimizationOverridesLegacySupplierAndCoversDelete() { optimizedOld.toByteArray(), BlockChangeView.PostValue.absent(), true, persisted); assertFalse(deleted.postAccount.isPresent()); assertEquals(1, deleted.reverseAssets.size()); - assertEquals(1, deleted.forwardAssets.size()); - assertFalse(deleted.forwardAssets.get(0).getPostValue().isPresent()); } @Test - public void sharedProjectionUsesOuterFinalViewAfterNestedMergeAndRevoke() { - byte[] address = archiveAddress(6); - byte[] assetKey = Bytes.concat(address, bytes("1000004")); - Account oldAccount = Account.newBuilder() - .setAddress(ByteString.copyFrom(address)) - .putAssetV2("1000004", 100L) - .build(); - MemoryDb memoryDb = new MemoryDb("account"); - memoryDb.put(address, oldAccount.toByteArray()); + public void targetAssetOptimizationComesFromTheCapturedPropertiesPostState() { + List invalidValues = Arrays.asList(null, new byte[]{1}, Longs.toByteArray(2)); + for (byte[] invalidValue : invalidValues) { + SnapshotManager manager = new SnapshotManager(""); + Chainbase account = new Chainbase(new SnapshotRoot(new MemoryDb("account"))); + MemoryDb propertiesRoot = new MemoryDb("properties"); + if (invalidValue != null) { + propertiesRoot.put(HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), + invalidValue); + } + Chainbase properties = new Chainbase(new SnapshotRoot(propertiesRoot)); + manager.add(account); + manager.add(properties); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector( + new AccountAssetArchiveProjector(), ignored -> Collections.emptyMap(), + SnapshotOldValueCollector::resolveTargetAssetOptimization), diff -> { }); + byte[] address = archiveAddress(12); + Account post = Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .putAssetV2("1000012", 12L).build(); + + assertThrows(ArchivePersistenceException.class, () -> { + try (ISession block = manager.buildSession()) { + account.put(address, post.toByteArray()); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + }); + assertEquals(0, manager.getActiveSession()); + assertEquals(0, manager.size()); + manager.shutdown(); + } + SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); + Chainbase account = new Chainbase(new SnapshotRoot(new MemoryDb("account"))); + MemoryDb propertiesRoot = new MemoryDb("properties"); + propertiesRoot.put(HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), + Longs.toByteArray(0)); + Chainbase properties = new Chainbase(new SnapshotRoot(propertiesRoot)); + manager.add(account); + manager.add(properties); manager.enable(); - BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); - + manager.installArchiveCollector(new SnapshotOldValueCollector( + new AccountAssetArchiveProjector(), ignored -> Collections.emptyMap(), + SnapshotOldValueCollector::resolveTargetAssetOptimization), diff -> { }); + byte[] address = archiveAddress(13); + Account post = Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .putAssetV2("1000013", 13L).build(); try (ISession block = manager.buildSession()) { - database.put(address, oldAccount.toBuilder().putAssetV2("1000004", 90L) - .build().toByteArray()); - try (ISession merged = manager.buildSession()) { - database.put(address, oldAccount.toBuilder().putAssetV2("1000004", 80L) - .build().toByteArray()); - merged.merge(); - } - try (ISession revoked = manager.buildSession()) { - database.put(address, oldAccount.toBuilder().putAssetV2("1000004", 70L) - .build().toByteArray()); - } - BlockChangeView view = BlockChangeView.capture(meta, - Collections.singletonList(database)); - BlockChangeView.Change finalChange = view.getDatabases().get(0).getChanges().get(0); - AccountAssetArchiveProjector.Projection projection = - new AccountAssetArchiveProjector().project(address, oldAccount.toByteArray(), - finalChange.getPostValue(), true, Collections.emptyMap()); - assertEquals(1, projection.forwardAssets.size()); - AssetMutation mutation = projection.forwardAssets.get(0); - assertArrayEquals(assetKey, mutation.getPhysicalRawKey()); - assertArrayEquals(Longs.toByteArray(80L), mutation.getPostValue().getValue()); + properties.put(HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), + Longs.toByteArray(1)); + account.put(address, post.toByteArray()); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); } + DbGroup assetGroup = prepared(account).getGroups().stream() + .filter(group -> "account-asset".equals(group.getDbName())) + .findFirst().orElseThrow(AssertionError::new); + assertFalse(find(assetGroup, Bytes.concat(address, bytes("1000013"))) + .getOldValue().isPresent()); manager.shutdown(); } @@ -669,10 +567,6 @@ public void sharedProjectionMatchesSnapshotRootBytesWithProposalSixtySix() { assertArrayEquals(projection.postAccount.getValue(), accountRootDb.get(address)); assertArrayEquals(Longs.toByteArray(500L), chainBaseManager.getAccountAssetStore().get(assetKey)); - assertEquals(1, projection.forwardAssets.size()); - assertArrayEquals(assetKey, projection.forwardAssets.get(0).getPhysicalRawKey()); - assertArrayEquals(Longs.toByteArray(500L), - projection.forwardAssets.get(0).getPostValue().getValue()); } @Test @@ -720,25 +614,15 @@ public void flushRetriesDurabilityAndEvidenceWithoutResubmittingHistory() throws ArchiveStoreScope.getStateDatabases()); FailOnceEvidenceSink sink = new FailOnceEvidenceSink(writer); manager.installArchiveCollector(new SnapshotOldValueCollector(), sink); - AtomicReference prepared = - new AtomicReference<>(); - manager.installArchiveProjectionPreparer(view -> { - AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = - sealReadyProjection(view.getMeta(), view); - prepared.set(projection); - return projection; - }); BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); commitBlock(manager, database, meta, "key-1"); setFlushCount(manager, 1); assertThrows(TronError.class, manager::flush); - assertTrue(manager.hasPendingArchiveForwardFlush()); verify(checkpoint, never()).updateByBatch(any(Map.class)); assertThrows(TronError.class, manager::flush); - assertTrue(manager.hasPendingArchiveForwardFlush()); verify(checkpoint, never()).updateByBatch(any(Map.class)); manager.flush(); @@ -746,9 +630,6 @@ public void flushRetriesDurabilityAndEvidenceWithoutResubmittingHistory() throws assertEquals(1, sink.acceptAllCalls); assertEquals(3, sink.awaitCalls); assertEquals(2, sink.evidenceCalls); - verify(prepared.get()).completeSeal(); - assertEquals(1, manager.claimArchiveForwardFlushPayloads().size()); - assertFalse(manager.hasPendingArchiveForwardFlush()); manager.shutdown(); writer.close(); } @@ -806,377 +687,6 @@ public void collectorFailureLeavesSessionOwnedSoCloseRevokesLayer() { manager.shutdown(); } - @Test - public void sharedProjectionPreparerIsDisabledUntilExplicitlyInstalled() { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - OldValueCollector collector = mock(OldValueCollector.class); - BlockReverseDiff reverse = mock(BlockReverseDiff.class); - when(collector.collect(any(BlockChangeView.class))).thenReturn(reverse); - manager.installArchiveCollector(collector, diff -> { }); - - try (ISession block = manager.buildSession()) { - database.put(bytes("key"), bytes("value")); - block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); - } - - verify(collector).collect(any(BlockChangeView.class)); - assertEquals(0, manager.getArchiveForwardPayloadOwnerCount()); - manager.shutdown(); - } - - @Test - public void sharedProjectionPreparerOwnsOneCapturedViewAndReversePayload() { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - OldValueCollector legacy = mock(OldValueCollector.class); - manager.installArchiveCollector(legacy, diff -> { }); - BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); - BlockReverseDiff reverse = mock(BlockReverseDiff.class); - AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = - preparedProjection(meta, reverse); - AtomicInteger calls = new AtomicInteger(); - AtomicReference captured = new AtomicReference<>(); - manager.installArchiveProjectionPreparer(view -> { - calls.incrementAndGet(); - captured.set(view); - return projection; - }); - - try (ISession block = manager.buildSession()) { - database.put(bytes("key"), bytes("value")); - block.commit(meta); - } - - assertEquals(1, calls.get()); - assertEquals(meta, captured.get().getMeta()); - assertEquals(reverse, prepared(database)); - assertTrue(manager.hasArchiveForwardPayloadOwner(meta)); - verify(legacy, never()).collect(any(BlockChangeView.class)); - manager.fastPop(); - verify(projection).abort(); - manager.shutdown(); - } - - @Test - public void projectionPrepareFailureLeavesSessionOwnedAndRegistryEmpty() { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); - manager.installArchiveProjectionPreparer(view -> { - throw new ArchivePersistenceException("injected prepare failure"); - }); - - assertThrows(ArchivePersistenceException.class, () -> { - try (ISession block = manager.buildSession()) { - database.put(bytes("key"), bytes("value")); - block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); - } - }); - - assertEquals(0, manager.getActiveSession()); - assertEquals(0, manager.size()); - assertEquals(0, manager.getArchiveForwardPayloadOwnerCount()); - assertTrue(database.getHead() instanceof SnapshotRoot); - manager.shutdown(); - } - - @Test - public void projectionAttachFailureAbortsUnownedPayloadAndRevokesLayer() { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); - BlockSnapshotMeta target = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); - AccountAssetBlockProjectionBridge.PreparedBlockProjection mismatched = - preparedProjection(BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L), - mock(BlockReverseDiff.class)); - manager.installArchiveProjectionPreparer(view -> mismatched); - - assertThrows(ArchivePersistenceException.class, () -> { - try (ISession block = manager.buildSession()) { - database.put(bytes("key"), bytes("value")); - block.commit(target); - } - }); - - verify(mismatched).abort(); - assertEquals(0, manager.getActiveSession()); - assertEquals(0, manager.size()); - assertEquals(0, manager.getArchiveForwardPayloadOwnerCount()); - assertTrue(database.getHead() instanceof SnapshotRoot); - manager.shutdown(); - } - - @Test - public void shortReorgDiscardsOnlySameMetaUnfrozenOwners() { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); - BlockSnapshotMeta firstMeta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); - BlockSnapshotMeta secondMeta = BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L); - AccountAssetBlockProjectionBridge.PreparedBlockProjection first = - preparedProjection(firstMeta, mock(BlockReverseDiff.class)); - AccountAssetBlockProjectionBridge.PreparedBlockProjection second = - preparedProjection(secondMeta, mock(BlockReverseDiff.class)); - manager.installArchiveProjectionPreparer( - view -> firstMeta.equals(view.getMeta()) ? first : second); - - try (ISession block = manager.buildSession()) { - database.put(bytes("key-1"), bytes("value-1")); - block.commit(firstMeta); - } - try (ISession block = manager.buildSession()) { - database.put(bytes("key-2"), bytes("value-2")); - block.commit(secondMeta); - } - - assertEquals(2, manager.getArchiveForwardPayloadOwnerCount()); - manager.fastPop(); - verify(second).abort(); - verify(first, never()).abort(); - assertTrue(manager.hasArchiveForwardPayloadOwner(firstMeta)); - assertFalse(manager.hasArchiveForwardPayloadOwner(secondMeta)); - manager.fastPop(); - verify(first).abort(); - assertEquals(0, manager.getArchiveForwardPayloadOwnerCount()); - manager.shutdown(); - } - - @Test - public void oldestForwardFlushRangeFreezesOnceAndExcludesFastPop() throws Exception { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); - BlockSnapshotMeta firstMeta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); - BlockSnapshotMeta secondMeta = BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L); - AccountAssetBlockProjectionBridge.PreparedBlockProjection first = - preparedProjection(firstMeta, mock(BlockReverseDiff.class)); - AccountAssetBlockProjectionBridge.PreparedBlockProjection second = - preparedProjection(secondMeta, mock(BlockReverseDiff.class)); - manager.installArchiveProjectionPreparer( - view -> firstMeta.equals(view.getMeta()) ? first : second); - commitBlock(manager, database, firstMeta, "key-1"); - commitBlock(manager, database, secondMeta, "key-2"); - setFlushCount(manager, 1); - - FrozenBatch pending = manager.freezeArchiveForwardFlushRange(); - - assertEquals(Collections.singletonList(firstMeta), pending.getExpectedMetas()); - assertSame(pending, manager.freezeArchiveForwardFlushRange()); - assertTrue(manager.hasPendingArchiveForwardFlush()); - assertEquals(1, manager.getArchiveForwardPayloadOwnerCount()); - manager.fastPop(); - verify(second).abort(); - verify(first, never()).abort(); - assertThrows(IllegalStateException.class, manager::fastPop); - assertEquals(1, manager.size()); - - manager.shutdown(); - verify(first).abort(); - } - - @Test - public void forwardFlushRegistryMismatchFailsBeforeOwnershipTransfer() throws Exception { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); - BlockSnapshotMeta firstMeta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); - BlockSnapshotMeta secondMeta = BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L); - AccountAssetBlockProjectionBridge.PreparedBlockProjection first = - preparedProjection(firstMeta, mock(BlockReverseDiff.class)); - AccountAssetBlockProjectionBridge.PreparedBlockProjection second = - preparedProjection(secondMeta, mock(BlockReverseDiff.class)); - manager.installArchiveProjectionPreparer( - view -> firstMeta.equals(view.getMeta()) ? first : second); - commitBlock(manager, database, firstMeta, "key-1"); - commitBlock(manager, database, secondMeta, "key-2"); - setFlushCount(manager, 1); - Map owners = forwardOwners(manager); - - AccountAssetPreparedBlockPayloadOwner removed = owners.remove(firstMeta); - assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); - assertEquals(1, owners.size()); - assertTrue(removed.isAttachedTo(firstMeta)); - owners.put(firstMeta, removed); - - BlockSnapshotMeta extraMeta = BlockSnapshotMeta.forBlock(3, hash(3), hash(2), 3L); - AccountAssetPreparedBlockPayloadOwner extra = - new AccountAssetPreparedBlockPayloadOwner(extraMeta); - extra.attach(preparedProjection(extraMeta, mock(BlockReverseDiff.class))); - owners.put(extraMeta, extra); - assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); - assertEquals(3, owners.size()); - assertTrue(removed.isAttachedTo(firstMeta)); - assertTrue(owners.get(secondMeta).isAttachedTo(secondMeta)); - - manager.shutdown(); - } - - @Test - public void forwardFlushRejectsTopologyGapAndUnattachedOwnerWithoutPartialTransfer() - throws Exception { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); - BlockSnapshotMeta firstMeta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); - BlockSnapshotMeta secondMeta = BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L); - AccountAssetBlockProjectionBridge.PreparedBlockProjection first = - preparedProjection(firstMeta, mock(BlockReverseDiff.class)); - AccountAssetBlockProjectionBridge.PreparedBlockProjection second = - preparedProjection(secondMeta, mock(BlockReverseDiff.class)); - manager.installArchiveProjectionPreparer( - view -> firstMeta.equals(view.getMeta()) ? first : second); - commitBlock(manager, database, firstMeta, "key-1"); - commitBlock(manager, database, secondMeta, "key-2"); - setFlushCount(manager, 1); - - SnapshotImpl newest = (SnapshotImpl) database.getHead(); - setBlockMeta(newest, firstMeta); - assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); - setBlockMeta(newest, BlockSnapshotMeta.forBlock(3, hash(3), hash(2), 3L)); - assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); - setBlockMeta(newest, secondMeta); - - Map owners = forwardOwners(manager); - owners.get(secondMeta).discard(); - assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); - assertEquals(2, owners.size()); - assertTrue(owners.get(firstMeta).isAttachedTo(firstMeta)); - verify(first, never()).abort(); - - manager.shutdown(); - verify(first).abort(); - verify(second).abort(); - } - - @Test - public void pendingForwardFlushSealRetriesAndClaimsOrderedPayloadsOnce() throws Exception { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); - BlockSnapshotMeta firstMeta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); - BlockSnapshotMeta secondMeta = BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L); - BlockChangeView firstView = mock(BlockChangeView.class); - BlockChangeView secondView = mock(BlockChangeView.class); - when(firstView.getMeta()).thenReturn(firstMeta); - when(secondView.getMeta()).thenReturn(secondMeta); - AccountAssetBlockProjectionBridge.PreparedBlockProjection first = - sealReadyProjection(firstMeta, firstView); - AccountAssetBlockProjectionBridge.PreparedBlockProjection second = - sealReadyProjection(secondMeta, secondView); - manager.installArchiveProjectionPreparer( - captured -> firstMeta.equals(captured.getMeta()) ? first : second); - commitBlock(manager, database, firstMeta, "key-1"); - commitBlock(manager, database, secondMeta, "key-2"); - setFlushCount(manager, 2); - FrozenBatch frozen = manager.freezeArchiveForwardFlushRange(); - - assertThrows(ArchivePersistenceException.class, - () -> manager.sealPendingArchiveForwardFlush( - Arrays.asList(marker(firstMeta), marker(BlockSnapshotMeta.forBlock( - 3, hash(3), hash(2), 3L))))); - assertSame(frozen, manager.freezeArchiveForwardFlushRange()); - verify(first, never()).completeSeal(); - verify(second, never()).completeSeal(); - - manager.sealPendingArchiveForwardFlush(Arrays.asList(marker(firstMeta), marker(secondMeta))); - - verify(first).completeSeal(); - verify(second).completeSeal(); - assertTrue(manager.hasPendingArchiveForwardFlush()); - assertThrows(IllegalStateException.class, manager::freezeArchiveForwardFlushRange); - assertThrows(IllegalStateException.class, manager::fastPop); - List claimed = - manager.claimArchiveForwardFlushPayloads(); - assertEquals(2, claimed.size()); - assertEquals(firstMeta, claimed.get(0).getMeta()); - assertSame(firstView, claimed.get(0).getView()); - assertEquals(secondMeta, claimed.get(1).getMeta()); - assertSame(secondView, claimed.get(1).getView()); - assertFalse(manager.hasPendingArchiveForwardFlush()); - assertThrows(IllegalStateException.class, manager::claimArchiveForwardFlushPayloads); - manager.shutdown(); - verify(first, never()).abort(); - verify(second, never()).abort(); - } - - @Test - public void durableEvidenceFailureKeepsFrozenSlotAndShutdownReleasesSealedSlot() - throws Exception { - MemoryDb memoryDb = new MemoryDb("code"); - SnapshotManager manager = new SnapshotManager(""); - Chainbase database = new Chainbase(new SnapshotRoot(memoryDb)); - manager.add(database); - manager.enable(); - manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); - BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); - HistoryCommitMarker committed = marker(meta); - BlockChangeView view = mock(BlockChangeView.class); - when(view.getMeta()).thenReturn(meta); - AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = - sealReadyProjection(meta, view); - manager.installArchiveProjectionPreparer(captured -> projection); - commitBlock(manager, database, meta, "key-1"); - setFlushCount(manager, 1); - FrozenBatch frozen = manager.freezeArchiveForwardFlushRange(); - boolean[] substitute = {true}; - DurableHistoryMarkerRangeEvidence evidence = new DurableHistoryMarkerRangeEvidence( - new DurableHistoryMarkerRangeEvidence.Source() { - @Override - public HistoryCommitMarker marker(long epoch) { - return substitute[0] - ? SnapshotOldValueCollectorTest.marker(BlockSnapshotMeta.forBlock( - 2, hash(2), hash(1), 2L)) : committed; - } - - @Override - public BlockReverseDiff readCommitted(long epoch) { - return new BlockReverseDiff(meta, Collections.emptyList()); - } - }, 1); - - assertThrows(ArchivePersistenceException.class, - () -> manager.sealPendingArchiveForwardFlush(evidence)); - assertSame(frozen, manager.freezeArchiveForwardFlushRange()); - substitute[0] = false; - manager.sealPendingArchiveForwardFlush(evidence); - assertTrue(manager.hasPendingArchiveForwardFlush()); - - manager.shutdown(); - - assertFalse(manager.hasPendingArchiveForwardFlush()); - assertThrows(IllegalStateException.class, manager::claimArchiveForwardFlushPayloads); - verify(projection).completeSeal(); - verify(projection, never()).abort(); - } - @Test public void flushPublishesOnlyTheNonRevertibleRange() throws Exception { MemoryDb memoryDb = new MemoryDb("code"); @@ -1204,6 +714,19 @@ public void flushPublishesOnlyTheNonRevertibleRange() throws Exception { } BlockReverseDiff second = prepared(database); setFlushCount(manager, 1); + HistoryCommitMarker committed = marker(first.getMeta()); + when(sink.createMarkerRangeEvidence(1)).thenReturn( + new DurableHistoryMarkerRangeEvidence(new DurableHistoryMarkerRangeEvidence.Source() { + @Override + public HistoryCommitMarker marker(long epoch) { + return committed; + } + + @Override + public BlockReverseDiff readCommitted(long epoch) { + return first; + } + }, 1)); manager.flush(); @@ -1241,46 +764,6 @@ private static void commitBlock(SnapshotManager manager, Chainbase database, } } - @SuppressWarnings("unchecked") - private static Map forwardOwners( - SnapshotManager manager) throws Exception { - java.lang.reflect.Field field = SnapshotManager.class.getDeclaredField( - "archiveForwardPayloadOwners"); - field.setAccessible(true); - return (Map) field.get(manager); - } - - private static void setBlockMeta(SnapshotImpl snapshot, BlockSnapshotMeta meta) - throws Exception { - java.lang.reflect.Field field = SnapshotImpl.class.getDeclaredField("blockSnapshotMeta"); - field.setAccessible(true); - field.set(snapshot, meta); - } - - private static AccountAssetBlockProjectionBridge.PreparedBlockProjection preparedProjection( - BlockSnapshotMeta meta, BlockReverseDiff reverse) { - AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = - mock(AccountAssetBlockProjectionBridge.PreparedBlockProjection.class); - when(projection.getMeta()).thenReturn(meta); - when(projection.getReverseDiff()).thenReturn(reverse); - return projection; - } - - private static AccountAssetBlockProjectionBridge.PreparedBlockProjection sealReadyProjection( - BlockSnapshotMeta meta, BlockChangeView view) { - AccountAssetBlockProjectionBridge.PreparedBlockProjection projection = - preparedProjection(meta, new BlockReverseDiff(meta, Collections.emptyList())); - when(projection.previewSealPayload(any(HistoryCommitMarker.class))).thenAnswer(invocation -> { - HistoryCommitMarker target = invocation.getArgument(0); - if (!meta.equals(target.getMeta())) { - throw new ArchivePersistenceException("Prepared block projection target mismatch"); - } - return new ArchiveBlockForwardPayload(target, view, - new AccountAssetForwardMutationManifest(target, Phase.P66_ON, Collections.emptyList())); - }); - return projection; - } - private static HistoryCommitMarker marker(BlockSnapshotMeta meta) { int epoch = (int) meta.getEpoch(); List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index b2cd37b3e79..a2711ec9f2f 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -8,12 +8,17 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.io.Closeable; +import com.google.protobuf.ByteString; +import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.AbstractMap; @@ -22,20 +27,29 @@ import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.mockito.ArgumentCaptor; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.Sha256Hash; import org.tron.core.ChainBaseManager; import org.tron.core.capsule.BlockCapsule; import org.tron.core.config.args.Storage; +import org.tron.core.config.args.StorageConfig; import org.tron.core.db.Manager; +import org.tron.core.db.TronDatabase; import org.tron.core.db.common.DbSourceInter; import org.tron.core.db2.ISession; -import org.tron.core.db2.archive.ArchiveProgressEnvelope.Kind; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; +import org.tron.core.db2.archive.StateArchiveRuntimeOwner.ServingIndexStage; import org.tron.core.db2.archive.StateArchiveRuntimeOwner.State; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.Flusher; @@ -43,23 +57,30 @@ import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.db2.core.SnapshotRoot; +import org.tron.core.store.AccountAssetStore; import org.tron.core.store.CheckTmpStore; import org.tron.core.store.DynamicPropertiesStore; +import org.tron.protos.Protocol.Account; public class StateArchiveManagerStartupIntegrationTest { private static final List PARTICIPANTS = participants(); + static { + org.rocksdb.RocksDB.loadLibrary(); + } + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test - public void automaticOverflowFlushKeepsForwardRegistryBoundToFullTopology() throws Exception { + public void automaticOverflowFlushAdvancesHistoryAndAllStoreServingIndex() throws Exception { Path output = temporaryFolder.newFolder("overflow-manager").toPath(); Path archive = output.resolve("state-archive"); HistoryCommitMarker head = initializeRecoverableTail(archive, "ROCKSDB"); SnapshotFixture fixture = snapshotFixture(); SnapshotManager snapshots = fixture.snapshots; + installRecoveredBinding(snapshots, archive, 6, 6); snapshots.setMaxSize(1); Manager manager = manager(snapshots, head); @@ -74,8 +95,9 @@ public void automaticOverflowFlushKeepsForwardRegistryBoundToFullTopology() thro block.commit(target); } if (epoch == 9) { - assertEquals(new BlockSnapshotMeta(7, 7, hash(7), hash(6), 7_000L), - manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + BlockSnapshotMeta durable = new BlockSnapshotMeta(7, 7, hash(7), hash(6), 7_000L); + assertEquals(durable, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertServingFixedPoint(archive, durable, 5); } } @@ -84,12 +106,14 @@ public void automaticOverflowFlushKeepsForwardRegistryBoundToFullTopology() thro } @Test - public void batchedAutomaticOverflowFlushUsesCompletePhysicalTopology() throws Exception { + public void batchedAutomaticOverflowFlushAdvancesHistoryAndAllStoreServingIndex() + throws Exception { Path output = temporaryFolder.newFolder("batched-overflow-manager").toPath(); Path archive = output.resolve("state-archive"); HistoryCommitMarker head = initializeRecoverableTail(archive, "ROCKSDB"); SnapshotFixture fixture = snapshotFixture(); SnapshotManager snapshots = fixture.snapshots; + installRecoveredBinding(snapshots, archive, 6, 6); snapshots.setMaxSize(1); snapshots.setMaxFlushCount(2); Manager manager = manager(snapshots, head); @@ -106,13 +130,84 @@ public void batchedAutomaticOverflowFlushUsesCompletePhysicalTopology() throws E } } - assertEquals(new BlockSnapshotMeta(8, 8, hash(8), hash(7), 8_000L), - manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); - assertEquals(2, snapshots.getArchiveForwardPayloadOwnerCount()); + BlockSnapshotMeta durable = new BlockSnapshotMeta(8, 8, hash(8), hash(7), 8_000L); + assertEquals(durable, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertServingFixedPoint(archive, durable, 5); invoke(manager, "closeStateArchive"); snapshots.shutdown(); } + @Test + public void servingIndexPublicationFailuresRetryWithoutResubmittingHistoryAndRestartCleanly() + throws Exception { + for (ServingIndexStage failureStage : ServingIndexStage.values()) { + Path output = temporaryFolder.newFolder( + "serving-failure-" + failureStage.name().toLowerCase()).toPath(); + Path archive = output.resolve("state-archive"); + HistoryCommitMarker head = initializeRecoverableTail(archive, "ROCKSDB"); + SnapshotFixture fixture = snapshotFixture(); + SnapshotManager snapshots = fixture.snapshots; + installRecoveredBinding(snapshots, archive, 6, 6); + Manager manager = manager(snapshots, head); + AtomicReference armed = new AtomicReference<>(); + setField(manager, "stateArchiveServingIndexFaultHook", + (StateArchiveRuntimeOwner.ServingIndexFaultHook) stage -> { + if (stage == armed.get() && armed.compareAndSet(stage, null)) { + throw new IOException("injected serving-index failure at " + stage); + } + }); + withArchiveConfig(output, "ROCKSDB", true, + () -> invoke(manager, "initStateArchive")); + + BlockSnapshotMeta target = new BlockSnapshotMeta(7, 7, hash(7), hash(6), 7_000L); + try (ISession block = snapshots.buildSession()) { + fixture.databases.get("proposal").put(new byte[]{7, 7}, new byte[]{7}); + block.commit(target); + } + setField(snapshots, "flushCount", 1); + armed.set(failureStage); + + assertThrows(org.tron.core.exception.TronError.class, snapshots::flushPending); + assertEquals(target, manager.getArchiveHistoryWriter().committedHeadMeta()); + assertEquals(6, snapshots.getArchiveReadableEpoch()); + assertThrows(ArchivePersistenceException.class, + () -> manager.getArchiveAccountBalance(6, + new byte[HistoricalAccountBalanceReader.ADDRESS_LENGTH])); + assertTrue(fixture.databases.values().stream() + .allMatch(database -> database.getHead() instanceof org.tron.core.db2.core.SnapshotImpl)); + + snapshots.flushPending(); + + assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertEquals(target.getEpoch(), snapshots.getArchiveReadableEpoch()); + assertFalse(manager.getArchiveAccountBalance(7, + new byte[HistoricalAccountBalanceReader.ADDRESS_LENGTH]).isPresent()); + assertServingFixedPoint(archive, target, 5); + assertEquals(1, countGenerationDirectories(archive)); + assertTrue(fixture.databases.values().stream() + .allMatch(database -> database.getHead() instanceof SnapshotRoot)); + @SuppressWarnings("unchecked") + ArgumentCaptor> checkpoints = ArgumentCaptor.forClass(Map.class); + verify(fixture.checkpoint, times(2)).updateByBatch(checkpoints.capture()); + Map recoveredCheckpoint = checkpoints.getAllValues().get(1); + + invoke(manager, "closeStateArchive"); + snapshots.shutdown(); + + SnapshotFixture restarted = snapshotFixture(recoveredCheckpoint); + invokeCheckpointRecovery(restarted.snapshots, restarted.checkpoint); + Manager restartedManager = manager(restarted.snapshots, target); + withArchiveConfig(output, "ROCKSDB", true, + () -> invoke(restartedManager, "initStateArchive")); + assertEquals(target, + restartedManager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertServingFixedPoint(archive, target, 5); + assertEquals(1, countGenerationDirectories(archive)); + invoke(restartedManager, "closeStateArchive"); + restarted.snapshots.shutdown(); + } + } + @Test public void managerBootstrapsFreshBaseAndContinuesNormalFlush() throws Exception { for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { @@ -128,7 +223,11 @@ public void managerBootstrapsFreshBaseAndContinuesNormalFlush() throws Exception assertEquals(State.RUNNING, manager.getStateArchiveRuntime().getState()); assertEquals(head, manager.getStateArchiveRuntime().getRecoveredHead()); assertEquals(0, manager.getStateArchiveRuntime().getStartupRecoveryActionCount()); + assertServingFixedPoint(archive, head, 6); + assertEquals(head.getEpoch(), snapshots.getArchiveReadableEpoch()); assertTrue(Files.isRegularFile(archive.resolve("MANIFEST"))); + assertFalse(Files.exists(archive.resolve("participants"))); + assertFalse(Files.exists(archive.resolve("progress"))); byte[] key = new byte[]{2, 7, 1, 8}; BlockSnapshotMeta target = new BlockSnapshotMeta(7, 7, hash(7), hash(6), 7_000L); @@ -139,6 +238,7 @@ public void managerBootstrapsFreshBaseAndContinuesNormalFlush() throws Exception setField(snapshots, "flushCount", 1); snapshots.flushPending(); assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertServingFixedPoint(archive, target, 6); try (PersistentServingKeyIndexGeneration serving = manager.getArchiveHistoryWriter() .buildServingGeneration(output.resolve("serving-" + engine.toLowerCase()), "fresh")) { assertEquals(6, serving.getIndexedFrom()); @@ -146,11 +246,350 @@ public void managerBootstrapsFreshBaseAndContinuesNormalFlush() throws Exception } invoke(manager, "closeStateArchive"); - assertNativeParticipantsReopen(archive, engine, PARTICIPANTS); + assertEquals(-1, snapshots.getArchiveReadableEpoch()); + assertFalse(Files.exists(archive.resolve("participants"))); + assertFalse(Files.exists(archive.resolve("progress"))); snapshots.shutdown(); } } + @Test + public void managerReadsRequestOwnedHistoricalAccountsAndDrainsBeforeRestart() + throws Exception { + Path output = temporaryFolder.newFolder("historical-account-manager").toPath(); + Path archive = output.resolve("state-archive"); + BlockSnapshotMeta head = new BlockSnapshotMeta(6, 6, hash(6), hash(5), 6_000L); + SnapshotFixture fixture = snapshotFixture(); + SnapshotManager snapshots = fixture.snapshots; + Manager manager = manager(snapshots, head); + withArchiveConfig(output, "ROCKSDB", true, () -> invoke(manager, "initStateArchive")); + + byte[] address = new byte[HistoricalAccountBalanceReader.ADDRESS_LENGTH]; + address[0] = 0x41; + assertFalse(manager.getArchiveAccountBalance(6, address).isPresent()); + + BlockSnapshotMeta target = null; + for (int epoch = 7; epoch <= 9; epoch++) { + target = new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L); + try (ISession block = snapshots.buildSession()) { + if (epoch == 7) { + fixture.databases.get("account").put(address, account(address, 10)); + } else if (epoch == 8) { + fixture.databases.get("account").put(address, account(address, 20)); + } else { + fixture.databases.get("account").delete(address); + } + block.commit(target); + } + setField(snapshots, "flushCount", 1); + snapshots.flushPending(); + } + + assertFalse(manager.getArchiveAccountBalance(6, address).isPresent()); + assertEquals(10, manager.getArchiveAccountBalance(7, address).getBalance()); + assertEquals(20, manager.getArchiveAccountBalance(8, address).getBalance()); + assertFalse(manager.getArchiveAccountBalance(9, address).isPresent()); + + try (ArchiveRuntimeQueryGate.Lease lease = + manager.getStateArchiveRuntime().pinHistoricalState(7)) { + assertThrows(IllegalStateException.class, () -> invoke(manager, "closeStateArchive")); + assertEquals(10, + HistoricalAccountBalanceReader.read(lease.getSnapshot(), address).getBalance()); + assertThrows(IllegalStateException.class, + () -> manager.getStateArchiveRuntime().pinHistoricalState(7)); + } + @SuppressWarnings("unchecked") + ArgumentCaptor> checkpoints = ArgumentCaptor.forClass(Map.class); + verify(fixture.checkpoint, times(3)).updateByBatch(checkpoints.capture()); + Map recoveredCheckpoint = checkpoints.getAllValues().get(2); + invoke(manager, "closeStateArchive"); + snapshots.shutdown(); + + SnapshotFixture restarted = snapshotFixture(recoveredCheckpoint); + invokeCheckpointRecovery(restarted.snapshots, restarted.checkpoint); + Manager restartedManager = manager(restarted.snapshots, target); + withArchiveConfig(output, "ROCKSDB", true, + () -> invoke(restartedManager, "initStateArchive")); + assertEquals(10, restartedManager.getArchiveAccountBalance(7, address).getBalance()); + assertEquals(20, restartedManager.getArchiveAccountBalance(8, address).getBalance()); + assertFalse(restartedManager.getArchiveAccountBalance(9, address).isPresent()); + invoke(restartedManager, "closeStateArchive"); + restarted.snapshots.shutdown(); + } + + @Test + public void managerReadsExactPointHistoryAcrossEveryStateStoreAndRestart() throws Exception { + Path output = temporaryFolder.newFolder("historical-exact-27-manager").toPath(); + Path archive = output.resolve("state-archive"); + BlockSnapshotMeta head = new BlockSnapshotMeta(6, 6, hash(6), hash(5), 6_000L); + SnapshotFixture fixture = snapshotFixture(); + SnapshotManager snapshots = fixture.snapshots; + Manager manager = manager(snapshots, head); + seedExact27State(fixture.databases, 6); + withArchiveConfig(output, "ROCKSDB", true, () -> invoke(manager, "initStateArchive")); + + assertExact27History(manager, 6); + BlockSnapshotMeta target = null; + for (int epoch = 7; epoch <= 8; epoch++) { + target = new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), + epoch * 1_000L); + try (ISession block = snapshots.buildSession()) { + int storeIndex = 0; + for (String dbName : PARTICIPANTS) { + Chainbase database = fixture.databases.get(dbName); + database.put(exact27Key(1, storeIndex), exact27Value(epoch, storeIndex)); + database.put(exact27Key(3, storeIndex), exact27Value(epoch, storeIndex)); + if (epoch == 7) { + database.delete(exact27Key(4, storeIndex)); + database.put(exact27Key(5, storeIndex), new byte[0]); + database.put(exact27Key(6, storeIndex), exact27Value(6, storeIndex)); + } + storeIndex++; + } + block.commit(target); + } + setField(snapshots, "flushCount", 1); + snapshots.flushPending(); + assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertExact27History(manager, epoch); + } + + assertExact27History(manager, 6); + assertExact27History(manager, 7); + assertExact27ServingIndex(archive); + assertThrows(IllegalArgumentException.class, + () -> manager.getArchiveStateValue(8, "block", new byte[]{1})); + @SuppressWarnings("unchecked") + ArgumentCaptor> checkpoints = ArgumentCaptor.forClass(Map.class); + verify(fixture.checkpoint, times(2)).updateByBatch(checkpoints.capture()); + Map recoveredCheckpoint = checkpoints.getAllValues().get(1); + invoke(manager, "closeStateArchive"); + snapshots.shutdown(); + + SnapshotFixture restarted = snapshotFixture(recoveredCheckpoint); + Manager restartedManager = manager(restarted.snapshots, target); + seedExact27State(restarted.databases, 8); + invokeCheckpointRecovery(restarted.snapshots, restarted.checkpoint); + withArchiveConfig(output, "ROCKSDB", true, + () -> invoke(restartedManager, "initStateArchive")); + assertExact27History(restartedManager, 6); + assertExact27History(restartedManager, 7); + assertExact27History(restartedManager, 8); + assertExact27ServingIndex(archive); + invoke(restartedManager, "closeStateArchive"); + restarted.snapshots.shutdown(); + } + + @Test + public void managerResolvesP66AccountAssetHistoryAndRejectsInvalidLayoutsAfterRestart() + throws Exception { + Path output = temporaryFolder.newFolder("historical-p66-manager").toPath(); + BlockSnapshotMeta head = new BlockSnapshotMeta(6, 6, hash(6), hash(5), 6_000L); + SnapshotFixture fixture = snapshotFixture(); + SnapshotManager snapshots = fixture.snapshots; + Manager manager = manager(snapshots, head); + byte[] address = archiveAddress(1); + byte[] absentAddress = archiveAddress(2); + byte[] orphanAddress = archiveAddress(3); + byte[] mixedAddress = archiveAddress(4); + String tokenId = "1000001"; + P66AccountAssetCodec codec = new P66AccountAssetCodec(); + byte[] directKey = codec.assetPhysicalKey(address, tokenId); + byte[] orphanKey = codec.assetPhysicalKey(orphanAddress, tokenId); + byte[] mixedKey = codec.assetPhysicalKey(mixedAddress, tokenId); + fixture.databases.get("properties").put( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), longValue(0)); + fixture.databases.get("account").put(address, + assetAccount(address, false, tokenId, 20)); + withArchiveConfig(output, "ROCKSDB", true, () -> invoke(manager, "initStateArchive")); + + assertAccountAsset(manager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertAccountAsset(manager, 6, absentAddress, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, false, 0); + + BlockSnapshotMeta target = null; + for (int epoch = 7; epoch <= 11; epoch++) { + target = new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), + epoch * 1_000L); + try (ISession block = snapshots.buildSession()) { + if (epoch == 7) { + fixture.databases.get("properties").put( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), longValue(1)); + fixture.databases.get("account").put(address, + assetAccount(address, true, null, 0)); + fixture.databases.get("account-asset").put(directKey, longValue(30)); + } else if (epoch == 8) { + fixture.databases.get("account-asset").put(directKey, longValue(40)); + } else if (epoch == 9) { + fixture.databases.get("account").delete(address); + fixture.databases.get("account-asset").delete(directKey); + } else if (epoch == 10) { + fixture.databases.get("account-asset").put(orphanKey, longValue(5)); + fixture.databases.get("account").put(mixedAddress, + assetAccount(mixedAddress, false, tokenId, 7)); + fixture.databases.get("account-asset").put(mixedKey, longValue(7)); + } else { + fixture.databases.get("account-asset").delete(orphanKey); + fixture.databases.get("account").delete(mixedAddress); + fixture.databases.get("account-asset").delete(mixedKey); + } + block.commit(target); + } + setField(snapshots, "flushCount", 1); + snapshots.flushPending(); + assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + } + + assertP66History(manager, address, absentAddress, tokenId); + assertThrows(ArchivePersistenceException.class, + () -> manager.getArchiveAccountAssetBalance(10, orphanAddress, tokenId)); + assertThrows(ArchivePersistenceException.class, + () -> manager.getArchiveAccountAssetBalance(10, mixedAddress, tokenId)); + assertAccountAsset(manager, 11, orphanAddress, tokenId, + P66AccountAssetCodec.Phase.P66_ON, false, 0); + assertAccountAsset(manager, 11, mixedAddress, tokenId, + P66AccountAssetCodec.Phase.P66_ON, false, 0); + + @SuppressWarnings("unchecked") + ArgumentCaptor> checkpoints = ArgumentCaptor.forClass(Map.class); + verify(fixture.checkpoint, times(5)).updateByBatch(checkpoints.capture()); + Map recoveredCheckpoint = checkpoints.getAllValues().get(4); + invoke(manager, "closeStateArchive"); + snapshots.shutdown(); + + SnapshotFixture restarted = snapshotFixture(recoveredCheckpoint); + Manager restartedManager = manager(restarted.snapshots, target); + restarted.databases.get("properties").put( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), longValue(1)); + invokeCheckpointRecovery(restarted.snapshots, restarted.checkpoint); + withArchiveConfig(output, "ROCKSDB", true, + () -> invoke(restartedManager, "initStateArchive")); + assertP66History(restartedManager, address, absentAddress, tokenId); + assertThrows(ArchivePersistenceException.class, + () -> restartedManager.getArchiveAccountAssetBalance(10, orphanAddress, tokenId)); + assertThrows(ArchivePersistenceException.class, + () -> restartedManager.getArchiveAccountAssetBalance(10, mixedAddress, tokenId)); + invoke(restartedManager, "closeStateArchive"); + restarted.snapshots.shutdown(); + } + + @Test + public void managerProjectsP66HistoryFromNativeSupplementalAccountAssetAndRestart() + throws Exception { + for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { + Path output = temporaryFolder.newFolder( + "supplemental-p66-" + engine.toLowerCase()).toPath(); + withArchiveConfig(output, engine, true, + () -> runSupplementalP66Scenario(output, engine)); + } + } + + private void runSupplementalP66Scenario(Path output, String engine) throws Exception { + Path archive = output.resolve("state-archive"); + BlockSnapshotMeta head = new BlockSnapshotMeta(6, 6, hash(6), hash(5), 6_000L); + SnapshotFixture fixture = snapshotFixtureWithoutAccountAsset(Collections.emptyMap()); + AtomicLong targetOptimization = new AtomicLong(); + TestAccountAssetStore accountAssetStore = new TestAccountAssetStore(); + Manager manager = manager(fixture.snapshots, head, accountAssetStore, targetOptimization); + byte[] address = archiveAddress(11); + String tokenId = "1000011"; + byte[] directKey = new P66AccountAssetCodec().assetPhysicalKey(address, tokenId); + fixture.databases.get("properties").put( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), longValue(0)); + fixture.databases.get("account").put(address, + assetAccount(address, false, tokenId, 20)); + invoke(manager, "initStateArchive"); + assertAccountAsset(manager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertFalse(accountAssetStore.has(directKey)); + + BlockSnapshotMeta target = null; + for (int epoch = 7; epoch <= 9; epoch++) { + targetOptimization.set(1); + target = new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), + epoch * 1_000L); + if (epoch == 8) { + accountAssetStore.failNextPrefixQuery(); + BlockSnapshotMeta failedTarget = target; + assertThrows(ArchivePersistenceException.class, () -> { + try (ISession block = fixture.snapshots.buildSession()) { + fixture.databases.get("account").put(address, + assetAccount(address, true, tokenId, 40)); + block.commit(failedTarget); + } + }); + assertEquals(7, manager.getArchiveHistoryWriter().committedHeadMeta().getEpoch()); + assertEquals(7, fixture.snapshots.getArchiveReadableEpoch()); + assertArrayEquals(longValue(30), accountAssetStore.get(directKey)); + } + try (ISession block = fixture.snapshots.buildSession()) { + if (epoch == 7) { + fixture.databases.get("properties").put( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), longValue(1)); + fixture.databases.get("account").put(address, + assetAccount(address, false, tokenId, 30)); + } else if (epoch == 8) { + fixture.databases.get("account").put(address, + assetAccount(address, true, tokenId, 40)); + } else { + fixture.databases.get("account").delete(address); + } + block.commit(target); + } + setField(fixture.snapshots, "flushCount", 1); + fixture.snapshots.flushPending(); + assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + if (epoch < 9) { + assertArrayEquals(longValue(epoch == 7 ? 30 : 40), accountAssetStore.get(directKey)); + } else { + assertFalse(accountAssetStore.has(directKey)); + } + } + + assertAccountAsset(manager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertAccountAsset(manager, 7, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 30); + assertAccountAsset(manager, 8, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 40); + assertAccountAsset(manager, 9, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, false, 0); + @SuppressWarnings("unchecked") + ArgumentCaptor> checkpoints = ArgumentCaptor.forClass(Map.class); + verify(fixture.checkpoint, times(3)).updateByBatch(checkpoints.capture()); + Map recoveredCheckpoint = checkpoints.getAllValues().get(2); + invoke(manager, "closeStateArchive"); + fixture.snapshots.shutdown(); + accountAssetStore.getDbSource().closeDB(); + + SnapshotFixture restarted = snapshotFixtureWithoutAccountAsset(recoveredCheckpoint); + targetOptimization.set(1); + TestAccountAssetStore reopenedAccountAssetStore = new TestAccountAssetStore(); + Manager restartedManager = manager(restarted.snapshots, target, reopenedAccountAssetStore, + targetOptimization); + restarted.databases.get("properties").put( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), longValue(1)); + invokeCheckpointRecovery(restarted.snapshots, restarted.checkpoint); + invoke(restartedManager, "initStateArchive"); + assertAccountAsset(restartedManager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertAccountAsset(restartedManager, 7, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 30); + assertAccountAsset(restartedManager, 8, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 40); + assertAccountAsset(restartedManager, 9, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, false, 0); + assertFalse(reopenedAccountAssetStore.has(directKey)); + assertEquals("LEVELDB".equals(engine), reopenedAccountAssetStore.getDbSource() + instanceof org.tron.common.storage.leveldb.LevelDbDataSourceImpl); + assertEquals("ROCKSDB".equals(engine), reopenedAccountAssetStore.getDbSource() + instanceof org.tron.common.storage.rocksdb.RocksDbDataSourceImpl); + invoke(restartedManager, "closeStateArchive"); + restarted.snapshots.shutdown(); + reopenedAccountAssetStore.getDbSource().closeDB(); + } + @Test public void managerRunsTwoNormalFlushTargetsThroughExact27FixedPoint() throws Exception { for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { @@ -159,15 +598,16 @@ public void managerRunsTwoNormalFlushTargetsThroughExact27FixedPoint() throws Ex HistoryCommitMarker head = initializeRecoverableTail(archive, engine); SnapshotFixture fixture = snapshotFixture(); SnapshotManager snapshots = fixture.snapshots; + installRecoveredBinding(snapshots, archive, 6, 6); Manager manager = manager(snapshots, head); withArchiveConfig(output, engine, true, () -> invoke(manager, "initStateArchive")); assertEquals(State.RUNNING, manager.getStateArchiveRuntime().getState()); - assertEquals(1, manager.getStateArchiveRuntime().getStartupRecoveryActionCount()); + assertEquals(0, manager.getStateArchiveRuntime().getStartupRecoveryActionCount()); assertEquals(head.getMeta(), manager.getStateArchiveRuntime().getRecoveredHead()); assertNotNull(manager.getArchiveHistoryWriter()); - assertEquals(-1, snapshots.getArchiveReadableEpoch()); + assertEquals(6, snapshots.getArchiveReadableEpoch()); byte[] key = new byte[]{3, 1, 4}; for (int epoch = 7; epoch <= 8; epoch++) { @@ -180,19 +620,16 @@ public void managerRunsTwoNormalFlushTargetsThroughExact27FixedPoint() throws Ex setField(snapshots, "flushCount", 1); snapshots.flushPending(); assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertServingFixedPoint(archive, target, 5); setField(snapshots, "size", 0); } invoke(manager, "closeStateArchive"); + assertEquals(-1, snapshots.getArchiveReadableEpoch()); assertNull(manager.getStateArchiveRuntime()); assertNull(manager.getArchiveHistoryWriter()); - assertNativeParticipantsReopen(archive, engine, PARTICIPANTS); - try (Closeable participant = (Closeable) openParticipant(archive, engine, "proposal")) { - byte[] value = engine.equals("ROCKSDB") - ? ((RocksDbArchiveParticipant) participant).get(key) - : ((LevelDbArchiveParticipant) participant).get(key); - assertArrayEquals(new byte[]{8}, value); - } + assertFalse(Files.exists(archive.resolve("participants"))); + assertFalse(Files.exists(archive.resolve("progress"))); snapshots.shutdown(); } } @@ -205,6 +642,7 @@ public void managerRunsMultiTargetNormalFlushThroughExact27FixedPoint() throws E HistoryCommitMarker head = initializeRecoverableTail(archive, engine); SnapshotFixture fixture = snapshotFixture(); SnapshotManager snapshots = fixture.snapshots; + installRecoveredBinding(snapshots, archive, 6, 6); Manager manager = manager(snapshots, head); withArchiveConfig(output, engine, true, () -> invoke(manager, "initStateArchive")); @@ -216,6 +654,10 @@ public void managerRunsMultiTargetNormalFlushThroughExact27FixedPoint() throws E hash(epoch - 1), epoch * 1_000L); try (ISession block = snapshots.buildSession()) { fixture.databases.get("proposal").put(key, new byte[]{(byte) epoch}); + fixture.databases.get("properties").put(new byte[]{1}, new byte[]{(byte) epoch}); + fixture.checkpointOnly.get("block").put(new byte[]{2}, new byte[]{(byte) epoch}); + fixture.checkpointOnly.get("block-index").put(new byte[]{3}, + new byte[]{(byte) epoch}); block.commit(target); } } @@ -224,44 +666,169 @@ public void managerRunsMultiTargetNormalFlushThroughExact27FixedPoint() throws E snapshots.flushPending(); assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertServingFixedPoint(archive, target, 5); + ArchiveWalBinding binding = snapshots.getLatestArchiveWalBinding(); + assertNotNull(binding); + assertEquals(7, binding.getFirst().getEpoch()); + assertEquals(8, binding.getLast().getEpoch()); + assertEquals(6, binding.getPredecessorEpoch()); + assertArrayEquals(hash(6), binding.getPredecessorHash()); + @SuppressWarnings("unchecked") + ArgumentCaptor> checkpointBatch = + ArgumentCaptor.forClass(Map.class); + verify(fixture.checkpoint).updateByBatch(checkpointBatch.capture()); + ArchiveWalBinding persisted = ArchiveWalBinding.fromCheckpointBatch( + checkpointBatch.getValue()); + assertNotNull(persisted); + assertArrayEquals(binding.getBatchDigest(), persisted.getBatchDigest()); + Set checkpointDatabases = checkpointDatabases(checkpointBatch.getValue()); + assertTrue(checkpointDatabases.contains("properties")); + assertTrue(checkpointDatabases.contains("proposal")); + assertTrue(checkpointDatabases.contains("block")); + assertTrue(checkpointDatabases.contains("block-index")); + assertTrue(checkpointDatabases.contains(ArchiveWalBinding.CHECKPOINT_DATABASE)); + SnapshotFixture recovered = snapshotFixture(checkpointBatch.getValue()); + invokeCheckpointRecovery(recovered.snapshots, recovered.checkpoint); + assertNotNull(recovered.snapshots.getRecoveredArchiveWalBinding()); + assertArrayEquals(binding.getBatchDigest(), + recovered.snapshots.getRecoveredArchiveWalBinding().getBatchDigest()); + assertArrayEquals(new byte[]{8}, recovered.databases.get("proposal").get(key)); + assertArrayEquals(new byte[]{8}, recovered.databases.get("properties").get(new byte[]{1})); + assertArrayEquals(new byte[]{8}, recovered.checkpointOnly.get("block").get(new byte[]{2})); + assertArrayEquals(new byte[]{8}, + recovered.checkpointOnly.get("block-index").get(new byte[]{3})); + invokeCheckpointRecovery(recovered.snapshots, recovered.checkpoint); + assertArrayEquals(binding.getBatchDigest(), + recovered.snapshots.getRecoveredArchiveWalBinding().getBatchDigest()); + recovered.snapshots.shutdown(); assertTrue(fixture.databases.values().stream() .allMatch(database -> database.getHead() instanceof SnapshotRoot)); invoke(manager, "closeStateArchive"); - assertNativeParticipantsReopen(archive, engine, PARTICIPANTS); - try (Closeable participant = (Closeable) openParticipant(archive, engine, "proposal")) { - byte[] value = engine.equals("ROCKSDB") - ? ((RocksDbArchiveParticipant) participant).get(key) - : ((LevelDbArchiveParticipant) participant).get(key); - assertArrayEquals(new byte[]{8}, value); - } + assertFalse(Files.exists(archive.resolve("participants"))); + assertFalse(Files.exists(archive.resolve("progress"))); snapshots.shutdown(); + + SnapshotFixture restarted = snapshotFixture(checkpointBatch.getValue()); + invokeCheckpointRecovery(restarted.snapshots, restarted.checkpoint); + BlockSnapshotMeta restartHead = target; + Manager restartedManager = manager(restarted.snapshots, restartHead); + withArchiveConfig(output, engine, true, + () -> invoke(restartedManager, "initStateArchive")); + assertEquals(restartHead, restartedManager.getStateArchiveRuntime().getRecoveredHead()); + assertEquals(0, + restartedManager.getStateArchiveRuntime().getStartupRecoveryActionCount()); + assertEquals(restartHead, + restartedManager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertServingFixedPoint(archive, restartHead, 5); + invoke(restartedManager, "closeStateArchive"); + restarted.snapshots.shutdown(); } } @Test - public void partialParticipantOpenRollsBackAndPreservesFailureEvidence() throws Exception { + public void newRuntimeDoesNotOpenLegacyParticipantEvidence() throws Exception { for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { Path output = temporaryFolder.newFolder("partial-" + engine.toLowerCase()).toPath(); Path archive = output.resolve("state-archive"); - HistoryCommitMarker head = initializeHistoryAndGlobalProgress(archive); - int failureIndex = 3; - List openedNames = PARTICIPANTS.subList(0, failureIndex); - initializeParticipants(archive, engine, openedNames, head); - Path failurePath = archive.resolve("participants").resolve(PARTICIPANTS.get(failureIndex)); + HistoryCommitMarker head = initializeHistory(archive, 7); + SnapshotManager snapshots = snapshotFixture().snapshots; + installRecoveredBinding(snapshots, archive, 7, 7); + Path failurePath = archive.resolve("participants").resolve(PARTICIPANTS.get(3)); + Files.createDirectories(failurePath.getParent()); byte[] evidence = new byte[]{4, 5, 6, 7}; Files.write(failurePath, evidence); - Manager manager = manager(new SnapshotManager(""), head); + Manager manager = manager(snapshots, head); - assertThrows(IllegalStateException.class, - () -> withArchiveConfig(output, engine, true, - () -> invoke(manager, "initStateArchive"))); + withArchiveConfig(output, engine, true, () -> invoke(manager, "initStateArchive")); - assertNull(manager.getStateArchiveRuntime()); + assertNotNull(manager.getStateArchiveRuntime()); assertArrayEquals(evidence, Files.readAllBytes(failurePath)); - assertNativeParticipantsReopen(archive, engine, openedNames); + invoke(manager, "closeStateArchive"); + snapshots.shutdown(); } } + @Test + public void missingWalBindingFailsBeforeNormalWriterAttachment() throws Exception { + Path output = temporaryFolder.newFolder("missing-wal-binding").toPath(); + Path archive = output.resolve("state-archive"); + HistoryCommitMarker head = initializeRecoverableTail(archive, "LEVELDB"); + SnapshotFixture fixture = snapshotFixture(); + Manager manager = manager(fixture.snapshots, head); + + assertThrows(IllegalStateException.class, + () -> withArchiveConfig(output, "LEVELDB", true, + () -> invoke(manager, "initStateArchive"))); + + assertNull(manager.getStateArchiveRuntime()); + assertFalse(Files.exists(archive.resolve("participants"))); + fixture.snapshots.shutdown(); + } + + @Test + public void substitutedWalBindingFailsBeforeNormalWriterAttachment() throws Exception { + Path output = temporaryFolder.newFolder("substituted-wal-binding").toPath(); + Path archive = output.resolve("state-archive"); + HistoryCommitMarker head = initializeRecoverableTail(archive, "ROCKSDB"); + SnapshotFixture fixture = snapshotFixture(); + ArchiveWalBinding valid = binding(archive, 6, 6); + byte[] substitutedRefs = valid.getHistoryRefsDigest(); + substitutedRefs[0] ^= 1; + ArchiveWalBinding substituted = new ArchiveWalBinding(valid.getFirst(), valid.getLast(), + valid.getPredecessorEpoch(), valid.getPredecessorHash(), valid.getBatchDigest(), + valid.getStoreScopeDigest(), substitutedRefs, valid.getBlockIndexRefsDigest()); + setField(fixture.snapshots, "recoveredArchiveWalBinding", substituted); + Manager manager = manager(fixture.snapshots, head); + + assertThrows(IllegalStateException.class, + () -> withArchiveConfig(output, "ROCKSDB", true, + () -> invoke(manager, "initStateArchive"))); + + assertNull(manager.getStateArchiveRuntime()); + assertFalse(Files.exists(archive.resolve("participants"))); + fixture.snapshots.shutdown(); + } + + @Test + public void substitutedAllStoreServingIndexFailsBeforeRuntimeAttachment() throws Exception { + Path output = temporaryFolder.newFolder("substituted-serving-index").toPath(); + Path archive = output.resolve("state-archive"); + HistoryCommitMarker head = initializeRecoverableTail(archive, "ROCKSDB"); + SnapshotFixture initial = snapshotFixture(); + installRecoveredBinding(initial.snapshots, archive, 6, 6); + Manager initialManager = manager(initial.snapshots, head); + withArchiveConfig(output, "ROCKSDB", true, + () -> invoke(initialManager, "initStateArchive")); + invoke(initialManager, "closeStateArchive"); + initial.snapshots.shutdown(); + + Path foreignArchive = temporaryFolder.newFolder("foreign-serving-history").toPath(); + Path foreignShadow = output.resolve("foreign-serving-shadow"); + try (ArchiveHistoryWriter foreign = new ArchiveHistoryWriter( + foreignArchive, 4096, ArchiveStoreScope.getStateDatabases())) { + foreign.accept(new BlockReverseDiff(head.getMeta(), Collections.singletonList( + new BlockReverseDiff.DbGroup("proposal", Collections.singletonList( + new BlockReverseDiff.Entry(new byte[]{9, 9}, OldValue.absent())))))); + try (PersistentServingKeyIndexGeneration ignored = + foreign.buildServingGeneration(foreignShadow, "foreign")) { + // Catalog publication reopens the generation after this build handle is closed. + } + } + try (PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.open(archive.resolve("serving-index"))) { + assertTrue(catalog.publish(catalog.getCurrentGenerationId(), foreignShadow)); + } + + SnapshotFixture restarted = snapshotFixture(); + installRecoveredBinding(restarted.snapshots, archive, 6, 6); + Manager restartedManager = manager(restarted.snapshots, head); + assertThrows(IllegalStateException.class, + () -> withArchiveConfig(output, "ROCKSDB", true, + () -> invoke(restartedManager, "initStateArchive"))); + assertNull(restartedManager.getStateArchiveRuntime()); + restarted.snapshots.shutdown(); + } + @Test public void disabledManagerControlDoesNotInspectOrCreateArchiveRuntime() throws Exception { Path output = temporaryFolder.newFolder("disabled-manager").toPath(); @@ -285,12 +852,24 @@ private static Manager manager(SnapshotManager snapshots, HistoryCommitMarker he private static Manager manager(SnapshotManager snapshots, BlockSnapshotMeta head) throws Exception { + return manager(snapshots, head, null, null); + } + + private static Manager manager(SnapshotManager snapshots, BlockSnapshotMeta head, + AccountAssetStore accountAssetStore, AtomicLong targetOptimization) throws Exception { DynamicPropertiesStore properties = mock(DynamicPropertiesStore.class); when(properties.getLatestBlockHeaderNumber()).thenReturn(head.getBlockNumber()); when(properties.getLatestBlockHeaderHash()) .thenReturn(Sha256Hash.wrap(head.getBlockHash())); ChainBaseManager chainBase = mock(ChainBaseManager.class); when(chainBase.getDynamicPropertiesStore()).thenReturn(properties); + if (accountAssetStore != null) { + when(chainBase.getAccountAssetStore()).thenReturn(accountAssetStore); + when(properties.getAllowAccountAssetOptimizationFromRoot()) + .thenAnswer(ignored -> targetOptimization.get()); + when(properties.supportAllowAssetOptimization()) + .thenAnswer(ignored -> targetOptimization.get() == 1); + } BlockCapsule headBlock = mock(BlockCapsule.class); when(headBlock.getParentHash()).thenReturn(Sha256Hash.wrap(head.getParentHash())); when(headBlock.getTimeStamp()).thenReturn(head.getTimestamp()); @@ -303,116 +882,108 @@ private static Manager manager(SnapshotManager snapshots, BlockSnapshotMeta head } private static SnapshotFixture snapshotFixture() { + return snapshotFixture(Collections.emptyMap()); + } + + private static SnapshotFixture snapshotFixture(Map checkpointEntries) { + return snapshotFixture(checkpointEntries, true); + } + + private static SnapshotFixture snapshotFixture(Map checkpointEntries, + boolean includeAccountAsset) { if (CommonParameter.getInstance().getStorage() == null) { CommonParameter.getInstance().storage = new Storage(); } SnapshotManager snapshots = new SnapshotManager(""); Map databases = new LinkedHashMap<>(); for (String participant : PARTICIPANTS) { - if (AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(participant)) { + if (!includeAccountAsset + && AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(participant)) { continue; } Chainbase database = new Chainbase(new SnapshotRoot(new MemoryDb(participant))); snapshots.add(database); databases.put(participant, database); } + Map checkpointOnly = new LinkedHashMap<>(); + for (String name : Arrays.asList("block", "block-index")) { + Chainbase database = new Chainbase(new SnapshotRoot(new MemoryDb(name))); + snapshots.add(database); + checkpointOnly.put(name, database); + } snapshots.enable(); snapshots.setUnChecked(false); CheckTmpStore checkpoint = mock(CheckTmpStore.class); @SuppressWarnings("unchecked") DbSourceInter checkpointDb = mock(DbSourceInter.class); - when(checkpointDb.iterator()).thenReturn(Collections.emptyIterator()); + when(checkpointDb.iterator()).thenAnswer( + ignored -> checkpointEntries.entrySet().iterator()); when(checkpoint.getDbSource()).thenReturn(checkpointDb); snapshots.setCheckTmpStore(checkpoint); - return new SnapshotFixture(snapshots, databases); + return new SnapshotFixture(snapshots, databases, checkpointOnly, checkpoint); + } + + private static SnapshotFixture snapshotFixtureWithoutAccountAsset( + Map checkpointEntries) { + return snapshotFixture(checkpointEntries, false); } private static HistoryCommitMarker initializeRecoverableTail(Path archive, String engine) throws Exception { - HistoryCommitMarker checkpoint; + return initializeHistory(archive, 6); + } + + private static void assertServingFixedPoint(Path archive, BlockSnapshotMeta expected, + long expectedFrom) throws Exception { + try (PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.open(archive.resolve("serving-index")); + PersistentServingKeyIndexGeneration generation = catalog.pin()) { + assertEquals(expectedFrom, generation.getIndexedFrom()); + assertEquals(expected.getEpoch(), generation.getIndexedThrough()); + assertArrayEquals(expected.getBlockHash(), generation.getHeadHash()); + assertEquals(PARTICIPANTS, generation.getParticipatingDatabases()); + assertTrue(generation.isLatestSourceIdentityBound()); + } + } + + private static long countGenerationDirectories(Path archive) throws IOException { + try (java.util.stream.Stream entries = + Files.list(archive.resolve("serving-index").resolve("generations"))) { + return entries.filter(Files::isDirectory).count(); + } + } + + private static void installRecoveredBinding(SnapshotManager snapshots, Path archive, + long firstEpoch, long lastEpoch) throws Exception { + setField(snapshots, "recoveredArchiveWalBinding", + binding(archive, firstEpoch, lastEpoch)); + } + + private static ArchiveWalBinding binding(Path archive, long firstEpoch, long lastEpoch) + throws Exception { + List markers = new ArrayList<>(); try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( archive, 4096, ArchiveStoreScope.getStateDatabases())) { - writer.accept(new BlockReverseDiff( - new BlockSnapshotMeta(6, 6, hash(6), hash(5), 6_000L), - Collections.emptyList())); - writer.accept(new BlockReverseDiff( - new BlockSnapshotMeta(7, 7, hash(7), hash(6), 7_000L), - Collections.emptyList())); - checkpoint = writer.get(6); + for (long epoch = firstEpoch; epoch <= lastEpoch; epoch++) { + markers.add(writer.get(epoch)); + } } - ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - new ArchiveProgressFile(archive.resolve("progress/checkpoint.progress"), codec) - .store(global(Kind.APPLY_CHECKPOINT, checkpoint)); - new ArchiveProgressFile(archive.resolve("progress/reader.progress"), codec) - .store(global(Kind.READER_VISIBLE, checkpoint)); - initializeParticipants(archive, engine, PARTICIPANTS, checkpoint); - return checkpoint; + return ArchiveWalBinding.fromMarkers(markers); } - private static HistoryCommitMarker initializeHistoryAndGlobalProgress(Path archive) + private static HistoryCommitMarker initializeHistory(Path archive, int epoch) throws Exception { HistoryCommitMarker head; try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( archive, 4096, ArchiveStoreScope.getStateDatabases())) { writer.accept(new BlockReverseDiff( - new BlockSnapshotMeta(7, 7, hash(7), hash(6), 7_000L), + new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), epoch * 1_000L), Collections.emptyList())); head = writer.committedHead(); } - ArchiveProgressEnvelopeCodec codec = new ArchiveProgressEnvelopeCodec(); - new ArchiveProgressFile(archive.resolve("progress/checkpoint.progress"), codec) - .store(global(Kind.APPLY_CHECKPOINT, head)); - new ArchiveProgressFile(archive.resolve("progress/reader.progress"), codec) - .store(global(Kind.READER_VISIBLE, head)); return head; } - private static void initializeParticipants(Path archive, String engine, List names, - HistoryCommitMarker head) throws Exception { - List opened = new ArrayList<>(); - try { - for (String name : names) { - ArchiveParticipant participant = openParticipant(archive, engine, name); - opened.add((Closeable) participant); - participant.apply(Collections.emptyList(), participant(name, head)); - } - } finally { - closeReverse(opened); - } - } - - private static void assertNativeParticipantsReopen(Path archive, String engine, - List names) throws Exception { - List reopened = new ArrayList<>(); - try { - for (String name : names) { - reopened.add((Closeable) openParticipant(archive, engine, name)); - } - } finally { - closeReverse(reopened); - } - } - - private static ArchiveParticipant openParticipant(Path archive, String engine, String name) - throws Exception { - Path directory = archive.resolve("participants").resolve(name); - return "ROCKSDB".equals(engine) - ? new RocksDbArchiveParticipant(directory, name, PARTICIPANTS) - : new LevelDbArchiveParticipant(directory, name, PARTICIPANTS); - } - - private static ArchiveProgressEnvelope participant(String name, HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(Kind.PARTICIPANT_PROGRESS, name, - marker.getMeta().getEpoch(), marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - } - - private static ArchiveProgressEnvelope global(Kind kind, HistoryCommitMarker marker) { - return new ArchiveProgressEnvelope(kind, null, marker.getMeta().getEpoch(), - marker.getMeta().getBlockHash(), marker.getBatchId(), - marker.getHistoryLocation().getBodyDigest(), PARTICIPANTS); - } - private static void withArchiveConfig(Path output, String engine, boolean enabled, ThrowingRunnable action) throws Exception { CommonParameter args = CommonParameter.getInstance(); @@ -421,6 +992,7 @@ private static void withArchiveConfig(Path output, String engine, boolean enable args.storage = storage; String oldOutput = args.outputDirectory; String oldDirectory = storage.getStateArchiveDirectory(); + String oldDbDirectory = storage.getDbDirectory(); String oldEngine = storage.getDbEngine(); long oldSegmentSize = storage.getStateArchiveMaxSegmentSize(); int oldQueueCapacity = storage.getStateArchiveQueueCapacity(); @@ -428,7 +1000,9 @@ private static void withArchiveConfig(Path output, String engine, boolean enable try { args.outputDirectory = output.toString(); storage.setStateArchiveDirectory("state-archive"); + storage.setDbDirectory("database"); storage.setDbEngine(engine); + storage.setDefaultDbOptions(new StorageConfig()); storage.setStateArchiveMaxSegmentSize(4096); storage.setStateArchiveQueueCapacity(4); storage.setStateArchiveEnabled(enabled); @@ -436,6 +1010,7 @@ private static void withArchiveConfig(Path output, String engine, boolean enable } finally { args.outputDirectory = oldOutput; storage.setStateArchiveDirectory(oldDirectory); + storage.setDbDirectory(oldDbDirectory); storage.setDbEngine(oldEngine); storage.setStateArchiveMaxSegmentSize(oldSegmentSize); storage.setStateArchiveQueueCapacity(oldQueueCapacity); @@ -444,24 +1019,6 @@ private static void withArchiveConfig(Path output, String engine, boolean enable } } - private static void closeReverse(List resources) throws Exception { - Exception failure = null; - for (int i = resources.size() - 1; i >= 0; i--) { - try { - resources.get(i).close(); - } catch (Exception closeFailure) { - if (failure == null) { - failure = closeFailure; - } else { - failure.addSuppressed(closeFailure); - } - } - } - if (failure != null) { - throw failure; - } - } - private static void setField(Object target, String name, Object value) throws Exception { Field field = target.getClass().getDeclaredField(name); field.setAccessible(true); @@ -482,6 +1039,21 @@ private static void invoke(Manager manager, String name) throws Exception { } } + private static void invokeCheckpointRecovery(SnapshotManager snapshots, + TronDatabase checkpoint) throws Exception { + Method method = SnapshotManager.class.getDeclaredMethod("recover", TronDatabase.class); + method.setAccessible(true); + try { + method.invoke(snapshots, checkpoint); + } catch (InvocationTargetException failure) { + Throwable cause = failure.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw failure; + } + } + private static List participants() { List participants = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); Collections.sort(participants); @@ -494,17 +1066,184 @@ private static byte[] hash(int suffix) { return hash; } + private static byte[] account(byte[] address, long balance) { + return Account.newBuilder().setAddress(ByteString.copyFrom(address)).setBalance(balance) + .build().toByteArray(); + } + + private static byte[] assetAccount(byte[] address, boolean optimized, String tokenId, + long balance) { + Account.Builder builder = Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .setAssetOptimized(optimized); + if (tokenId != null) { + builder.putAssetV2(tokenId, balance); + } + return builder.build().toByteArray(); + } + + private static void assertP66History(Manager manager, byte[] address, byte[] absentAddress, + String tokenId) { + assertAccountAsset(manager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertAccountAsset(manager, 6, absentAddress, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, false, 0); + assertAccountAsset(manager, 7, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 30); + assertAccountAsset(manager, 8, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 40); + assertAccountAsset(manager, 9, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, false, 0); + } + + private static void assertAccountAsset(Manager manager, int targetEpoch, byte[] address, + String tokenId, P66AccountAssetCodec.Phase phase, boolean present, long balance) { + HistoricalAccountAssetBalanceResolver.Result result = + manager.getArchiveAccountAssetBalance(targetEpoch, address, tokenId); + assertEquals(targetEpoch, result.getBlockNumber()); + assertArrayEquals(address, result.getAddress()); + assertEquals(tokenId, result.getTokenId()); + assertEquals(phase, result.getPhase()); + assertEquals(present, result.isAccountPresent()); + if (present) { + assertEquals(balance, result.getBalance()); + } + } + + private static byte[] archiveAddress(int suffix) { + byte[] address = new byte[HistoricalAccountBalanceReader.ADDRESS_LENGTH]; + address[0] = 0x41; + address[address.length - 1] = (byte) suffix; + return address; + } + + private static byte[] longValue(long value) { + return ByteBuffer.allocate(Long.BYTES).putLong(value).array(); + } + + private static void seedExact27State(Map databases, int latestEpoch) { + int storeIndex = 0; + for (String dbName : PARTICIPANTS) { + Chainbase database = databases.get(dbName); + database.put(exact27Key(1, storeIndex), exact27Value(latestEpoch, storeIndex)); + database.put(exact27Key(2, storeIndex), exact27Value(6, storeIndex)); + if (latestEpoch == 6) { + database.put(exact27Key(4, storeIndex), exact27Value(6, storeIndex)); + } else { + database.put(exact27Key(3, storeIndex), exact27Value(latestEpoch, storeIndex)); + database.put(exact27Key(5, storeIndex), new byte[0]); + } + database.put(exact27Key(6, storeIndex), exact27Value(6, storeIndex)); + storeIndex++; + } + } + + private static void assertExact27History(Manager manager, int targetEpoch) { + int storeIndex = 0; + for (String dbName : PARTICIPANTS) { + assertHistoricalValue(manager, targetEpoch, dbName, exact27Key(1, storeIndex), + exact27Value(targetEpoch, storeIndex)); + assertHistoricalValue(manager, targetEpoch, dbName, exact27Key(2, storeIndex), + exact27Value(6, storeIndex)); + assertHistoricalValue(manager, targetEpoch, dbName, exact27Key(3, storeIndex), + targetEpoch == 6 ? null : exact27Value(targetEpoch, storeIndex)); + assertHistoricalValue(manager, targetEpoch, dbName, exact27Key(4, storeIndex), + targetEpoch == 6 ? exact27Value(6, storeIndex) : null); + assertHistoricalValue(manager, targetEpoch, dbName, exact27Key(5, storeIndex), + targetEpoch == 6 ? null : new byte[0]); + assertHistoricalValue(manager, targetEpoch, dbName, exact27Key(6, storeIndex), + exact27Value(6, storeIndex)); + storeIndex++; + } + } + + private static void assertHistoricalValue(Manager manager, int targetEpoch, String dbName, + byte[] physicalRawKey, byte[] expected) { + OldValue actual = manager.getArchiveStateValue(targetEpoch, dbName, physicalRawKey); + assertEquals(expected != null, actual.isPresent()); + assertEquals(expected != null, + manager.hasArchiveStateValue(targetEpoch, dbName, physicalRawKey)); + if (expected != null) { + assertArrayEquals(expected, actual.getValue()); + } + } + + private static void assertExact27ServingIndex(Path archive) throws IOException { + try (PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.open(archive.resolve("serving-index")); + PersistentServingKeyIndexGeneration generation = catalog.pin()) { + int storeIndex = 0; + for (String dbName : PARTICIPANTS) { + for (int changedKind : Arrays.asList(1, 3, 4, 5)) { + assertEquals(7, generation.firstChangeAfter(dbName, + exact27Key(changedKind, storeIndex), 6, 8).getAsLong()); + } + assertFalse(generation.firstChangeAfter(dbName, + exact27Key(2, storeIndex), 6, 8).isPresent()); + assertFalse(generation.firstChangeAfter(dbName, + exact27Key(6, storeIndex), 6, 8).isPresent()); + storeIndex++; + } + } + } + + private static byte[] exact27Key(int kind, int storeIndex) { + return new byte[]{(byte) 0xa1, (byte) kind, (byte) storeIndex}; + } + + private static byte[] exact27Value(int epoch, int storeIndex) { + return new byte[]{(byte) 0xb1, (byte) epoch, (byte) storeIndex}; + } + + private static Set checkpointDatabases(Map batch) { + Set databases = new LinkedHashSet<>(); + for (byte[] key : batch.keySet()) { + ByteBuffer input = ByteBuffer.wrap(key); + int length = input.getInt(); + byte[] database = new byte[length]; + input.get(database); + databases.add(new String(database, StandardCharsets.UTF_8)); + } + return databases; + } + private static final class SnapshotFixture { private final SnapshotManager snapshots; private final Map databases; + private final Map checkpointOnly; + private final CheckTmpStore checkpoint; - private SnapshotFixture(SnapshotManager snapshots, Map databases) { + private SnapshotFixture(SnapshotManager snapshots, Map databases, + Map checkpointOnly, CheckTmpStore checkpoint) { this.snapshots = snapshots; this.databases = databases; + this.checkpointOnly = checkpointOnly; + this.checkpoint = checkpoint; + } + } + + private static final class TestAccountAssetStore extends AccountAssetStore { + private boolean failPrefixQuery; + + private TestAccountAssetStore() { + super(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB); + } + + private void failNextPrefixQuery() { + failPrefixQuery = true; + } + + @Override + public Map prefixQuery(byte[] key) { + if (failPrefixQuery) { + failPrefixQuery = false; + throw new ArchivePersistenceException("injected supplemental prefix failure"); + } + return super.prefixQuery(key); } } - private static final class MemoryDb implements DB, Flusher { + private static final class MemoryDb implements DB, Flusher, + SnapshotCapableStore { private final String name; private final Map values = new LinkedHashMap<>(); @@ -572,6 +1311,51 @@ public String getDbName() { return name; } + @Override + public String getSourceIdentity() { + return "memory:" + name; + } + + @Override + public StoreSnapshot pin(long blockNumber, byte[] blockHash) { + Map pinned = new LinkedHashMap<>(); + values.forEach((key, value) -> pinned.put(WrappedByteArray.copyOf(key.getBytes()), + Arrays.copyOf(value, value.length))); + byte[] pinnedHash = Arrays.copyOf(blockHash, blockHash.length); + return new StoreSnapshot() { + @Override + public String getDbName() { + return name; + } + + @Override + public String getSourceIdentity() { + return MemoryDb.this.getSourceIdentity(); + } + + @Override + public long getBlockNumber() { + return blockNumber; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(pinnedHash, pinnedHash.length); + } + + @Override + public byte[] get(byte[] physicalRawKey) { + byte[] value = pinned.get(WrappedByteArray.of(physicalRawKey)); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + @Override + public void close() { + pinned.clear(); + } + }; + } + @Override public void stat() { } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java index 46fd54e3d52..14f868c0d99 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveRuntimeOwnerTest.java @@ -25,7 +25,6 @@ import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedHistory; import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; import org.tron.core.db2.archive.ArchiveRuntimeQueryGate.Lease; -import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; import org.tron.core.db2.archive.StateArchiveRuntimeOwner.State; import org.tron.core.db2.core.SnapshotManager; @@ -43,21 +42,22 @@ public void freshBootstrapPublishesRecoverableExact27FixedPoint() throws Excepti SnapshotManager snapshots = new SnapshotManager(""); try (StateArchiveRuntimeOwner owner = StateArchiveRuntimeOwner.bootstrapAndRecover( - snapshots, archive, 4096, engine, head, Phase.P66_ON)) { + snapshots, archive, 4096, head)) { assertEquals(head, owner.getRecoveredHead()); assertEquals(0, owner.getStartupRecoveryActionCount()); assertEquals(State.RECOVERED, owner.getState()); } assertTrue(Files.isRegularFile(archive.resolve("MANIFEST"))); - assertTrue(Files.isRegularFile(archive.resolve("progress/checkpoint.progress"))); - assertTrue(Files.isRegularFile(archive.resolve("progress/reader.progress"))); + assertTrue(Files.isRegularFile(archive.resolve("bootstrap.anchor"))); + assertFalse(Files.exists(archive.resolve("progress"))); + assertFalse(Files.exists(archive.resolve("participants"))); try (java.util.stream.Stream entries = Files.list(parent)) { assertFalse(entries.anyMatch(path -> path.getFileName().toString() .startsWith(".state-archive.bootstrap-"))); } try (StateArchiveRuntimeOwner reopened = StateArchiveRuntimeOwner.recover( - snapshots, archive, 4096, engine)) { + snapshots, archive, 4096)) { assertEquals(head, reopened.getRecoveredHead()); assertEquals(0, reopened.getStartupRecoveryActionCount()); } @@ -70,7 +70,7 @@ public void freshBootstrapPublishesRecoverableExact27FixedPoint() throws Excepti } assertThrows(ArchivePersistenceException.class, () -> StateArchiveRuntimeOwner.bootstrapAndRecover(snapshots, archive, 4096, - engine, head, Phase.P66_ON)); + head)); } } @@ -162,8 +162,7 @@ private static SnapshotManager manager() { } private static ArchiveRuntimeAttachment attachment(DurableBlockReverseDiffSink sink) { - return new ArchiveRuntimeAttachment(mock(OldValueCollector.class), - mock(ArchiveBlockProjectionPreparer.class), sink); + return new ArchiveRuntimeAttachment(mock(OldValueCollector.class), sink); } private static ArchiveReadSnapshot snapshot() throws IOException { From 93f7b4d76496faf491468842adc3190edd8dd2d1 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 25 Aug 2026 11:52:29 +0800 Subject: [PATCH 061/161] fix(common): handle short market price keys --- .../org/tron/common/utils/DBKeyComparatorTest.java | 14 ++++++++++++++ .../org/tron/common/utils/MarketComparator.java | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/framework/src/test/java/org/tron/common/utils/DBKeyComparatorTest.java b/framework/src/test/java/org/tron/common/utils/DBKeyComparatorTest.java index e9eb0f23ad3..ccc18b7290b 100644 --- a/framework/src/test/java/org/tron/common/utils/DBKeyComparatorTest.java +++ b/framework/src/test/java/org/tron/common/utils/DBKeyComparatorTest.java @@ -49,6 +49,20 @@ public void pairKeyIsEqual() { Assert.assertFalse(MarketUtils.pairKeyIsEqual(pairPriceKey1, pairPriceKey2)); } + @Test + public void shortInternalKeysUseDeterministicUnsignedOrdering() { + byte[] empty = new byte[0]; + byte[] shortKey = new byte[]{1, (byte) 0xff}; + byte[] valid = MarketUtils.createPairPriceKey( + ByteArray.fromString("100"), ByteArray.fromString("200"), 1000L, 2000L); + + Assert.assertEquals(0, MarketComparator.comparePriceKey(empty, empty)); + Assert.assertEquals(-1, MarketComparator.comparePriceKey(empty, shortKey)); + Assert.assertEquals(1, MarketComparator.comparePriceKey(shortKey, empty)); + Assert.assertTrue(MarketComparator.comparePriceKey(shortKey, valid) < 0); + Assert.assertTrue(MarketComparator.comparePriceKey(valid, shortKey) > 0); + } + } diff --git a/platform/src/main/java/common/org/tron/common/utils/MarketComparator.java b/platform/src/main/java/common/org/tron/common/utils/MarketComparator.java index c2742d4d10b..7c87bb57db8 100644 --- a/platform/src/main/java/common/org/tron/common/utils/MarketComparator.java +++ b/platform/src/main/java/common/org/tron/common/utils/MarketComparator.java @@ -5,9 +5,14 @@ public class MarketComparator { public static final int TOKEN_ID_LENGTH = Long.toString(Long.MAX_VALUE).getBytes().length; // 19 + private static final int PRICE_KEY_LENGTH = TOKEN_ID_LENGTH * 2 + Long.BYTES * 2; public static int comparePriceKey(byte[] o1, byte[] o2) { + if (o1 == null || o2 == null + || o1.length < PRICE_KEY_LENGTH || o2.length < PRICE_KEY_LENGTH) { + return compareUnsigned(o1, o2); + } //compare pair byte[] pair1 = new byte[TOKEN_ID_LENGTH * 2]; byte[] pair2 = new byte[TOKEN_ID_LENGTH * 2]; From 39b7a802cebc6677856820ad9ff6757778bf4348 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 25 Aug 2026 11:52:34 +0800 Subject: [PATCH 062/161] fix(chainbase): guard archive readable state --- .../db2/archive/StateArchiveRuntimeOwner.java | 44 +- .../main/java/org/tron/core/db/Manager.java | 6 +- ...eArchiveManagerStartupIntegrationTest.java | 420 +++++++++++++++++- 3 files changed, 463 insertions(+), 7 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java index c9f28f83839..da5276e9aa3 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java @@ -34,6 +34,18 @@ public interface ServingIndexFaultHook { void afterStage(ServingIndexStage stage) throws IOException; } + public enum ReadableStateStage { + CANONICAL_REFRESHED, + LATEST_PUBLISHED, + READABLE_PUBLISHED + } + + @FunctionalInterface + public interface ReadableStateFaultHook { + + void afterStage(ReadableStateStage stage) throws IOException; + } + public enum State { RECOVERED, RUNNING, @@ -49,6 +61,7 @@ public enum State { private final BlockSnapshotMeta recoveredHead; private final int startupRecoveryActionCount; private final ServingIndexFaultHook servingIndexFaultHook; + private final ReadableStateFaultHook readableStateFaultHook; private ArchiveRuntimeAttachment attachment; private ArchiveRuntimeQueryGate queryGate; private Closeable latestCoordinator; @@ -86,13 +99,15 @@ public StateArchiveRuntimeOwner(SnapshotManager snapshotManager, this.recoveredHead = null; this.startupRecoveryActionCount = 0; this.servingIndexFaultHook = stage -> { }; + this.readableStateFaultHook = stage -> { }; this.state = State.RUNNING; validateUniqueOwnership(); } private StateArchiveRuntimeOwner(SnapshotManager snapshotManager, Path archiveDirectory, long maxSegmentSize, BlockSnapshotMeta recoveredHead, - int startupRecoveryActionCount, ServingIndexFaultHook servingIndexFaultHook) { + int startupRecoveryActionCount, ServingIndexFaultHook servingIndexFaultHook, + ReadableStateFaultHook readableStateFaultHook) { this.snapshotManager = Objects.requireNonNull(snapshotManager, "snapshotManager"); this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); this.maxSegmentSize = maxSegmentSize; @@ -110,6 +125,8 @@ private StateArchiveRuntimeOwner(SnapshotManager snapshotManager, this.startupRecoveryActionCount = startupRecoveryActionCount; this.servingIndexFaultHook = Objects.requireNonNull(servingIndexFaultHook, "servingIndexFaultHook"); + this.readableStateFaultHook = Objects.requireNonNull(readableStateFaultHook, + "readableStateFaultHook"); this.state = State.RECOVERED; } @@ -122,6 +139,13 @@ public static StateArchiveRuntimeOwner recover(SnapshotManager snapshotManager, public static StateArchiveRuntimeOwner recover(SnapshotManager snapshotManager, Path archiveDirectory, long maxSegmentSize, ServingIndexFaultHook servingIndexFaultHook) throws IOException { + return recover(snapshotManager, archiveDirectory, maxSegmentSize, servingIndexFaultHook, + stage -> { }); + } + + public static StateArchiveRuntimeOwner recover(SnapshotManager snapshotManager, + Path archiveDirectory, long maxSegmentSize, ServingIndexFaultHook servingIndexFaultHook, + ReadableStateFaultHook readableStateFaultHook) throws IOException { Objects.requireNonNull(snapshotManager, "snapshotManager"); Path root = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); HistoryCommitMarker head; @@ -133,7 +157,7 @@ public static StateArchiveRuntimeOwner recover(SnapshotManager snapshotManager, throw new ArchivePersistenceException("State Archive recovered H head is missing"); } return new StateArchiveRuntimeOwner(snapshotManager, root, maxSegmentSize, head.getMeta(), 0, - servingIndexFaultHook); + servingIndexFaultHook, readableStateFaultHook); } /** @@ -149,6 +173,14 @@ public static StateArchiveRuntimeOwner bootstrapAndRecover(SnapshotManager snaps public static StateArchiveRuntimeOwner bootstrapAndRecover(SnapshotManager snapshotManager, Path archiveDirectory, long maxSegmentSize, BlockSnapshotMeta baseHead, ServingIndexFaultHook servingIndexFaultHook) throws IOException { + return bootstrapAndRecover(snapshotManager, archiveDirectory, maxSegmentSize, baseHead, + servingIndexFaultHook, stage -> { }); + } + + public static StateArchiveRuntimeOwner bootstrapAndRecover(SnapshotManager snapshotManager, + Path archiveDirectory, long maxSegmentSize, BlockSnapshotMeta baseHead, + ServingIndexFaultHook servingIndexFaultHook, + ReadableStateFaultHook readableStateFaultHook) throws IOException { Objects.requireNonNull(snapshotManager, "snapshotManager"); Path root = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); BlockSnapshotMeta head = Objects.requireNonNull(baseHead, "baseHead"); @@ -185,7 +217,8 @@ public static StateArchiveRuntimeOwner bootstrapAndRecover(SnapshotManager snaps "State Archive bootstrap requires atomic directory publication", failure); } HistorySegmentStore.syncDirectory(parent); - return recover(snapshotManager, root, maxSegmentSize, servingIndexFaultHook); + return recover(snapshotManager, root, maxSegmentSize, servingIndexFaultHook, + readableStateFaultHook); } catch (IOException | RuntimeException failure) { throw failure; } @@ -359,14 +392,19 @@ private synchronized void publishReadableState(ArchiveHistoryWriter writer, throw new ArchivePersistenceException( "Readable-state target differs from committed history head"); } + readableStateFaultHook.afterStage(ReadableStateStage.CANONICAL_REFRESHED); bindAndPublishLatest(writer, catalog, latest, target); + readableStateFaultHook.afterStage(ReadableStateStage.LATEST_PUBLISHED); long previousReadable = snapshotManager.getArchiveReadableEpoch(); + BlockSnapshotMeta previousReadableHead = readableHead; snapshotManager.markArchiveReadableThrough(target.getEpoch()); try { validateReadableState(catalog, target); readableHead = target; + readableStateFaultHook.afterStage(ReadableStateStage.READABLE_PUBLISHED); } catch (IOException | RuntimeException failure) { snapshotManager.markArchiveReadableThrough(previousReadable); + readableHead = previousReadableHead; throw failure; } } diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 12733cefc28..af68977d77e 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -206,6 +206,8 @@ public class Manager { private StateArchiveRuntimeOwner stateArchiveRuntime; private StateArchiveRuntimeOwner.ServingIndexFaultHook stateArchiveServingIndexFaultHook = stage -> { }; + private StateArchiveRuntimeOwner.ReadableStateFaultHook stateArchiveReadableStateFaultHook = + stage -> { }; private static final int NO_BLOCK_WAITING_LOCK = 0; private final int shieldedTransInPendingMaxCounts = Args.getInstance().getShieldedTransInPendingMaxCounts(); @@ -650,13 +652,13 @@ private void initStateArchive() { recovered = StateArchiveRuntimeOwner.bootstrapAndRecover( (SnapshotManager) revokingStore, archiveDirectory, storage.getStateArchiveMaxSegmentSize(), canonicalHead, - stateArchiveServingIndexFaultHook); + stateArchiveServingIndexFaultHook, stateArchiveReadableStateFaultHook); logger.info("State archive fresh baseline published: directory={}, head={}", archiveDirectory, headNumber); } else { recovered = StateArchiveRuntimeOwner.recover((SnapshotManager) revokingStore, archiveDirectory, storage.getStateArchiveMaxSegmentSize(), - stateArchiveServingIndexFaultHook); + stateArchiveServingIndexFaultHook, stateArchiveReadableStateFaultHook); } BlockSnapshotMeta archiveHead = recovered.getRecoveredHead(); if (archiveHead != null diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index a2711ec9f2f..cec89c72cab 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -38,6 +38,8 @@ import org.junit.rules.TemporaryFolder; import org.mockito.ArgumentCaptor; import org.tron.common.parameter.CommonParameter; +import org.tron.common.storage.leveldb.LevelDbDataSourceImpl; +import org.tron.common.storage.rocksdb.RocksDbDataSourceImpl; import org.tron.common.utils.Sha256Hash; import org.tron.core.ChainBaseManager; import org.tron.core.capsule.BlockCapsule; @@ -49,10 +51,13 @@ import org.tron.core.db2.ISession; import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; +import org.tron.core.db2.archive.StateArchiveRuntimeOwner.ReadableStateStage; import org.tron.core.db2.archive.StateArchiveRuntimeOwner.ServingIndexStage; import org.tron.core.db2.archive.StateArchiveRuntimeOwner.State; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.Flusher; +import org.tron.core.db2.common.LevelDB; +import org.tron.core.db2.common.RocksDB; import org.tron.core.db2.common.WrappedByteArray; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; @@ -485,6 +490,238 @@ public void managerProjectsP66HistoryFromNativeSupplementalAccountAssetAndRestar } } + @Test + public void postRefreshFailuresReopenNativeSupplementalP66FixedPoint() throws Exception { + for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { + for (ReadableStateStage failureStage : ReadableStateStage.values()) { + Path output = temporaryFolder.newFolder("post-refresh-p66-" + + engine.toLowerCase() + "-" + failureStage.name().toLowerCase()).toPath(); + withArchiveConfig(output, engine, true, + () -> runPostRefreshP66Failure(output, engine, failureStage)); + } + } + } + + private void runPostRefreshP66Failure(Path output, String engine, + ReadableStateStage failureStage) throws Exception { + Path archive = output.resolve("state-archive"); + BlockSnapshotMeta base = new BlockSnapshotMeta(6, 6, hash(6), hash(5), 6_000L); + SnapshotFixture fixture = snapshotFixtureWithoutAccountAsset(Collections.emptyMap()); + AtomicLong targetOptimization = new AtomicLong(); + AtomicReference armed = new AtomicReference<>(); + TestAccountAssetStore accountAssetStore = new TestAccountAssetStore(); + Manager manager = manager(fixture.snapshots, base, accountAssetStore, targetOptimization); + setField(manager, "stateArchiveReadableStateFaultHook", + (StateArchiveRuntimeOwner.ReadableStateFaultHook) stage -> { + if (stage == armed.get() && armed.compareAndSet(stage, null)) { + throw new IOException("injected readable-state failure at " + stage); + } + }); + byte[] address = archiveAddress(12); + String tokenId = "1000012"; + byte[] directKey = new P66AccountAssetCodec().assetPhysicalKey(address, tokenId); + fixture.databases.get("properties").put( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), longValue(0)); + fixture.databases.get("account").put(address, + assetAccount(address, false, tokenId, 20)); + invoke(manager, "initStateArchive"); + + targetOptimization.set(1); + BlockSnapshotMeta activation = new BlockSnapshotMeta(7, 7, hash(7), hash(6), 7_000L); + try (ISession block = fixture.snapshots.buildSession()) { + fixture.databases.get("properties").put( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), longValue(1)); + fixture.databases.get("account").put(address, + assetAccount(address, false, tokenId, 30)); + block.commit(activation); + } + setField(fixture.snapshots, "flushCount", 1); + fixture.snapshots.flushPending(); + assertEquals(activation, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + + BlockSnapshotMeta target = new BlockSnapshotMeta(8, 8, hash(8), hash(7), 8_000L); + try (ISession block = fixture.snapshots.buildSession()) { + fixture.databases.get("account").put(address, + assetAccount(address, true, tokenId, 40)); + block.commit(target); + } + setField(fixture.snapshots, "flushCount", 1); + armed.set(failureStage); + assertThrows(org.tron.core.exception.TronError.class, fixture.snapshots::flushPending); + + assertEquals(target, manager.getArchiveHistoryWriter().committedHeadMeta()); + assertEquals(activation.getEpoch(), fixture.snapshots.getArchiveReadableEpoch()); + assertArrayEquals(longValue(40), accountAssetStore.get(directKey)); + assertTrue(fixture.databases.values().stream() + .allMatch(database -> database.getHead() instanceof SnapshotRoot)); + assertThrows(ArchivePersistenceException.class, + () -> manager.getArchiveAccountAssetBalance(7, address, tokenId)); + assertThrows(ArchivePersistenceException.class, + () -> manager.getArchiveAccountAssetBalance(8, address, tokenId)); + Map historyAuthority = historyAuthoritySnapshot(archive); + + @SuppressWarnings("unchecked") + ArgumentCaptor> checkpoints = ArgumentCaptor.forClass(Map.class); + verify(fixture.checkpoint, times(2)).updateByBatch(checkpoints.capture()); + Map recoveredCheckpoint = checkpoints.getAllValues().get(1); + invoke(manager, "closeStateArchive"); + fixture.snapshots.shutdown(); + accountAssetStore.getDbSource().closeDB(); + assertHistoryAuthorityEquals(historyAuthority, archive); + + SnapshotFixture restarted = snapshotFixtureWithoutAccountAsset(recoveredCheckpoint); + TestAccountAssetStore reopenedAccountAssetStore = new TestAccountAssetStore(); + Manager restartedManager = manager(restarted.snapshots, target, reopenedAccountAssetStore, + targetOptimization); + invokeCheckpointRecovery(restarted.snapshots, restarted.checkpoint); + invoke(restartedManager, "initStateArchive"); + assertEquals(0, restartedManager.getStateArchiveRuntime().getStartupRecoveryActionCount()); + assertEquals(target, + restartedManager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertAccountAsset(restartedManager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertAccountAsset(restartedManager, 7, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 30); + assertAccountAsset(restartedManager, 8, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 40); + assertArrayEquals(longValue(40), reopenedAccountAssetStore.get(directKey)); + assertEquals(1, countGenerationDirectories(archive)); + assertHistoryAuthorityEquals(historyAuthority, archive); + + ArchiveRuntimeQueryGate.Lease active = + restartedManager.getStateArchiveRuntime().pinHistoricalState(8); + assertThrows(IllegalStateException.class, + () -> invoke(restartedManager, "closeStateArchive")); + active.close(); + invoke(restartedManager, "closeStateArchive"); + restarted.snapshots.shutdown(); + reopenedAccountAssetStore.getDbSource().closeDB(); + + SnapshotFixture secondRestart = snapshotFixtureWithoutAccountAsset(recoveredCheckpoint); + TestAccountAssetStore secondAccountAssetStore = new TestAccountAssetStore(); + Manager secondManager = manager(secondRestart.snapshots, target, secondAccountAssetStore, + targetOptimization); + invokeCheckpointRecovery(secondRestart.snapshots, secondRestart.checkpoint); + invoke(secondManager, "initStateArchive"); + assertEquals(0, secondManager.getStateArchiveRuntime().getStartupRecoveryActionCount()); + assertEquals(target, secondManager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertAccountAsset(secondManager, 8, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 40); + assertHistoryAuthorityEquals(historyAuthority, archive); + assertEquals("LEVELDB".equals(engine), secondAccountAssetStore.getDbSource() + instanceof org.tron.common.storage.leveldb.LevelDbDataSourceImpl); + assertEquals("ROCKSDB".equals(engine), secondAccountAssetStore.getDbSource() + instanceof org.tron.common.storage.rocksdb.RocksDbDataSourceImpl); + invoke(secondManager, "closeStateArchive"); + secondRestart.snapshots.shutdown(); + secondAccountAssetStore.getDbSource().closeDB(); + } + + @Test + public void allNativeExact27StoresReopenWithStableIdentityAndHistory() throws Exception { + for (String engine : Arrays.asList("LEVELDB", "ROCKSDB")) { + Path output = temporaryFolder.newFolder("all-native-exact27-" + + engine.toLowerCase()).toPath(); + withArchiveConfig(output, engine, true, + () -> runAllNativeExact27Scenario(output, engine)); + } + } + + private void runAllNativeExact27Scenario(Path output, String engine) throws Exception { + Path archive = output.resolve("state-archive"); + BlockSnapshotMeta base = new BlockSnapshotMeta(6, 6, hash(6), hash(5), 6_000L); + SnapshotFixture fixture = nativeSnapshotFixture(output, engine, Collections.emptyMap()); + AtomicLong targetOptimization = new AtomicLong(); + TestAccountAssetStore accountAssetStore = new TestAccountAssetStore(); + Manager manager = manager(fixture.snapshots, base, accountAssetStore, targetOptimization); + byte[] address = archiveAddress(13); + String tokenId = "1000013"; + byte[] directKey = new P66AccountAssetCodec().assetPhysicalKey(address, tokenId); + seedNativeExact27(fixture.databases, 6); + fixture.databases.get("properties").put( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), longValue(0)); + fixture.databases.get("account").put(address, + assetAccount(address, false, tokenId, 20)); + Map sourceIdentities = sourceIdentities(fixture, accountAssetStore); + invoke(manager, "initStateArchive"); + + BlockSnapshotMeta target = null; + for (int epoch = 7; epoch <= 8; epoch++) { + targetOptimization.set(1); + target = new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), + epoch * 1_000L); + try (ISession block = fixture.snapshots.buildSession()) { + mutateNativeExact27(fixture.databases, epoch); + if (epoch == 7) { + fixture.databases.get("properties").put( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), longValue(1)); + fixture.databases.get("account").put(address, + assetAccount(address, false, tokenId, 30)); + } else { + fixture.databases.get("account").put(address, + assetAccount(address, true, tokenId, 40)); + } + block.commit(target); + } + setField(fixture.snapshots, "flushCount", 1); + fixture.snapshots.flushPending(); + assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + } + + assertNativeExact27History(manager, address, directKey); + assertAccountAsset(manager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertAccountAsset(manager, 7, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 30); + assertAccountAsset(manager, 8, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 40); + @SuppressWarnings("unchecked") + ArgumentCaptor> checkpoints = ArgumentCaptor.forClass(Map.class); + verify(fixture.checkpoint, times(2)).updateByBatch(checkpoints.capture()); + Map recoveredCheckpoint = checkpoints.getAllValues().get(1); + invoke(manager, "closeStateArchive"); + fixture.snapshots.shutdown(); + closeNativeStores(fixture); + accountAssetStore.getDbSource().closeDB(); + + SnapshotFixture restarted = nativeSnapshotFixture(output, engine, recoveredCheckpoint); + TestAccountAssetStore reopenedAccountAssetStore = new TestAccountAssetStore(); + assertEquals(sourceIdentities, sourceIdentities(restarted, reopenedAccountAssetStore)); + Manager restartedManager = manager(restarted.snapshots, target, reopenedAccountAssetStore, + targetOptimization); + invokeCheckpointRecovery(restarted.snapshots, restarted.checkpoint); + invoke(restartedManager, "initStateArchive"); + assertEquals(0, restartedManager.getStateArchiveRuntime().getStartupRecoveryActionCount()); + assertEquals(target, + restartedManager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertNativeExact27History(restartedManager, address, directKey); + assertAccountAsset(restartedManager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertAccountAsset(restartedManager, 7, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 30); + assertAccountAsset(restartedManager, 8, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 40); + invoke(restartedManager, "closeStateArchive"); + restarted.snapshots.shutdown(); + closeNativeStores(restarted); + reopenedAccountAssetStore.getDbSource().closeDB(); + + SnapshotFixture secondRestart = nativeSnapshotFixture(output, engine, recoveredCheckpoint); + TestAccountAssetStore secondAccountAssetStore = new TestAccountAssetStore(); + assertEquals(sourceIdentities, sourceIdentities(secondRestart, secondAccountAssetStore)); + Manager secondManager = manager(secondRestart.snapshots, target, secondAccountAssetStore, + targetOptimization); + invokeCheckpointRecovery(secondRestart.snapshots, secondRestart.checkpoint); + invoke(secondManager, "initStateArchive"); + assertEquals(0, secondManager.getStateArchiveRuntime().getStartupRecoveryActionCount()); + assertEquals(target, secondManager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertNativeExact27History(secondManager, address, directKey); + invoke(secondManager, "closeStateArchive"); + secondRestart.snapshots.shutdown(); + closeNativeStores(secondRestart); + secondAccountAssetStore.getDbSource().closeDB(); + } + private void runSupplementalP66Scenario(Path output, String engine) throws Exception { Path archive = output.resolve("state-archive"); BlockSnapshotMeta head = new BlockSnapshotMeta(6, 6, hash(6), hash(5), 6_000L); @@ -920,7 +1157,57 @@ private static SnapshotFixture snapshotFixture(Map checkpointEnt ignored -> checkpointEntries.entrySet().iterator()); when(checkpoint.getDbSource()).thenReturn(checkpointDb); snapshots.setCheckTmpStore(checkpoint); - return new SnapshotFixture(snapshots, databases, checkpointOnly, checkpoint); + return new SnapshotFixture(snapshots, databases, checkpointOnly, checkpoint, + Collections.emptyMap()); + } + + private static SnapshotFixture nativeSnapshotFixture(Path output, String engine, + Map checkpointEntries) { + if (CommonParameter.getInstance().getStorage() == null) { + CommonParameter.getInstance().storage = new Storage(); + } + SnapshotManager snapshots = new SnapshotManager(""); + Map databases = new LinkedHashMap<>(); + Map nativeStores = new LinkedHashMap<>(); + for (String participant : PARTICIPANTS) { + if (AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(participant)) { + continue; + } + DB nativeStore = openNativeStore(output, engine, participant); + Chainbase database = new Chainbase(new SnapshotRoot(nativeStore)); + snapshots.add(database); + databases.put(participant, database); + nativeStores.put(participant, (SnapshotCapableStore) nativeStore); + } + Map checkpointOnly = new LinkedHashMap<>(); + for (String name : Arrays.asList("block", "block-index")) { + DB nativeStore = openNativeStore(output, engine, name); + Chainbase database = new Chainbase(new SnapshotRoot(nativeStore)); + snapshots.add(database); + checkpointOnly.put(name, database); + nativeStores.put(name, (SnapshotCapableStore) nativeStore); + } + snapshots.enable(); + snapshots.setUnChecked(false); + CheckTmpStore checkpoint = mock(CheckTmpStore.class); + @SuppressWarnings("unchecked") + DbSourceInter checkpointDb = mock(DbSourceInter.class); + when(checkpointDb.iterator()).thenAnswer( + ignored -> checkpointEntries.entrySet().iterator()); + when(checkpoint.getDbSource()).thenReturn(checkpointDb); + snapshots.setCheckTmpStore(checkpoint); + return new SnapshotFixture(snapshots, databases, checkpointOnly, checkpoint, nativeStores); + } + + private static DB openNativeStore(Path output, String engine, String name) { + if ("LEVELDB".equals(engine)) { + return new LevelDB(new LevelDbDataSourceImpl(output.toString(), name)); + } + if ("ROCKSDB".equals(engine)) { + return new RocksDB(new RocksDbDataSourceImpl( + output.resolve("database").toString(), name)); + } + throw new IllegalArgumentException("Unsupported native fixture engine: " + engine); } private static SnapshotFixture snapshotFixtureWithoutAccountAsset( @@ -953,6 +1240,32 @@ private static long countGenerationDirectories(Path archive) throws IOException } } + private static Map historyAuthoritySnapshot(Path archive) throws IOException { + Map snapshot = new LinkedHashMap<>(); + for (String relative : Arrays.asList("MANIFEST", "bootstrap.anchor", + "history.scan-anchor", "state_history.idx", "commits/commit.log")) { + Path file = archive.resolve(relative); + snapshot.put(relative, Files.readAllBytes(file)); + } + try (java.util.stream.Stream segments = Files.list(archive.resolve("history"))) { + Iterator ordered = segments.filter(Files::isRegularFile).sorted().iterator(); + while (ordered.hasNext()) { + Path file = ordered.next(); + snapshot.put("history/" + file.getFileName(), Files.readAllBytes(file)); + } + } + return snapshot; + } + + private static void assertHistoryAuthorityEquals(Map expected, Path archive) + throws IOException { + Map actual = historyAuthoritySnapshot(archive); + assertEquals(expected.keySet(), actual.keySet()); + for (String relative : expected.keySet()) { + assertArrayEquals(relative, expected.get(relative), actual.get(relative)); + } + } + private static void installRecoveredBinding(SnapshotManager snapshots, Path archive, long firstEpoch, long lastEpoch) throws Exception { setField(snapshots, "recoveredArchiveWalBinding", @@ -1137,6 +1450,106 @@ private static void seedExact27State(Map databases, int lates } } + private static void seedNativeExact27(Map databases, int epoch) { + int storeIndex = 0; + for (String dbName : PARTICIPANTS) { + if (!AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(dbName) + && !"account".equals(dbName)) { + databases.get(dbName).put(nativeExactKey(dbName, storeIndex), + nativeExactValue(epoch, storeIndex)); + } + storeIndex++; + } + } + + private static void mutateNativeExact27(Map databases, int epoch) { + int storeIndex = 0; + for (String dbName : PARTICIPANTS) { + if (!AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(dbName) + && !"account".equals(dbName)) { + Chainbase database = databases.get(dbName); + if (epoch == 7 || storeIndex % 3 == 0) { + database.put(nativeExactKey(dbName, storeIndex), nativeExactValue(epoch, storeIndex)); + } else if (storeIndex % 3 == 1) { + database.delete(nativeExactKey(dbName, storeIndex)); + } else { + database.put(nativeExactKey(dbName, storeIndex), nativeExactValue(7, storeIndex)); + } + } + storeIndex++; + } + } + + private static void assertNativeExact27History(Manager manager, byte[] address, + byte[] directKey) { + for (int epoch = 6; epoch <= 8; epoch++) { + int storeIndex = 0; + for (String dbName : PARTICIPANTS) { + if (!AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(dbName) + && !"account".equals(dbName)) { + byte[] expected; + if (epoch == 6) { + expected = nativeExactValue(6, storeIndex); + } else if (epoch == 7 || storeIndex % 3 == 0) { + expected = nativeExactValue(epoch, storeIndex); + } else if (storeIndex % 3 == 1) { + expected = null; + } else { + expected = nativeExactValue(7, storeIndex); + } + assertHistoricalValue(manager, epoch, dbName, nativeExactKey(dbName, storeIndex), + expected); + } + storeIndex++; + } + assertTrue(manager.getArchiveStateValue(epoch, "account", address).isPresent()); + assertHistoricalValue(manager, epoch, AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + directKey, epoch == 6 ? null : longValue(epoch == 7 ? 30 : 40)); + } + } + + private static byte[] nativeExactKey(String dbName, int storeIndex) { + if ("market_pair_price_to_order".equals(dbName)) { + return org.tron.core.capsule.utils.MarketUtils.createPairPriceKey( + "100".getBytes(StandardCharsets.UTF_8), + "200".getBytes(StandardCharsets.UTF_8), 1000L + storeIndex, 2000L); + } + return new byte[]{(byte) 0xc1, (byte) storeIndex}; + } + + private static byte[] nativeExactValue(int epoch, int storeIndex) { + return new byte[]{(byte) 0xd1, (byte) epoch, (byte) storeIndex}; + } + + private static Map sourceIdentities(SnapshotFixture fixture, + AccountAssetStore accountAssetStore) { + Map identities = new LinkedHashMap<>(); + for (Map.Entry entry : fixture.nativeStores.entrySet()) { + identities.put(entry.getKey(), entry.getValue().getSourceIdentity()); + } + DbSourceInter source = accountAssetStore.getDbSource(); + if (source instanceof LevelDbDataSourceImpl) { + identities.put(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + ((LevelDbDataSourceImpl) source).getSnapshotSourceIdentity()); + } else if (source instanceof RocksDbDataSourceImpl) { + identities.put(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + ((RocksDbDataSourceImpl) source).getSnapshotSourceIdentity()); + } else { + throw new IllegalArgumentException("AccountAsset Store is not native"); + } + return identities; + } + + private static void closeNativeStores(SnapshotFixture fixture) { + for (SnapshotCapableStore store : fixture.nativeStores.values()) { + if (store instanceof LevelDB) { + ((LevelDB) store).getDb().closeDB(); + } else if (store instanceof RocksDB) { + ((RocksDB) store).getDb().closeDB(); + } + } + } + private static void assertExact27History(Manager manager, int targetEpoch) { int storeIndex = 0; for (String dbName : PARTICIPANTS) { @@ -1211,13 +1624,16 @@ private static final class SnapshotFixture { private final Map databases; private final Map checkpointOnly; private final CheckTmpStore checkpoint; + private final Map nativeStores; private SnapshotFixture(SnapshotManager snapshots, Map databases, - Map checkpointOnly, CheckTmpStore checkpoint) { + Map checkpointOnly, CheckTmpStore checkpoint, + Map nativeStores) { this.snapshots = snapshots; this.databases = databases; this.checkpointOnly = checkpointOnly; this.checkpoint = checkpoint; + this.nativeStores = nativeStores; } } From 4f4ff4ae2858c609f47b5470ff12a581e18f9156 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 25 Aug 2026 15:19:28 +0800 Subject: [PATCH 063/161] feat(chainbase): add bounded archive range index --- .../leveldb/LevelDbDataSourceImpl.java | 38 ++++- .../rocksdb/RocksDbDataSourceImpl.java | 34 ++++ .../core/db2/archive/ArchiveReadSnapshot.java | 5 +- .../db2/archive/HistoricalRangeOverlay.java | 4 + .../archive/LatestStateGenerationAdapter.java | 52 +++++- .../LatestStateGenerationCoordinator.java | 6 +- .../PersistentServingKeyIndexGeneration.java | 159 ++++++++++++++++-- .../archive/ServingKeyIndexGeneration.java | 2 +- .../db2/archive/StateArchiveRuntimeOwner.java | 36 +++- .../org/tron/core/db2/common/LevelDB.java | 7 + .../org/tron/core/db2/common/RocksDB.java | 7 + .../main/java/org/tron/core/db/Manager.java | 18 ++ .../ArchiveAuthorityHandleSourcesTest.java | 2 +- .../db2/archive/ArchiveReadSnapshotTest.java | 2 +- ...oricalAccountAssetBalanceResolverTest.java | 2 +- .../HistoricalAccountBalanceReaderTest.java | 2 +- .../LatestStateGenerationAdapterTest.java | 2 +- ...rsistentServingKeyIndexGenerationTest.java | 55 +++++- ...eArchiveManagerStartupIntegrationTest.java | 135 +++++++++++++++ 19 files changed, 533 insertions(+), 35 deletions(-) diff --git a/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java b/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java index 1cc6522c0c5..35e7fbced64 100644 --- a/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java +++ b/chainbase/src/main/java/org/tron/common/storage/leveldb/LevelDbDataSourceImpl.java @@ -17,9 +17,11 @@ import static org.fusesource.leveldbjni.JniDBFactory.factory; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Sets; -import com.google.common.primitives.Bytes; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.primitives.Bytes; +import com.google.common.primitives.UnsignedBytes; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -550,6 +552,36 @@ public synchronized byte[] get(byte[] key) { return pinnedDatabase.get(key, readOptions); } + /** Returns at most {@code maxEntries} rows from this pinned lexical range. */ + public synchronized List> range(byte[] lowerInclusive, + byte[] upperExclusive, int maxEntries) { + if (closed) { + throw new IllegalStateException("LevelDB snapshot lease is closed"); + } + if (lowerInclusive == null || maxEntries <= 0) { + throw new IllegalArgumentException("Invalid LevelDB snapshot range"); + } + List> result = new ArrayList<>(); + try (DBIterator iterator = pinnedDatabase.iterator(readOptions)) { + iterator.seek(lowerInclusive); + while (iterator.hasNext() && result.size() < maxEntries) { + Map.Entry entry = iterator.next(); + if (upperExclusive != null + && UnsignedBytes.lexicographicalComparator().compare(entry.getKey(), + upperExclusive) >= 0) { + break; + } + result.add(Maps.immutableEntry( + Arrays.copyOf(entry.getKey(), entry.getKey().length), + Arrays.copyOf(entry.getValue(), entry.getValue().length))); + } + } catch (IOException failure) { + throw new RuntimeException("Failed to read pinned LevelDB range " + dataBaseName, + failure); + } + return Collections.unmodifiableList(result); + } + public String getSourceIdentity() { return sourceIdentity; } diff --git a/chainbase/src/main/java/org/tron/common/storage/rocksdb/RocksDbDataSourceImpl.java b/chainbase/src/main/java/org/tron/common/storage/rocksdb/RocksDbDataSourceImpl.java index 99e8b35c3ba..18cf6da50b0 100644 --- a/chainbase/src/main/java/org/tron/common/storage/rocksdb/RocksDbDataSourceImpl.java +++ b/chainbase/src/main/java/org/tron/common/storage/rocksdb/RocksDbDataSourceImpl.java @@ -1,8 +1,10 @@ package org.tron.common.storage.rocksdb; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.primitives.Bytes; +import com.google.common.primitives.UnsignedBytes; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -527,6 +529,38 @@ public synchronized byte[] get(byte[] key) { } } + /** Returns at most {@code maxEntries} rows from this pinned lexical range. */ + public synchronized List> range(byte[] lowerInclusive, + byte[] upperExclusive, int maxEntries) { + if (closed) { + throw new IllegalStateException("RocksDB snapshot lease is closed"); + } + if (lowerInclusive == null || maxEntries <= 0) { + throw new IllegalArgumentException("Invalid RocksDB snapshot range"); + } + List> result = new ArrayList<>(); + try (RocksIterator iterator = pinnedDatabase.newIterator(readOptions)) { + iterator.seek(lowerInclusive); + while (iterator.isValid() && result.size() < maxEntries) { + byte[] key = iterator.key(); + if (upperExclusive != null + && UnsignedBytes.lexicographicalComparator().compare(key, upperExclusive) >= 0) { + break; + } + byte[] value = iterator.value(); + result.add(Maps.immutableEntry( + Arrays.copyOf(key, key.length), + Arrays.copyOf(value, value.length))); + iterator.next(); + } + iterator.status(); + } catch (RocksDBException failure) { + throw new RuntimeException("Failed to read pinned RocksDB range " + dataBaseName, + failure); + } + return Collections.unmodifiableList(result); + } + public String getSourceIdentity() { return sourceIdentity; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java index ce3537cc45b..40d825e253c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java @@ -109,7 +109,7 @@ public synchronized List range(String dbName, KeyRange range, Limits limi throws IOException { ensureOpen(); List pinnedLatest = latest.range(dbName, range.getLowerInclusive(), - range.getUpperExclusive()); + range.getUpperExclusive(), limits.getMaxCandidateKeys()); if (pinnedLatest == null) { throw new IllegalStateException("Pinned latest range returned null"); } @@ -248,7 +248,8 @@ default byte[] getSourceIdentityDigest() { OldValue get(String dbName, byte[] physicalRawKey) throws IOException; - List range(String dbName, byte[] lowerInclusive, byte[] upperExclusive) + List range(String dbName, byte[] lowerInclusive, byte[] upperExclusive, + int maxEntries) throws IOException; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java index ea7bc8a8ef3..3b152023bf5 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalRangeOverlay.java @@ -107,6 +107,10 @@ public Limits(int maxChangedKeys, int maxCandidateKeys, int maxResults) { this.maxCandidateKeys = maxCandidateKeys; this.maxResults = maxResults; } + + int getMaxCandidateKeys() { + return maxCandidateKeys; + } } public static final class KeyRange { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java index 2aca0962138..8345cb4fd30 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java @@ -82,7 +82,7 @@ public static SnapshotCapableStore fromDataSource(String dbName, return capable(dbName, level.getSnapshotSourceIdentity(), (blockNumber, blockHash) -> { LevelDbDataSourceImpl.PinnedSnapshot pinned = level.pinSnapshot(); return snapshot(dbName, pinned.getSourceIdentity(), blockNumber, blockHash, - pinned::get, pinned::close); + pinned::get, pinned::range, pinned::close); }); } if (source instanceof RocksDbDataSourceImpl) { @@ -90,7 +90,7 @@ public static SnapshotCapableStore fromDataSource(String dbName, return capable(dbName, rocks.getSnapshotSourceIdentity(), (blockNumber, blockHash) -> { RocksDbDataSourceImpl.PinnedSnapshot pinned = rocks.pinSnapshot(); return snapshot(dbName, pinned.getSourceIdentity(), blockNumber, blockHash, - pinned::get, pinned::close); + pinned::get, pinned::range, pinned::close); }); } throw new ArchivePersistenceException( @@ -118,7 +118,7 @@ public StoreSnapshot pin(long blockNumber, byte[] blockHash) throws IOException } private static StoreSnapshot snapshot(String dbName, String sourceIdentity, long blockNumber, - byte[] blockHash, PointReader reader, CloseableAction close) { + byte[] blockHash, PointReader reader, RangeReader rangeReader, CloseableAction close) { byte[] expectedHash = Arrays.copyOf(blockHash, blockHash.length); return new StoreSnapshot() { @Override @@ -146,6 +146,12 @@ public byte[] get(byte[] physicalRawKey) { return reader.get(physicalRawKey); } + @Override + public List> range(byte[] lowerInclusive, + byte[] upperExclusive, int maxEntries) { + return rangeReader.range(lowerInclusive, upperExclusive, maxEntries); + } + @Override public void close() throws IOException { close.close(); @@ -274,6 +280,11 @@ public interface StoreSnapshot extends Closeable { byte[] getBlockHash(); byte[] get(byte[] physicalRawKey) throws IOException; + + default List> range(byte[] lowerInclusive, byte[] upperExclusive, + int maxEntries) throws IOException { + throw new UnsupportedOperationException("Pinned Store range is unsupported"); + } } @FunctionalInterface @@ -286,6 +297,12 @@ private interface PointReader { byte[] get(byte[] key); } + @FunctionalInterface + private interface RangeReader { + List> range(byte[] lowerInclusive, byte[] upperExclusive, + int maxEntries); + } + @FunctionalInterface private interface CloseableAction { void close() throws IOException; @@ -336,10 +353,31 @@ public synchronized OldValue get(String dbName, byte[] physicalRawKey) throws IO } @Override - public List range(String dbName, byte[] lowerInclusive, - byte[] upperExclusive) { - throw new UnsupportedOperationException( - "Latest-generation range is outside the Phase 1 point-query scope"); + public synchronized List range(String dbName, + byte[] lowerInclusive, byte[] upperExclusive, int maxEntries) throws IOException { + ensureOpen(); + if (!"account-asset".equals(dbName) || maxEntries <= 0 + || maxEntries == Integer.MAX_VALUE) { + throw new UnsupportedOperationException( + "Latest-generation range is limited to bounded account-asset queries"); + } + StoreSnapshot snapshot = snapshots.get(dbName); + if (snapshot == null) { + throw new ArchivePersistenceException( + "Database is outside pinned latest generation: " + dbName); + } + int scanLimit = maxEntries == Integer.MAX_VALUE ? Integer.MAX_VALUE : maxEntries + 1; + List> raw = snapshot.range( + lowerInclusive, upperExclusive, scanLimit); + if (raw.size() > maxEntries) { + throw new ArchiveQueryLimitExceededException( + "latest AccountAsset candidate-key budget exceeded"); + } + List result = new ArrayList<>(raw.size()); + for (Map.Entry entry : raw) { + result.add(new HistoricalRangeOverlay.Entry(entry.getKey(), entry.getValue())); + } + return Collections.unmodifiableList(result); } @Override diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java index c1ac022be45..4a751c0846b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java @@ -350,9 +350,9 @@ public synchronized OldValue get(String dbName, byte[] physicalRawKey) throws IO @Override public List range(String dbName, byte[] lowerInclusive, - byte[] upperExclusive) { - throw new UnsupportedOperationException( - "Latest-generation range is outside the Phase 1 point-query scope"); + byte[] upperExclusive, int maxEntries) throws IOException { + ensureOpen(); + return generation.root.range(dbName, lowerInclusive, upperExclusive, maxEntries); } @Override diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java index 9b8a90ae084..63bec99c64a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java @@ -33,9 +33,10 @@ public final class PersistentServingKeyIndexGeneration implements ServingKeyIndex { private static final int MAGIC = 0x534b4947; // SKIG - private static final short VERSION = 3; + private static final short VERSION = 4; private static final int MAX_MANIFEST_SIZE = 1024 * 1024; private static final byte DATA_PREFIX = 1; + private static final byte RANGE_DATA_PREFIX = 2; private static final byte[] PRESENT = new byte[]{1}; private static final String MANIFEST = "generation.meta"; private static final String MANIFEST_TEMP = "generation.meta.tmp"; @@ -114,6 +115,7 @@ public static PersistentServingKeyIndexGeneration build(Path directory, String g for (HistoryIndexRecord.KeyGroup group : record.getGroups()) { for (byte[] key : group.getKeys()) { batch.put(dataKey(group.getDbName(), key, meta.getEpoch()), PRESENT); + batch.put(rangeDataKey(group.getDbName(), key, meta.getEpoch()), PRESENT); keyChanges++; } } @@ -134,7 +136,8 @@ public static PersistentServingKeyIndexGeneration build(Path directory, String g buildOptions.close(); } - Descriptor descriptor = new Descriptor(scopeIdentity, generationId, baseEpoch, previousEpoch, + Descriptor descriptor = new Descriptor(VERSION, scopeIdentity, generationId, baseEpoch, + previousEpoch, previousHash, sourceDigest.digest(), latestSourceIdentityDigest, participants, keyChanges); persistDescriptor(directory, descriptor); HistorySegmentStore.syncDirectory(directory); @@ -178,11 +181,65 @@ public synchronized OptionalLong firstChangeAfter(String dbName, byte[] rawKey, } @Override - public List changesInRange(String dbName, + public synchronized List changesInRange(String dbName, byte[] lowerInclusive, byte[] upperExclusive, long targetBlock, long upperBound, - int maxChangedKeys) { - throw new UnsupportedOperationException( - "Persistent generic range serving is outside the Phase 1 point-query scope"); + int maxChangedKeys) throws IOException { + ensureOpen(); + Objects.requireNonNull(dbName, "dbName"); + Objects.requireNonNull(lowerInclusive, "lowerInclusive"); + if (maxChangedKeys <= 0) { + throw new IllegalArgumentException("maxChangedKeys must be positive"); + } + if (upperExclusive != null + && BlockReverseDiff.compareUnsigned(lowerInclusive, upperExclusive) > 0) { + throw new IllegalArgumentException("lowerInclusive must not exceed upperExclusive"); + } + if (!supportsRangeQueries()) { + throw new ArchivePersistenceException( + "Serving index generation must be upgraded before range queries"); + } + validateCoverage(dbName, targetBlock, upperBound); + if (targetBlock == Long.MAX_VALUE) { + return Collections.emptyList(); + } + + byte[] databasePrefix = rangeDatabasePrefix(dbName); + List result = new ArrayList<>(); + try (RocksIterator iterator = database.newIterator()) { + iterator.seek(concat(databasePrefix, encodeRangeRawKey(lowerInclusive))); + while (iterator.isValid()) { + RangeDataKey found = decodeRangeDataKey(iterator.key(), databasePrefix); + if (found == null) { + break; + } + if (upperExclusive != null + && BlockReverseDiff.compareUnsigned(found.rawKey, upperExclusive) >= 0) { + break; + } + if (found.epoch <= targetBlock) { + iterator.seek(rangeDataKey(dbName, found.rawKey, targetBlock + 1)); + if (!iterator.isValid()) { + break; + } + RangeDataKey candidate = decodeRangeDataKey(iterator.key(), databasePrefix); + if (candidate == null || !Arrays.equals(candidate.rawKey, found.rawKey)) { + continue; + } + found = candidate; + } + if (found.epoch <= upperBound) { + if (result.size() == maxChangedKeys) { + throw new ArchiveQueryLimitExceededException("changed-key budget exceeded"); + } + result.add(new ServingKeyIndexGeneration.ChangedKey(found.rawKey, found.epoch)); + } + iterator.seek(rangeAfterRawKey(databasePrefix, found.rawKey)); + } + iterator.status(); + } catch (RocksDBException failure) { + throw new IOException("Failed to scan serving index generation", failure); + } + return Collections.unmodifiableList(result); } @Override @@ -236,6 +293,10 @@ public boolean isLatestSourceIdentityBound() { return false; } + public boolean supportsRangeQueries() { + return descriptor.formatVersion >= VERSION; + } + Path getDirectory() { return directory; } @@ -281,6 +342,81 @@ private static byte[] dataPrefix(String dbName, byte[] rawKey) { .array(); } + private static byte[] rangeDataKey(String dbName, byte[] rawKey, long epoch) { + if (epoch < 0) { + throw new IllegalArgumentException("Serving index epoch must not be negative"); + } + byte[] databasePrefix = rangeDatabasePrefix(dbName); + byte[] encodedKey = encodeRangeRawKey(rawKey); + return ByteBuffer.allocate(databasePrefix.length + encodedKey.length + 2 + Long.BYTES) + .put(databasePrefix).put(encodedKey).put((byte) 0).put((byte) 0).putLong(epoch).array(); + } + + private static byte[] rangeDatabasePrefix(String dbName) { + byte[] name = dbName.getBytes(StandardCharsets.UTF_8); + return ByteBuffer.allocate(1 + Integer.BYTES + name.length) + .put(RANGE_DATA_PREFIX).putInt(name.length).put(name).array(); + } + + private static byte[] encodeRangeRawKey(byte[] rawKey) { + ByteArrayOutputStream encoded = new ByteArrayOutputStream(rawKey.length); + for (byte value : rawKey) { + encoded.write(value); + if (value == 0) { + encoded.write(0xff); + } + } + return encoded.toByteArray(); + } + + private static byte[] rangeAfterRawKey(byte[] databasePrefix, byte[] rawKey) { + byte[] encoded = encodeRangeRawKey(rawKey); + return ByteBuffer.allocate(databasePrefix.length + encoded.length + 2) + .put(databasePrefix).put(encoded).put((byte) 0).put((byte) 1).array(); + } + + private static RangeDataKey decodeRangeDataKey(byte[] key, byte[] databasePrefix) { + if (!startsWith(key, databasePrefix) || key.length < databasePrefix.length + 2 + Long.BYTES) { + return null; + } + ByteArrayOutputStream rawKey = new ByteArrayOutputStream(); + int cursor = databasePrefix.length; + int epochOffset = key.length - Long.BYTES; + while (cursor < epochOffset) { + byte value = key[cursor++]; + if (value != 0) { + rawKey.write(value); + } else if (cursor < epochOffset && key[cursor] == (byte) 0xff) { + rawKey.write(0); + cursor++; + } else if (cursor < epochOffset && key[cursor] == 0) { + cursor++; + if (cursor != epochOffset) { + return null; + } + return new RangeDataKey(rawKey.toByteArray(), + ByteBuffer.wrap(key, epochOffset, Long.BYTES).getLong()); + } else { + return null; + } + } + return null; + } + + private static byte[] concat(byte[] left, byte[] right) { + return ByteBuffer.allocate(left.length + right.length).put(left).put(right).array(); + } + + private static final class RangeDataKey { + private final byte[] rawKey; + private final long epoch; + + private RangeDataKey(byte[] rawKey, long epoch) { + this.rawKey = rawKey; + this.epoch = epoch; + } + } + private static boolean startsWith(byte[] value, byte[] prefix) { if (value.length < prefix.length) { return false; @@ -462,7 +598,7 @@ private static Descriptor decodeDescriptor(byte[] encoded) { throw new IllegalArgumentException("Unsupported serving index manifest"); } short version = input.readShort(); - if (version != VERSION || input.readShort() != 0) { + if ((version != VERSION && version != VERSION - 1) || input.readShort() != 0) { throw new IllegalArgumentException("Unsupported serving index manifest"); } String scopeIdentity = input.readUTF(); @@ -492,14 +628,15 @@ private static Descriptor decodeDescriptor(byte[] encoded) { if (!scopeIdentity.equals(ArchiveParticipantDescriptor.scopeIdentity(sorted))) { throw new IllegalArgumentException("Serving index manifest scope identity mismatch"); } - return new Descriptor(scopeIdentity, generationId, from, through, headHash, sourceDigest, - latestSourceIdentityDigest, sorted, keyChanges); + return new Descriptor(version, scopeIdentity, generationId, from, through, headHash, + sourceDigest, latestSourceIdentityDigest, sorted, keyChanges); } catch (IOException invalid) { throw new IllegalArgumentException("Serving index manifest is truncated", invalid); } } private static final class Descriptor { + private final short formatVersion; private final String scopeIdentity; private final String generationId; private final long indexedFrom; @@ -510,10 +647,12 @@ private static final class Descriptor { private final List participants; private final long keyChanges; - private Descriptor(String scopeIdentity, String generationId, long indexedFrom, + private Descriptor(short formatVersion, String scopeIdentity, String generationId, + long indexedFrom, long indexedThrough, byte[] headHash, byte[] sourceDigest, byte[] latestSourceIdentityDigest, List participants, long keyChanges) { + this.formatVersion = formatVersion; this.scopeIdentity = scopeIdentity; this.generationId = generationId; this.indexedFrom = indexedFrom; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java index cd2bae398e6..724062032cc 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ServingKeyIndexGeneration.java @@ -271,7 +271,7 @@ public static final class ChangedKey { private final byte[] key; private final long firstChangeBlock; - private ChangedKey(byte[] key, long firstChangeBlock) { + ChangedKey(byte[] key, long firstChangeBlock) { this.key = Arrays.copyOf(key, key.length); this.firstChangeBlock = firstChangeBlock; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java index da5276e9aa3..526cbda0dcb 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java @@ -504,7 +504,19 @@ private PersistentServingKeyIndexCatalog openOrCreateServingCatalog( ArchiveHistoryWriter writer) throws IOException { Path root = archiveDirectory.resolve("serving-index"); if (Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { - return PersistentServingKeyIndexCatalog.open(root, this::afterCatalogStage); + PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.open(root, this::afterCatalogStage); + try { + upgradeServingRangeIndex(writer, catalog); + return catalog; + } catch (IOException | RuntimeException failure) { + try { + catalog.close(); + } catch (IOException | RuntimeException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } } String generationId = generationId(writer.committedHeadMeta()); Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); @@ -515,6 +527,28 @@ private PersistentServingKeyIndexCatalog openOrCreateServingCatalog( return PersistentServingKeyIndexCatalog.create(root, shadow, this::afterCatalogStage); } + private void upgradeServingRangeIndex(ArchiveHistoryWriter writer, + PersistentServingKeyIndexCatalog catalog) throws IOException { + String expected = catalog.getCurrentGenerationId(); + byte[] latestSourceIdentityDigest; + try (PersistentServingKeyIndexGeneration current = catalog.pin()) { + if (current.supportsRangeQueries()) { + return; + } + latestSourceIdentityDigest = current.getLatestSourceIdentityDigest(); + } + BlockSnapshotMeta target = writer.committedHeadMeta(); + Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); + try (PersistentServingKeyIndexGeneration candidate = writer.buildServingGeneration(shadow, + generationId(target), latestSourceIdentityDigest)) { + validateServingGeneration(writer, candidate, target); + } + if (!catalog.publish(expected, shadow)) { + throw new ArchivePersistenceException( + "Serving index catalog changed during range-index upgrade"); + } + } + private synchronized void publishServingIndex(ArchiveHistoryWriter writer, PersistentServingKeyIndexCatalog catalog, BlockSnapshotMeta target) throws IOException { BlockSnapshotMeta historyHead = writer.committedHeadMeta(); diff --git a/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java b/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java index f97e73546c1..5fdf3835a7d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java +++ b/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import lombok.Getter; import org.tron.common.parameter.CommonParameter; @@ -92,6 +93,12 @@ public byte[] get(byte[] physicalRawKey) { return pinned.get(physicalRawKey); } + @Override + public List> range(byte[] lowerInclusive, + byte[] upperExclusive, int maxEntries) { + return pinned.range(lowerInclusive, upperExclusive, maxEntries); + } + @Override public void close() throws IOException { pinned.close(); diff --git a/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java b/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java index cd88f34ae65..91118c3fd29 100644 --- a/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java +++ b/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import lombok.Getter; import org.tron.common.parameter.CommonParameter; @@ -93,6 +94,12 @@ public byte[] get(byte[] physicalRawKey) { return pinned.get(physicalRawKey); } + @Override + public List> range(byte[] lowerInclusive, + byte[] upperExclusive, int maxEntries) { + return pinned.range(lowerInclusive, upperExclusive, maxEntries); + } + @Override public void close() throws IOException { pinned.close(); diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index af68977d77e..aeb466522a5 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -122,6 +122,7 @@ import org.tron.core.db2.archive.ArchiveStoreScope; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.archive.HistoricalAccountAssetBalanceResolver; +import org.tron.core.db2.archive.HistoricalAccountAssetPrefixResolver; import org.tron.core.db2.archive.HistoricalAccountBalanceReader; import org.tron.core.db2.archive.OldValue; import org.tron.core.db2.archive.SnapshotOldValueCollector; @@ -743,6 +744,23 @@ public HistoricalAccountAssetBalanceResolver.Result getArchiveAccountAssetBalanc } } + /** Resolves one bounded P66-aware historical TRC10 balance prefix per request generation. */ + public HistoricalAccountAssetPrefixResolver.Result getArchiveAccountAssets( + long blockNumber, byte[] address, HistoricalAccountAssetPrefixResolver.Limits limits) { + StateArchiveRuntimeOwner runtime = stateArchiveRuntime; + if (runtime == null) { + throw new IllegalStateException("Experimental state archive is disabled"); + } + try (org.tron.core.db2.archive.ArchiveRuntimeQueryGate.Lease lease = + runtime.pinHistoricalState(blockNumber)) { + return new HistoricalAccountAssetPrefixResolver().resolve( + lease.getSnapshot(), address, limits); + } catch (java.io.IOException failure) { + throw new org.tron.core.db2.archive.ArchivePersistenceException( + "Failed to resolve request-owned historical AccountAsset prefix", failure); + } + } + /** Reads one physical key from an exact versioned State Store at a historical block. */ public OldValue getArchiveStateValue(long blockNumber, String dbName, byte[] physicalRawKey) { if (!ArchiveStoreScope.isStateDatabase(dbName)) { diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java index 1e424c58fe8..d9d43f12792 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveAuthorityHandleSourcesTest.java @@ -167,7 +167,7 @@ public OldValue get(String dbName, byte[] physicalRawKey) { @Override public List range(String dbName, byte[] lowerInclusive, - byte[] upperExclusive) { + byte[] upperExclusive, int maxEntries) { throw new AssertionError("Admission must not scan latest business data"); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java index 7d202775dfb..11f14051030 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java @@ -624,7 +624,7 @@ public OldValue get(String dbName, byte[] physicalRawKey) { @Override public List range(String dbName, byte[] lowerInclusive, - byte[] upperExclusive) { + byte[] upperExclusive, int maxEntries) { List result = new ArrayList<>(); Map rows = scopedValues == null ? values : scopedValues.getOrDefault(dbName, Collections.emptyMap()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolverTest.java b/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolverTest.java index 1f5d8b14fb2..67171d72dbd 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolverTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolverTest.java @@ -405,7 +405,7 @@ public OldValue get(String dbName, byte[] rawKey) { @Override public List range(String dbName, byte[] lower, - byte[] upper) { + byte[] upper, int maxEntries) { List result = new ArrayList<>(); for (Value value : values) { if (value.dbName.equals(dbName) diff --git a/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountBalanceReaderTest.java b/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountBalanceReaderTest.java index b875990806c..86e6ef7a804 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountBalanceReaderTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/HistoricalAccountBalanceReaderTest.java @@ -89,7 +89,7 @@ public OldValue get(String dbName, byte[] rawKey) { @Override public java.util.List range(String dbName, byte[] lower, - byte[] upper) { + byte[] upper, int maxEntries) { return Collections.emptyList(); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationAdapterTest.java b/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationAdapterTest.java index 642272e876a..d4fca0f2d9d 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationAdapterTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/LatestStateGenerationAdapterTest.java @@ -52,7 +52,7 @@ public void pinsExactGenerationAndSurvivesLiveSourceReplacement() throws Excepti assertArrayEquals(bytes("old"), pinned.get("account", bytes("key")).getValue()); assertArrayEquals(expectedDigest, pinned.getSourceIdentityDigest()); assertThrows(UnsupportedOperationException.class, - () -> pinned.range("account", new byte[0], null)); + () -> pinned.range("account", new byte[0], null, 1)); } assertEquals(1, account.closedSnapshots.get()); assertEquals(1, properties.closedSnapshots.get()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java b/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java index 02a65d1b87f..fdbb83faf39 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java @@ -71,12 +71,61 @@ public void persistsExactKeyChangesAndSourceIdentityAcrossReopen() throws Except try (PersistentServingKeyIndexGeneration reopened = PersistentServingKeyIndexGeneration.open(generationPath)) { + assertTrue(reopened.supportsRangeQueries()); assertEquals(ArchiveParticipantDescriptor.scopeIdentity(PARTICIPANTS), reopened.getScopeIdentity()); assertArrayEquals(hash(77), reopened.getLatestSourceIdentityDigest()); assertEquals(3, change(reopened, "account", bytes("hot"), 2, 3)); - assertThrows(UnsupportedOperationException.class, - () -> reopened.changesInRange("account", new byte[0], null, 0, 3, 10)); + List changed = reopened.changesInRange( + "account", bytes("h"), bytes("z"), 0, 3, 10); + assertEquals(1, changed.size()); + assertArrayEquals(bytes("hot"), changed.get(0).getKey()); + assertEquals(1, changed.get(0).getFirstChangeBlock()); + assertThrows(ArchiveQueryLimitExceededException.class, + () -> reopened.changesInRange("account", new byte[0], null, 0, 3, 1)); + } + + byte[] legacyRangeManifest = Files.readAllBytes(generationPath.resolve("generation.meta")); + ByteBuffer.wrap(legacyRangeManifest).putShort(4, (short) 3); + refreshChecksum(legacyRangeManifest); + Files.write(generationPath.resolve("generation.meta"), legacyRangeManifest); + try (PersistentServingKeyIndexGeneration legacyRange = + PersistentServingKeyIndexGeneration.open(generationPath)) { + assertFalse(legacyRange.supportsRangeQueries()); + assertThrows(ArchivePersistenceException.class, + () -> legacyRange.changesInRange("account", new byte[0], null, 0, 3, 10)); + } + } + } + + @Test + public void rangeIndexPreservesUnsignedBinaryKeyOrderAndPrefixBoundaries() throws Exception { + Path root = temporaryFolder.newFolder("persistent-binary-range").toPath(); + try (Fixture fixture = new Fixture(root.resolve("authoritative"))) { + byte[] zero = new byte[]{0}; + byte[] zeroZero = new byte[]{0, 0}; + byte[] zeroFf = new byte[]{0, (byte) 0xff}; + byte[] one = new byte[]{1}; + fixture.append(1, group("account", zero, zeroZero, zeroFf, one, + new byte[]{(byte) 0xff})); + fixture.sync(); + + try (PersistentServingKeyIndexGeneration generation = + PersistentServingKeyIndexGeneration.build(root.resolve("generation"), "binary", 0, + hash(0), fixture.markers, fixture.index::read, PARTICIPANTS)) { + List changed = generation.changesInRange( + "account", zero, one, 0, 1, 10); + assertEquals(3, changed.size()); + assertArrayEquals(zero, changed.get(0).getKey()); + assertArrayEquals(zeroZero, changed.get(1).getKey()); + assertArrayEquals(zeroFf, changed.get(2).getKey()); + + List strictUpper = generation.changesInRange( + "account", zero, zeroFf, 0, 1, 10); + assertEquals(2, strictUpper.size()); + assertArrayEquals(zero, strictUpper.get(0).getKey()); + assertArrayEquals(zeroZero, strictUpper.get(1).getKey()); + assertTrue(generation.changesInRange("account", zero, one, 1, 1, 10).isEmpty()); } } } @@ -479,7 +528,7 @@ public OldValue get(String dbName, byte[] physicalRawKey) { @Override public List range(String dbName, byte[] lowerInclusive, - byte[] upperExclusive) { + byte[] upperExclusive, int maxEntries) { return Collections.emptyList(); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index cec89c72cab..3199e6bc92f 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -12,6 +12,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.common.hash.Hashing; import com.google.protobuf.ByteString; import java.io.IOException; import java.lang.reflect.Field; @@ -451,6 +452,10 @@ public void managerResolvesP66AccountAssetHistoryAndRejectsInvalidLayoutsAfterRe () -> manager.getArchiveAccountAssetBalance(10, orphanAddress, tokenId)); assertThrows(ArchivePersistenceException.class, () -> manager.getArchiveAccountAssetBalance(10, mixedAddress, tokenId)); + assertThrows(ArchivePersistenceException.class, + () -> manager.getArchiveAccountAssets(10, orphanAddress, prefixLimits())); + assertThrows(ArchivePersistenceException.class, + () -> manager.getArchiveAccountAssets(10, mixedAddress, prefixLimits())); assertAccountAsset(manager, 11, orphanAddress, tokenId, P66AccountAssetCodec.Phase.P66_ON, false, 0); assertAccountAsset(manager, 11, mixedAddress, tokenId, @@ -475,6 +480,10 @@ public void managerResolvesP66AccountAssetHistoryAndRejectsInvalidLayoutsAfterRe () -> restartedManager.getArchiveAccountAssetBalance(10, orphanAddress, tokenId)); assertThrows(ArchivePersistenceException.class, () -> restartedManager.getArchiveAccountAssetBalance(10, mixedAddress, tokenId)); + assertThrows(ArchivePersistenceException.class, + () -> restartedManager.getArchiveAccountAssets(10, orphanAddress, prefixLimits())); + assertThrows(ArchivePersistenceException.class, + () -> restartedManager.getArchiveAccountAssets(10, mixedAddress, prefixLimits())); invoke(restartedManager, "closeStateArchive"); restarted.snapshots.shutdown(); } @@ -739,6 +748,8 @@ private void runSupplementalP66Scenario(Path output, String engine) throws Excep invoke(manager, "initStateArchive"); assertAccountAsset(manager, 6, address, tokenId, P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertAccountAssetPrefix(manager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); assertFalse(accountAssetStore.has(directKey)); BlockSnapshotMeta target = null; @@ -779,6 +790,8 @@ private void runSupplementalP66Scenario(Path output, String engine) throws Excep assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); if (epoch < 9) { assertArrayEquals(longValue(epoch == 7 ? 30 : 40), accountAssetStore.get(directKey)); + assertAccountAssetPrefix(manager, epoch, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, epoch == 7 ? 30 : 40); } else { assertFalse(accountAssetStore.has(directKey)); } @@ -792,6 +805,14 @@ private void runSupplementalP66Scenario(Path output, String engine) throws Excep P66AccountAssetCodec.Phase.P66_ON, true, 40); assertAccountAsset(manager, 9, address, tokenId, P66AccountAssetCodec.Phase.P66_ON, false, 0); + assertAccountAssetPrefix(manager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertAccountAssetPrefix(manager, 7, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 30); + assertAccountAssetPrefix(manager, 8, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 40); + assertAccountAssetPrefix(manager, 9, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, false, 0); @SuppressWarnings("unchecked") ArgumentCaptor> checkpoints = ArgumentCaptor.forClass(Map.class); verify(fixture.checkpoint, times(3)).updateByBatch(checkpoints.capture()); @@ -799,6 +820,30 @@ private void runSupplementalP66Scenario(Path output, String engine) throws Excep invoke(manager, "closeStateArchive"); fixture.snapshots.shutdown(); accountAssetStore.getDbSource().closeDB(); + assertEquals(1, countGenerationDirectories(archive)); + downgradeOnlyServingGenerationToV3(archive); + + SnapshotFixture interrupted = snapshotFixtureWithoutAccountAsset(recoveredCheckpoint); + TestAccountAssetStore interruptedAccountAssetStore = new TestAccountAssetStore(); + Manager interruptedManager = manager(interrupted.snapshots, target, + interruptedAccountAssetStore, targetOptimization); + interrupted.databases.get("properties").put( + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), longValue(1)); + invokeCheckpointRecovery(interrupted.snapshots, interrupted.checkpoint); + AtomicReference upgradeFailure = + new AtomicReference<>(ServingIndexStage.GENERATION_INSTALLED); + setField(interruptedManager, "stateArchiveServingIndexFaultHook", + (StateArchiveRuntimeOwner.ServingIndexFaultHook) stage -> { + if (stage == upgradeFailure.get() && upgradeFailure.compareAndSet(stage, null)) { + throw new IOException("injected v3 range-index upgrade failure"); + } + }); + assertThrows(IllegalStateException.class, + () -> invoke(interruptedManager, "initStateArchive")); + assertNull(interruptedManager.getStateArchiveRuntime()); + interrupted.snapshots.shutdown(); + interruptedAccountAssetStore.getDbSource().closeDB(); + assertEquals(2, countGenerationDirectories(archive)); SnapshotFixture restarted = snapshotFixtureWithoutAccountAsset(recoveredCheckpoint); targetOptimization.set(1); @@ -817,7 +862,16 @@ private void runSupplementalP66Scenario(Path output, String engine) throws Excep P66AccountAssetCodec.Phase.P66_ON, true, 40); assertAccountAsset(restartedManager, 9, address, tokenId, P66AccountAssetCodec.Phase.P66_ON, false, 0); + assertAccountAssetPrefix(restartedManager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertAccountAssetPrefix(restartedManager, 7, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 30); + assertAccountAssetPrefix(restartedManager, 8, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 40); + assertAccountAssetPrefix(restartedManager, 9, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, false, 0); assertFalse(reopenedAccountAssetStore.has(directKey)); + assertServingGenerationSupportsRange(archive); assertEquals("LEVELDB".equals(engine), reopenedAccountAssetStore.getDbSource() instanceof org.tron.common.storage.leveldb.LevelDbDataSourceImpl); assertEquals("ROCKSDB".equals(engine), reopenedAccountAssetStore.getDbSource() @@ -1240,6 +1294,30 @@ private static long countGenerationDirectories(Path archive) throws IOException } } + private static void downgradeOnlyServingGenerationToV3(Path archive) throws IOException { + Path generations = archive.resolve("serving-index").resolve("generations"); + Path generation; + try (java.util.stream.Stream entries = Files.list(generations)) { + generation = entries.filter(Files::isDirectory).findFirst() + .orElseThrow(() -> new IOException("Serving generation is missing")); + } + Path manifest = generation.resolve("generation.meta"); + byte[] encoded = Files.readAllBytes(manifest); + ByteBuffer.wrap(encoded).putShort(4, (short) 3); + int payloadLength = encoded.length - Integer.BYTES; + int checksum = Hashing.crc32c().hashBytes(encoded, 0, payloadLength).asInt(); + ByteBuffer.wrap(encoded, payloadLength, Integer.BYTES).putInt(checksum); + Files.write(manifest, encoded); + } + + private static void assertServingGenerationSupportsRange(Path archive) throws IOException { + try (PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.open(archive.resolve("serving-index")); + PersistentServingKeyIndexGeneration generation = catalog.pin()) { + assertTrue(generation.supportsRangeQueries()); + } + } + private static Map historyAuthoritySnapshot(Path archive) throws IOException { Map snapshot = new LinkedHashMap<>(); for (String relative : Arrays.asList("MANIFEST", "bootstrap.anchor", @@ -1406,6 +1484,35 @@ private static void assertP66History(Manager manager, byte[] address, byte[] abs P66AccountAssetCodec.Phase.P66_ON, true, 40); assertAccountAsset(manager, 9, address, tokenId, P66AccountAssetCodec.Phase.P66_ON, false, 0); + assertAccountAssetPrefix(manager, 6, address, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, true, 20); + assertAccountAssetPrefix(manager, 6, absentAddress, tokenId, + P66AccountAssetCodec.Phase.P66_OFF, false, 0); + assertAccountAssetPrefix(manager, 7, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 30); + assertAccountAssetPrefix(manager, 8, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 40); + assertAccountAssetPrefix(manager, 9, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, false, 0); + } + + private static void assertAccountAssetPrefix(Manager manager, int targetEpoch, byte[] address, + String tokenId, P66AccountAssetCodec.Phase phase, boolean present, long balance) { + HistoricalAccountAssetPrefixResolver.Result result = manager.getArchiveAccountAssets( + targetEpoch, address, prefixLimits()); + assertEquals(targetEpoch, result.getBlockNumber()); + assertArrayEquals(address, result.getAddress()); + assertEquals(phase, result.getPhase()); + assertEquals(present, result.isAccountPresent()); + assertEquals(present ? 1 : 0, result.getBalances().size()); + if (present) { + assertEquals(tokenId, result.getBalances().get(0).getTokenId()); + assertEquals(balance, result.getBalances().get(0).getBalance()); + } + } + + private static HistoricalAccountAssetPrefixResolver.Limits prefixLimits() { + return new HistoricalAccountAssetPrefixResolver.Limits(10, 10, 10, 64, 8, 1_000); } private static void assertAccountAsset(Manager manager, int targetEpoch, byte[] address, @@ -1765,6 +1872,34 @@ public byte[] get(byte[] physicalRawKey) { return value == null ? null : Arrays.copyOf(value, value.length); } + @Override + public List> range(byte[] lowerInclusive, + byte[] upperExclusive, int maxEntries) { + if (lowerInclusive == null || maxEntries <= 0) { + throw new IllegalArgumentException("Invalid memory snapshot range"); + } + List> entries = new ArrayList<>(); + pinned.forEach((key, value) -> entries.add(new AbstractMap.SimpleImmutableEntry<>( + key.getBytes(), Arrays.copyOf(value, value.length)))); + entries.sort((left, right) -> + BlockReverseDiff.compareUnsigned(left.getKey(), right.getKey())); + List> result = new ArrayList<>(); + for (Map.Entry entry : entries) { + if (BlockReverseDiff.compareUnsigned(entry.getKey(), lowerInclusive) < 0) { + continue; + } + if (upperExclusive != null + && BlockReverseDiff.compareUnsigned(entry.getKey(), upperExclusive) >= 0) { + break; + } + if (result.size() == maxEntries) { + break; + } + result.add(entry); + } + return Collections.unmodifiableList(result); + } + @Override public void close() { pinned.clear(); From 3f8af03ab913c55a63eb2e32d9ce2268d01c7665 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 25 Aug 2026 15:19:43 +0800 Subject: [PATCH 064/161] feat(framework): add archive startup diagnostics --- .../org/tron/core/exception/TronError.java | 1 + .../tron/program/ArchiveStateDiagnostic.java | 345 ++++++++++++++++++ .../main/java/org/tron/program/FullNode.java | 10 + .../program/ArchiveStateDiagnosticTest.java | 194 ++++++++++ 4 files changed, 550 insertions(+) create mode 100644 framework/src/main/java/org/tron/program/ArchiveStateDiagnostic.java create mode 100644 framework/src/test/java/org/tron/program/ArchiveStateDiagnosticTest.java diff --git a/common/src/main/java/org/tron/core/exception/TronError.java b/common/src/main/java/org/tron/core/exception/TronError.java index 4ee7cdae916..82da5286707 100644 --- a/common/src/main/java/org/tron/core/exception/TronError.java +++ b/common/src/main/java/org/tron/core/exception/TronError.java @@ -50,6 +50,7 @@ public enum ErrCode { SOLID_NODE_INIT(0), PARAMETER_INIT(1), ACTUATOR_REGISTER(1), + STATE_ARCHIVE_INIT(1), JDK_VERSION(1); private final int code; diff --git a/framework/src/main/java/org/tron/program/ArchiveStateDiagnostic.java b/framework/src/main/java/org/tron/program/ArchiveStateDiagnostic.java new file mode 100644 index 00000000000..2cfba7066bf --- /dev/null +++ b/framework/src/main/java/org/tron/program/ArchiveStateDiagnostic.java @@ -0,0 +1,345 @@ +package org.tron.program; + +import com.google.common.hash.Hashing; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeSet; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.util.encoders.Hex; +import org.tron.common.application.TronApplicationContext; +import org.tron.core.db.Manager; +import org.tron.core.db.RevokingDatabase; +import org.tron.core.db.common.DbSourceInter; +import org.tron.core.db2.archive.ArchivePersistenceException; +import org.tron.core.db2.archive.ArchiveStoreScope; +import org.tron.core.db2.archive.HistoricalAccountAssetBalanceResolver; +import org.tron.core.db2.archive.HistoricalAccountAssetPrefixResolver; +import org.tron.core.db2.archive.HistoricalAccountAssetPrefixResolver.Balance; +import org.tron.core.db2.archive.OldValue; +import org.tron.core.db2.archive.P66AccountAssetCodec; +import org.tron.core.db2.archive.P66AccountAssetCodec.DecodedAssetRow; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.store.AccountAssetStore; + +/** Opt-in startup diagnostic for request-owned State Archive point reads. */ +@Slf4j(topic = "DB") +public final class ArchiveStateDiagnostic { + + public static final String ENABLE_PROPERTY = "tron.stateArchive.startupDiagnostic"; + private static final String ACCOUNT_ASSET_DATABASE = "account-asset"; + private static final int ABSENT_KEY_ATTEMPTS = 16; + + private ArchiveStateDiagnostic() { + } + + /** Runs after Spring recovery and before FullNode services start. */ + public static void runIfEnabled(TronApplicationContext context) { + if (!Boolean.parseBoolean(System.getProperty(ENABLE_PROPERTY, "false"))) { + return; + } + RevokingDatabase revokingDatabase = context.getBean(RevokingDatabase.class); + if (!(revokingDatabase instanceof SnapshotManager)) { + throw new IllegalStateException("State Archive diagnostic requires SnapshotManager"); + } + Report report = run(context.getBean(Manager.class), (SnapshotManager) revokingDatabase); + logger.info("State archive startup diagnostic complete: block={}, stores={}, present={}, " + + "absent={}, p66Phase={}, p66Balance={}, p66PrefixEntries={}", + report.getBlockNumber(), report.getStoreCount(), report.getPresentCount(), + report.getAbsentCount(), report.getP66Phase(), report.getP66Balance(), + report.getP66PrefixCount()); + } + + static Report run(Manager manager, SnapshotManager snapshotManager) { + Objects.requireNonNull(manager, "manager"); + Objects.requireNonNull(snapshotManager, "snapshotManager"); + long blockNumber = manager.getDynamicPropertiesStore().getLatestBlockHeaderNumber(); + Map stores = collectStores(manager, snapshotManager); + List expectedStores = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(expectedStores); + if (!stores.keySet().equals(new TreeSet<>(expectedStores))) { + throw new ArchivePersistenceException( + "State Archive diagnostic Store scope is not exact-27: " + stores.keySet()); + } + + int presentCount = 0; + int absentCount = 0; + for (String dbName : expectedStores) { + Sample sample = stores.get(dbName).sample(dbName); + OldValue historical = manager.getArchiveStateValue(blockNumber, dbName, sample.key); + OldValue current = OldValue.fromNullable(sample.value); + if (!current.equals(historical)) { + throw new ArchivePersistenceException( + "State Archive diagnostic mismatch at block " + blockNumber + " Store " + dbName); + } + if (historical.isPresent()) { + presentCount++; + } else { + absentCount++; + } + logger.info("State archive diagnostic point: block={}, store={}, sample={}, key={}, " + + "present={}, valueSha256={}", + blockNumber, dbName, sample.presentSample ? "present" : "absent", + Hex.toHexString(sample.key), historical.isPresent(), digest(historical)); + } + + AccountAssetStore accountAssetStore = manager.getAccountAssetStore(); + Sample assetSample = StoreView.from(accountAssetStore.getDbSource()) + .requirePresentSample(ACCOUNT_ASSET_DATABASE); + DecodedAssetRow decoded = new P66AccountAssetCodec() + .decodePresentAssetRow(assetSample.key, assetSample.value); + HistoricalAccountAssetBalanceResolver.Result logical = + manager.getArchiveAccountAssetBalance(blockNumber, decoded.getAccountAddress(), + decoded.getTokenId()); + if (!logical.isAccountPresent() || logical.getPhase() != Phase.P66_ON + || logical.getBalance() != decoded.getBalance()) { + throw new ArchivePersistenceException( + "State Archive diagnostic P66 AccountAsset mismatch at block " + blockNumber); + } + logger.info("State archive diagnostic P66: block={}, address={}, tokenId={}, phase={}, " + + "balance={}", + blockNumber, Hex.toHexString(decoded.getAccountAddress()), decoded.getTokenId(), + logical.getPhase(), logical.getBalance()); + + Map currentBalances = currentAccountAssetBalances( + accountAssetStore, decoded.getAccountAddress()); + HistoricalAccountAssetPrefixResolver.Limits limits = prefixLimits( + accountAssetStore, decoded.getAccountAddress()); + HistoricalAccountAssetPrefixResolver.Result prefix = manager.getArchiveAccountAssets( + blockNumber, decoded.getAccountAddress(), limits); + if (!prefix.isAccountPresent() || prefix.getPhase() != Phase.P66_ON + || prefix.getBalances().size() != currentBalances.size()) { + throw new ArchivePersistenceException( + "State Archive diagnostic P66 AccountAsset prefix mismatch at block " + blockNumber); + } + for (Balance balance : prefix.getBalances()) { + Long currentBalance = currentBalances.get(balance.getTokenId()); + if (currentBalance == null || currentBalance != balance.getBalance()) { + throw new ArchivePersistenceException( + "State Archive diagnostic P66 AccountAsset prefix value mismatch at block " + + blockNumber); + } + } + logger.info("State archive diagnostic P66 prefix: block={}, address={}, phase={}, entries={}", + blockNumber, Hex.toHexString(decoded.getAccountAddress()), prefix.getPhase(), + prefix.getBalances().size()); + return new Report(blockNumber, expectedStores.size(), presentCount, absentCount, + logical.getPhase(), logical.getBalance(), prefix.getBalances().size()); + } + + private static Map currentAccountAssetBalances(AccountAssetStore store, + byte[] address) { + Map balances = new java.util.TreeMap<>(); + P66AccountAssetCodec codec = new P66AccountAssetCodec(); + for (Map.Entry entry : store.prefixQuery(address).entrySet()) { + DecodedAssetRow decoded = codec.decodePresentAssetRow( + entry.getKey().getBytes(), entry.getValue()); + if (!Arrays.equals(address, decoded.getAccountAddress()) + || balances.put(decoded.getTokenId(), decoded.getBalance()) != null) { + throw new ArchivePersistenceException( + "State Archive diagnostic current AccountAsset prefix is invalid"); + } + } + if (balances.isEmpty()) { + throw new ArchivePersistenceException( + "State Archive diagnostic requires a nonempty AccountAsset prefix"); + } + return balances; + } + + private static HistoricalAccountAssetPrefixResolver.Limits prefixLimits( + AccountAssetStore store, byte[] address) { + int entries = 0; + int maxKeyBytes = 1; + int maxValueBytes = 1; + long totalBytes = 0L; + for (Map.Entry entry : store.prefixQuery(address).entrySet()) { + byte[] key = entry.getKey().getBytes(); + byte[] value = Objects.requireNonNull(entry.getValue(), "AccountAsset prefix value"); + entries++; + maxKeyBytes = Math.max(maxKeyBytes, key.length); + maxValueBytes = Math.max(maxValueBytes, value.length); + totalBytes = Math.addExact(totalBytes, Math.addExact((long) key.length, value.length)); + } + if (entries == 0) { + throw new ArchivePersistenceException( + "State Archive diagnostic requires AccountAsset prefix limits"); + } + return new HistoricalAccountAssetPrefixResolver.Limits( + 1, entries, entries, maxKeyBytes, maxValueBytes, totalBytes); + } + + private static Map collectStores(Manager manager, + SnapshotManager snapshotManager) { + Map stores = new java.util.TreeMap<>(); + for (Chainbase database : snapshotManager.getDbs()) { + if (!ArchiveStoreScope.isStateDatabase(database.getDbName())) { + continue; + } + if (stores.put(database.getDbName(), StoreView.from(database)) != null) { + throw new ArchivePersistenceException( + "Duplicate State Archive diagnostic Store " + database.getDbName()); + } + } + if (!stores.containsKey(ACCOUNT_ASSET_DATABASE)) { + AccountAssetStore accountAssetStore = manager.getAccountAssetStore(); + if (accountAssetStore == null) { + throw new ArchivePersistenceException( + "State Archive diagnostic requires account-asset Store"); + } + stores.put(ACCOUNT_ASSET_DATABASE, StoreView.from(accountAssetStore.getDbSource())); + } + return stores; + } + + private static String digest(OldValue value) { + return value.isPresent() ? Hashing.sha256().hashBytes(value.getValue()).toString() : "absent"; + } + + private interface StoreView { + + Iterator> iterator(); + + byte[] get(byte[] key); + + default Sample sample(String dbName) { + Iterator> iterator = iterator(); + try { + if (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + return new Sample(entry.getKey(), entry.getValue(), true); + } + } finally { + close(iterator, dbName); + } + for (int attempt = 0; attempt < ABSENT_KEY_ATTEMPTS; attempt++) { + byte[] key = Hashing.sha256().hashString( + "state-archive-diagnostic\u0000" + dbName + "\u0000" + attempt, + StandardCharsets.UTF_8).asBytes(); + if (get(key) == null) { + return new Sample(key, null, false); + } + } + throw new ArchivePersistenceException( + "Unable to construct absent State Archive diagnostic key for " + dbName); + } + + default Sample requirePresentSample(String dbName) { + Sample sample = sample(dbName); + if (!sample.presentSample) { + throw new ArchivePersistenceException( + "State Archive diagnostic requires a present sample for " + dbName); + } + return sample; + } + + static StoreView from(Chainbase database) { + return new StoreView() { + @Override + public Iterator> iterator() { + return database.iterator(); + } + + @Override + public byte[] get(byte[] key) { + return database.getUnchecked(key); + } + }; + } + + static StoreView from(DbSourceInter database) { + return new StoreView() { + @Override + public Iterator> iterator() { + return database.iterator(); + } + + @Override + public byte[] get(byte[] key) { + return database.getData(key); + } + }; + } + + static void close(Iterator iterator, String dbName) { + if (!(iterator instanceof AutoCloseable)) { + return; + } + try { + ((AutoCloseable) iterator).close(); + } catch (Exception failure) { + throw new ArchivePersistenceException( + "Failed to close State Archive diagnostic iterator for " + dbName, failure); + } + } + } + + private static final class Sample { + private final byte[] key; + private final byte[] value; + private final boolean presentSample; + + private Sample(byte[] key, byte[] value, boolean presentSample) { + this.key = Arrays.copyOf(Objects.requireNonNull(key, "key"), key.length); + this.value = value == null ? null : Arrays.copyOf(value, value.length); + this.presentSample = presentSample; + } + } + + static final class Report { + private final long blockNumber; + private final int storeCount; + private final int presentCount; + private final int absentCount; + private final Phase p66Phase; + private final long p66Balance; + private final int p66PrefixCount; + + private Report(long blockNumber, int storeCount, int presentCount, int absentCount, + Phase p66Phase, long p66Balance, int p66PrefixCount) { + this.blockNumber = blockNumber; + this.storeCount = storeCount; + this.presentCount = presentCount; + this.absentCount = absentCount; + this.p66Phase = p66Phase; + this.p66Balance = p66Balance; + this.p66PrefixCount = p66PrefixCount; + } + + long getBlockNumber() { + return blockNumber; + } + + int getStoreCount() { + return storeCount; + } + + int getPresentCount() { + return presentCount; + } + + int getAbsentCount() { + return absentCount; + } + + Phase getP66Phase() { + return p66Phase; + } + + long getP66Balance() { + return p66Balance; + } + + int getP66PrefixCount() { + return p66PrefixCount; + } + } +} diff --git a/framework/src/main/java/org/tron/program/FullNode.java b/framework/src/main/java/org/tron/program/FullNode.java index 96b9f73d577..e05416fd133 100644 --- a/framework/src/main/java/org/tron/program/FullNode.java +++ b/framework/src/main/java/org/tron/program/FullNode.java @@ -58,6 +58,7 @@ public static void main(String[] args) { context.register(DefaultConfig.class); context.refresh(); Application appT = ApplicationFactory.create(context); + runArchiveStateDiagnostic(() -> ArchiveStateDiagnostic.runIfEnabled(context)); context.registerShutdownHook(); appT.startup(); if (parameter.isSolidityNode()) { @@ -67,6 +68,15 @@ public static void main(String[] args) { appT.blockUntilShutdown(); } + static void runArchiveStateDiagnostic(Runnable diagnostic) { + try { + diagnostic.run(); + } catch (RuntimeException failure) { + throw new TronError("State Archive startup diagnostic failed", failure, + TronError.ErrCode.STATE_ARCHIVE_INIT); + } + } + private static void checkJdkVersion() { try { Arch.throwIfUnsupportedJavaVersion(); diff --git a/framework/src/test/java/org/tron/program/ArchiveStateDiagnosticTest.java b/framework/src/test/java/org/tron/program/ArchiveStateDiagnosticTest.java new file mode 100644 index 00000000000..b1ea4322724 --- /dev/null +++ b/framework/src/test/java/org/tron/program/ArchiveStateDiagnosticTest.java @@ -0,0 +1,194 @@ +package org.tron.program; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.primitives.Longs; +import java.nio.charset.StandardCharsets; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.tron.common.application.TronApplicationContext; +import org.tron.core.db.Manager; +import org.tron.core.db.common.DbSourceInter; +import org.tron.core.db2.archive.ArchivePersistenceException; +import org.tron.core.db2.archive.ArchiveStoreScope; +import org.tron.core.db2.archive.HistoricalAccountAssetBalanceResolver; +import org.tron.core.db2.archive.HistoricalAccountAssetPrefixResolver; +import org.tron.core.db2.archive.OldValue; +import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.exception.TronError; +import org.tron.core.store.AccountAssetStore; +import org.tron.core.store.DynamicPropertiesStore; + +public class ArchiveStateDiagnosticTest { + + private static final long BLOCK_NUMBER = 100L; + + @Test + public void shouldConvertDiagnosticFailureToFatalStartupError() { + ArchivePersistenceException failure = new ArchivePersistenceException("injected failure"); + + TronError fatal = assertThrows(TronError.class, + () -> FullNode.runArchiveStateDiagnostic(() -> { + throw failure; + })); + + assertEquals(TronError.ErrCode.STATE_ARCHIVE_INIT, fatal.getErrCode()); + assertEquals(failure, fatal.getCause()); + } + + @Test + public void shouldRemainDisabledWithoutExplicitSystemProperty() { + String previous = System.getProperty(ArchiveStateDiagnostic.ENABLE_PROPERTY); + System.clearProperty(ArchiveStateDiagnostic.ENABLE_PROPERTY); + try { + TronApplicationContext context = mock(TronApplicationContext.class); + + ArchiveStateDiagnostic.runIfEnabled(context); + + verifyNoInteractions(context); + } finally { + if (previous == null) { + System.clearProperty(ArchiveStateDiagnostic.ENABLE_PROPERTY); + } else { + System.setProperty(ArchiveStateDiagnostic.ENABLE_PROPERTY, previous); + } + } + } + + @Test + public void shouldCompareExact27AndP66ThroughManagerRequests() { + Manager manager = mock(Manager.class); + SnapshotManager snapshotManager = mock(SnapshotManager.class); + DynamicPropertiesStore properties = mock(DynamicPropertiesStore.class); + AccountAssetStore accountAssetStore = mock(AccountAssetStore.class); + @SuppressWarnings("unchecked") + DbSourceInter accountAssetSource = mock(DbSourceInter.class); + when(manager.getDynamicPropertiesStore()).thenReturn(properties); + when(properties.getLatestBlockHeaderNumber()).thenReturn(BLOCK_NUMBER); + when(manager.getAccountAssetStore()).thenReturn(accountAssetStore); + when(accountAssetStore.getDbSource()).thenReturn(accountAssetSource); + + byte[] address = new byte[21]; + address[0] = 0x41; + Arrays.fill(address, 1, address.length, (byte) 7); + String tokenId = "1000001"; + byte[] assetKey = concat(address, tokenId.getBytes(StandardCharsets.US_ASCII)); + byte[] assetValue = Longs.toByteArray(9L); + when(accountAssetSource.iterator()).thenAnswer(invocation -> Collections.singletonList( + entry(assetKey, assetValue)).iterator()); + when(accountAssetSource.getData(any(byte[].class))).thenAnswer(invocation -> + Arrays.equals(assetKey, invocation.getArgument(0)) ? assetValue : null); + when(accountAssetStore.prefixQuery(any(byte[].class))).thenReturn(Collections.singletonMap( + WrappedByteArray.of(assetKey), assetValue)); + + List databases = new ArrayList<>(); + for (String dbName : ArchiveStoreScope.getStateDatabases()) { + if ("account-asset".equals(dbName)) { + continue; + } + Chainbase database = mock(Chainbase.class); + when(database.getDbName()).thenReturn(dbName); + if ("nullifier".equals(dbName)) { + when(database.iterator()).thenAnswer(invocation -> Collections.emptyIterator()); + when(database.getUnchecked(any(byte[].class))).thenReturn(null); + } else { + byte[] key = bytes("key-" + dbName); + byte[] value = bytes("value-" + dbName); + when(database.iterator()).thenAnswer(invocation -> Collections.singletonList( + entry(key, value)).iterator()); + when(database.getUnchecked(any(byte[].class))).thenAnswer(invocation -> + Arrays.equals(key, invocation.getArgument(0)) ? value : null); + } + databases.add(database); + } + when(snapshotManager.getDbs()).thenReturn(databases); + when(manager.getArchiveStateValue(anyLong(), anyString(), any(byte[].class))) + .thenAnswer(invocation -> { + String dbName = invocation.getArgument(1); + if ("nullifier".equals(dbName)) { + return OldValue.absent(); + } + if ("account-asset".equals(dbName)) { + return OldValue.present(assetValue); + } + return OldValue.present(bytes("value-" + dbName)); + }); + + HistoricalAccountAssetBalanceResolver.Result logical = + mock(HistoricalAccountAssetBalanceResolver.Result.class); + when(logical.isAccountPresent()).thenReturn(true); + when(logical.getPhase()).thenReturn(Phase.P66_ON); + when(logical.getBalance()).thenReturn(9L); + when(manager.getArchiveAccountAssetBalance(eq(BLOCK_NUMBER), any(byte[].class), eq(tokenId))) + .thenReturn(logical); + HistoricalAccountAssetPrefixResolver.Balance prefixBalance = + mock(HistoricalAccountAssetPrefixResolver.Balance.class); + when(prefixBalance.getTokenId()).thenReturn(tokenId); + when(prefixBalance.getBalance()).thenReturn(9L); + HistoricalAccountAssetPrefixResolver.Result prefix = + mock(HistoricalAccountAssetPrefixResolver.Result.class); + when(prefix.isAccountPresent()).thenReturn(true); + when(prefix.getPhase()).thenReturn(Phase.P66_ON); + when(prefix.getBalances()).thenReturn(Collections.singletonList(prefixBalance)); + when(manager.getArchiveAccountAssets(eq(BLOCK_NUMBER), any(byte[].class), + any(HistoricalAccountAssetPrefixResolver.Limits.class))).thenReturn(prefix); + + ArchiveStateDiagnostic.Report report = ArchiveStateDiagnostic.run(manager, snapshotManager); + + assertEquals(BLOCK_NUMBER, report.getBlockNumber()); + assertEquals(27, report.getStoreCount()); + assertEquals(26, report.getPresentCount()); + assertEquals(1, report.getAbsentCount()); + assertEquals(Phase.P66_ON, report.getP66Phase()); + assertEquals(9L, report.getP66Balance()); + assertEquals(1, report.getP66PrefixCount()); + } + + @Test + public void shouldRejectIncompleteRuntimeStoreScope() { + Manager manager = mock(Manager.class); + SnapshotManager snapshotManager = mock(SnapshotManager.class); + DynamicPropertiesStore properties = mock(DynamicPropertiesStore.class); + AccountAssetStore accountAssetStore = mock(AccountAssetStore.class); + @SuppressWarnings("unchecked") + DbSourceInter accountAssetSource = mock(DbSourceInter.class); + when(manager.getDynamicPropertiesStore()).thenReturn(properties); + when(properties.getLatestBlockHeaderNumber()).thenReturn(BLOCK_NUMBER); + when(manager.getAccountAssetStore()).thenReturn(accountAssetStore); + when(accountAssetStore.getDbSource()).thenReturn(accountAssetSource); + when(snapshotManager.getDbs()).thenReturn(Collections.emptyList()); + + assertThrows(ArchivePersistenceException.class, + () -> ArchiveStateDiagnostic.run(manager, snapshotManager)); + } + + private static Map.Entry entry(byte[] key, byte[] value) { + return new SimpleImmutableEntry<>(key, value); + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] concat(byte[] first, byte[] second) { + byte[] result = Arrays.copyOf(first, first.length + second.length); + System.arraycopy(second, 0, result, first.length, second.length); + return result; + } +} From 7389b1cb298c7df86b2f8f94201dede37c74f576 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 26 Aug 2026 16:11:44 +0800 Subject: [PATCH 065/161] feat(chainbase): add exact archive serving index build and publish the exact-27 serving index incrementally from committed history, bind it to Manager recovery and publication, and expose measured runtime inspection with restart and fail-closed coverage --- .../db2/archive/ArchiveHistoryWriter.java | 49 + .../LatestStateGenerationCoordinator.java | 4 + .../PersistentServingKeyIndexGeneration.java | 984 +++++++++++++++++- .../archive/ServingIndexIncrementalPlan.java | 279 +++++ .../db2/archive/StateArchiveRuntimeOwner.java | 316 +++++- .../main/java/org/tron/core/db/Manager.java | 14 + .../db2/archive/ArchiveHistoryWriterTest.java | 33 + ...rsistentServingKeyIndexGenerationTest.java | 133 +++ .../ServingIndexIncrementalPlanTest.java | 193 ++++ ...eArchiveManagerStartupIntegrationTest.java | 140 ++- 10 files changed, 2072 insertions(+), 73 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ServingIndexIncrementalPlan.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/ServingIndexIncrementalPlanTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java index 1e84487bf86..e0058bfa11a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java @@ -251,6 +251,55 @@ public synchronized PersistentServingKeyIndexGeneration buildServingGeneration( participatingDatabases, latestSourceIdentityDigest); } + /** + * Plans the exact-27 serving delta after durable I without scanning history before I. + * + *

The returned plan contains no persistent encoding. The caller must atomically apply and + * publish the complete plan before advancing durable serving authority. + */ + public synchronized ServingIndexIncrementalPlan planServingIncrement(long indexedThrough, + byte[] indexedHeadHash) throws IOException { + HistoryCommitMarker head = commits.head(); + if (head == null) { + throw new IllegalStateException("Cannot plan a serving increment from empty history"); + } + long firstEpoch = commits.firstEpoch(); + long baseEpoch = firstEpoch - 1; + byte[] expectedHash; + if (indexedThrough == baseEpoch) { + expectedHash = commits.get(firstEpoch).getMeta().getParentHash(); + } else if (indexedThrough >= firstEpoch + && indexedThrough <= head.getMeta().getEpoch()) { + HistoryCommitMarker indexed = commits.get(indexedThrough); + if (indexed == null) { + throw new ArchivePersistenceException("Serving I is outside committed history"); + } + expectedHash = indexed.getMeta().getBlockHash(); + } else { + throw new ArchivePersistenceException("Serving I is outside committed history coverage"); + } + if (!Arrays.equals(expectedHash, indexedHeadHash)) { + throw new ArchivePersistenceException("Serving I hash differs from committed history"); + } + + List suffix = new ArrayList<>(); + for (long epoch = indexedThrough + 1; epoch <= head.getMeta().getEpoch(); epoch++) { + HistoryCommitMarker marker = commits.get(epoch); + if (marker == null) { + throw new ArchivePersistenceException("Committed history suffix contains a gap"); + } + suffix.add(marker); + } + return ServingIndexIncrementalPlan.plan(indexedThrough, indexedHeadHash, + participatingDatabases, suffix, index::read); + } + + /** Plans a deterministic v5 rebuild from the archive coverage base through current H. */ + public synchronized ServingIndexIncrementalPlan planServingRebuild() throws IOException { + ServingSource source = servingSource(); + return planServingIncrement(source.baseEpoch, source.baseHash); + } + /** Recomputes the exact all-Store serving source identity without writing a generation. */ public synchronized ServingKeyIndexGeneration buildServingIdentity(String generationId) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java index 4a751c0846b..c9a53db3acb 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinator.java @@ -250,6 +250,10 @@ public long getBlockNumber() { return authority.getEpoch(); } + public String getGenerationId() { + return generationId; + } + public byte[] getBlockHash() { return authority.getBlockHash(); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java index 63bec99c64a..5c6e873e5ec 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java @@ -11,6 +11,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; @@ -19,9 +20,15 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.OptionalLong; +import java.util.stream.Stream; +import org.rocksdb.Checkpoint; +import org.rocksdb.CompressionType; import org.rocksdb.Options; import org.rocksdb.RocksDB; import org.rocksdb.RocksDBException; @@ -34,13 +41,26 @@ public final class PersistentServingKeyIndexGeneration implements ServingKeyInde private static final int MAGIC = 0x534b4947; // SKIG private static final short VERSION = 4; + private static final short EXACT_VERSION = 5; private static final int MAX_MANIFEST_SIZE = 1024 * 1024; private static final byte DATA_PREFIX = 1; private static final byte RANGE_DATA_PREFIX = 2; + private static final byte KEY_META_PREFIX = 3; + private static final byte KEY_PAGE_PREFIX = 4; + private static final byte STORE_COVERAGE_PREFIX = 5; + private static final int INLINE_EPOCH_LIMIT = 4; + private static final int EPOCHS_PER_PAGE = 512; + private static final byte INLINE = 1; + private static final byte PAGED = 2; private static final byte[] PRESENT = new byte[]{1}; private static final String MANIFEST = "generation.meta"; private static final String MANIFEST_TEMP = "generation.meta.tmp"; private static final String DATABASE = "keys"; + private static final String ESTIMATED_LIVE_DATA_SIZE = + "rocksdb.estimate-live-data-size"; + private static final String TOTAL_SST_FILES_SIZE = "rocksdb.total-sst-files-size"; + private static final String PENDING_COMPACTION_BYTES = + "rocksdb.estimate-pending-compaction-bytes"; static { RocksDB.loadLibrary(); @@ -59,9 +79,17 @@ private PersistentServingKeyIndexGeneration(Path directory, Descriptor descripto this.descriptor = descriptor; this.release = Objects.requireNonNull(release, "release"); this.options = new Options().setCreateIfMissing(false); + RocksDB opened = null; try { - this.database = RocksDB.openReadOnly(options, directory.resolve(DATABASE).toString()); - } catch (RocksDBException failure) { + opened = RocksDB.openReadOnly(options, directory.resolve(DATABASE).toString()); + if (descriptor.formatVersion == EXACT_VERSION) { + validateExactStoreCoverage(opened, descriptor); + } + this.database = opened; + } catch (RocksDBException | RuntimeException failure) { + if (opened != null) { + opened.close(); + } options.close(); throw new IOException("Failed to open serving index generation", failure); } @@ -153,6 +181,95 @@ static PersistentServingKeyIndexGeneration open(Path directory, Runnable release return new PersistentServingKeyIndexGeneration(directory, loadDescriptor(directory), release); } + /** Creates one approved v5 exact-only generation from a validated logical increment plan. */ + public static PersistentServingKeyIndexGeneration buildExact(Path directory, + String generationId, ServingIndexIncrementalPlan plan, + byte[] latestSourceIdentityDigest) throws IOException { + return buildExact(directory, generationId, plan, latestSourceIdentityDigest, () -> { }); + } + + static PersistentServingKeyIndexGeneration buildExact(Path directory, String generationId, + ServingIndexIncrementalPlan plan, byte[] latestSourceIdentityDigest, + ExactWriteFaultHook faultHook) throws IOException { + Objects.requireNonNull(directory, "directory"); + Objects.requireNonNull(plan, "plan"); + Objects.requireNonNull(faultHook, "faultHook"); + validateExactIdentity(generationId, plan, latestSourceIdentityDigest); + if (Files.exists(directory)) { + throw new IllegalArgumentException("Serving generation directory already exists"); + } + Files.createDirectories(directory); + byte[] sourceDigest = rollSourceDigest(plan.getSourceSeedDigest(), + plan.getSourceStepDigests()); + long keyChanges; + try (Options buildOptions = exactOptions(true); + RocksDB target = RocksDB.open(buildOptions, directory.resolve(DATABASE).toString())) { + keyChanges = applyExactPlan(target, generationId, plan, plan.getIndexedFrom(), + sourceDigest, faultHook); + } catch (RocksDBException failure) { + throw new IOException("Failed to build exact serving generation", failure); + } + Descriptor descriptor = new Descriptor(EXACT_VERSION, + ArchiveParticipantDescriptor.FORMAT_ID, generationId, plan.getIndexedFrom(), + plan.getIndexedThrough(), plan.getHeadHash(), sourceDigest, latestSourceIdentityDigest, + plan.getParticipatingDatabases(), keyChanges); + persistDescriptor(directory, descriptor); + HistorySegmentStore.syncDirectory(directory); + return open(directory); + } + + /** Checkpoints this immutable v5 generation and applies only the validated {@code (I,H]} plan. */ + public synchronized PersistentServingKeyIndexGeneration extendExact(Path directory, + String generationId, ServingIndexIncrementalPlan plan, + byte[] latestSourceIdentityDigest) throws IOException { + return extendExact(directory, generationId, plan, latestSourceIdentityDigest, () -> { }); + } + + synchronized PersistentServingKeyIndexGeneration extendExact(Path directory, + String generationId, ServingIndexIncrementalPlan plan, + byte[] latestSourceIdentityDigest, ExactWriteFaultHook faultHook) throws IOException { + ensureOpen(); + Objects.requireNonNull(directory, "directory"); + Objects.requireNonNull(plan, "plan"); + Objects.requireNonNull(faultHook, "faultHook"); + validateExactIdentity(generationId, plan, latestSourceIdentityDigest); + if (descriptor.formatVersion != EXACT_VERSION + || plan.getIndexedFrom() != descriptor.indexedThrough + || !Arrays.equals(plan.getIndexedFromHash(), descriptor.headHash) + || !plan.getParticipatingDatabases().equals(descriptor.participants)) { + throw new IllegalArgumentException("Exact serving increment does not extend current I"); + } + if (Files.exists(directory)) { + throw new IllegalArgumentException("Serving generation directory already exists"); + } + Files.createDirectories(directory); + try (Options checkpointOptions = exactOptions(false); + RocksDB checkpointSource = RocksDB.open(checkpointOptions, + this.directory.resolve(DATABASE).toString()); + Checkpoint checkpoint = Checkpoint.create(checkpointSource)) { + checkpoint.createCheckpoint(directory.resolve(DATABASE).toString()); + } catch (RocksDBException failure) { + throw new IOException("Failed to checkpoint exact serving generation", failure); + } + byte[] sourceDigest = rollSourceDigest(descriptor.sourceDigest, + plan.getSourceStepDigests()); + long added; + try (Options writeOptions = exactOptions(false); + RocksDB target = RocksDB.open(writeOptions, directory.resolve(DATABASE).toString())) { + added = applyExactPlan(target, generationId, plan, descriptor.indexedFrom, + sourceDigest, faultHook); + } catch (RocksDBException failure) { + throw new IOException("Failed to extend exact serving generation", failure); + } + Descriptor replacement = new Descriptor(EXACT_VERSION, descriptor.scopeIdentity, + generationId, descriptor.indexedFrom, plan.getIndexedThrough(), plan.getHeadHash(), + sourceDigest, latestSourceIdentityDigest, descriptor.participants, + descriptor.keyChanges + added); + persistDescriptor(directory, replacement); + HistorySegmentStore.syncDirectory(directory); + return open(directory); + } + @Override public synchronized OptionalLong firstChangeAfter(String dbName, byte[] rawKey, long targetBlock, long upperBound) throws IOException { @@ -163,6 +280,9 @@ public synchronized OptionalLong firstChangeAfter(String dbName, byte[] rawKey, if (targetBlock == Long.MAX_VALUE) { return OptionalLong.empty(); } + if (descriptor.formatVersion == EXACT_VERSION) { + return firstExactChangeAfter(dbName, rawKey, targetBlock, upperBound); + } byte[] prefix = dataPrefix(dbName, rawKey); byte[] seek = ByteBuffer.allocate(prefix.length + Long.BYTES).put(prefix) .putLong(targetBlock + 1).array(); @@ -294,7 +414,66 @@ public boolean isLatestSourceIdentityBound() { } public boolean supportsRangeQueries() { - return descriptor.formatVersion >= VERSION; + return descriptor.formatVersion == VERSION; + } + + public boolean isExactOnlyFormat() { + return descriptor.formatVersion == EXACT_VERSION; + } + + public PersistentStoreCoverage getPersistentStoreCoverage(String dbName) throws IOException { + ensureOpen(); + if (!isExactOnlyFormat()) { + throw new ArchivePersistenceException("Serving generation has no durable Store coverage"); + } + byte[] encoded; + try { + encoded = database.get(storeCoverageKey(dbName)); + } catch (RocksDBException failure) { + throw new IOException("Failed to read serving Store coverage", failure); + } + PersistentStoreCoverage coverage = decodeCoverage(encoded); + if (!coverage.dbName.equals(dbName) + || coverage.indexedFrom != descriptor.indexedFrom + || coverage.indexedThrough != descriptor.indexedThrough + || !Arrays.equals(coverage.headHash, descriptor.headHash) + || !Arrays.equals(coverage.sourceDigest, descriptor.sourceDigest) + || !coverage.generationId.equals(descriptor.generationId)) { + throw new ArchivePersistenceException("Serving Store coverage identity mismatch"); + } + return coverage; + } + + /** Performs an explicit read-only scan of v5 metadata for measured observability. */ + public synchronized GenerationStatistics inspectStatistics() throws IOException { + return inspectStatistics(this::readLongProperty); + } + + synchronized GenerationStatistics inspectStatistics(RocksPropertyReader propertyReader) + throws IOException { + ensureOpen(); + Objects.requireNonNull(propertyReader, "propertyReader"); + if (!isExactOnlyFormat()) { + throw new ArchivePersistenceException("Serving statistics require exact-only format"); + } + Map stores = new LinkedHashMap<>(); + for (String participant : descriptor.participants) { + stores.put(participant, inspectStore(participant)); + } + long measuredChanges = stores.values().stream() + .mapToLong(StoreStatistics::getChangeEntryCount).sum(); + if (measuredChanges != descriptor.keyChanges) { + throw new ArchivePersistenceException( + "Serving statistics differ from generation change count"); + } + FileSizeMeasurement files = measureGenerationFiles(directory); + EngineStatistics engine = new EngineStatistics( + readProperty(propertyReader, ESTIMATED_LIVE_DATA_SIZE), + readProperty(propertyReader, TOTAL_SST_FILES_SIZE), + readProperty(propertyReader, PENDING_COMPACTION_BYTES)); + return new GenerationStatistics(descriptor.generationId, descriptor.indexedFrom, + descriptor.indexedThrough, stores, files.apparentBytes, files.allocatedBytes, + files.allocatedExact, engine); } Path getDirectory() { @@ -327,6 +506,511 @@ private void ensureOpen() { } } + private StoreStatistics inspectStore(String dbName) throws IOException { + byte[] metaPrefix = exactPartitionPrefix(KEY_META_PREFIX, dbName); + byte[] pagePrefix = exactPartitionPrefix(KEY_PAGE_PREFIX, dbName); + long keyMetadata = 0; + long inlineKeys = 0; + long pagedKeys = 0; + long changeEntries = 0; + long expectedPagedEntries = 0; + long pages = 0; + long pagedEntries = 0; + long logicalBytes = 0; + try (RocksIterator iterator = database.newIterator()) { + iterator.seek(metaPrefix); + while (iterator.isValid() && startsWith(iterator.key(), metaPrefix)) { + byte[] key = iterator.key(); + byte[] value = iterator.value(); + KeyMeta meta = decodeKeyMeta(value); + keyMetadata++; + changeEntries += meta.count; + logicalBytes += key.length + value.length; + if (meta.mode == INLINE) { + inlineKeys++; + } else { + pagedKeys++; + expectedPagedEntries += meta.count; + } + iterator.next(); + } + iterator.status(); + } catch (RocksDBException failure) { + throw new IOException("Failed to inspect exact serving key metadata", failure); + } + try (RocksIterator iterator = database.newIterator()) { + iterator.seek(pagePrefix); + while (iterator.isValid() && startsWith(iterator.key(), pagePrefix)) { + byte[] key = iterator.key(); + byte[] value = iterator.value(); + pages++; + pagedEntries += decodeEpochPage(value).length; + logicalBytes += key.length + value.length; + iterator.next(); + } + iterator.status(); + } catch (RocksDBException failure) { + throw new IOException("Failed to inspect exact serving epoch pages", failure); + } + if (pagedEntries != expectedPagedEntries) { + throw new ArchivePersistenceException( + "Serving statistics found inconsistent paged entry totals: " + dbName); + } + byte[] coverageKey = storeCoverageKey(dbName); + try { + byte[] coverageValue = database.get(coverageKey); + decodeCoverage(coverageValue); + logicalBytes += coverageKey.length + coverageValue.length; + } catch (RocksDBException failure) { + throw new IOException("Failed to inspect serving Store coverage", failure); + } + return new StoreStatistics(dbName, keyMetadata, inlineKeys, pagedKeys, pages, + changeEntries, logicalBytes); + } + + private static byte[] exactPartitionPrefix(byte prefix, String dbName) { + int storeId = ArchiveParticipantDescriptor.current().getStoreId(dbName); + return ByteBuffer.allocate(1 + Short.BYTES).put(prefix).putShort((short) storeId).array(); + } + + private static FileSizeMeasurement measureGenerationFiles(Path root) throws IOException { + long apparent = 0; + long allocated = 0; + boolean exact = true; + try (Stream paths = Files.walk(root)) { + Iterator iterator = paths.filter(Files::isRegularFile).iterator(); + while (iterator.hasNext()) { + Path file = iterator.next(); + apparent += Files.size(file); + if (exact) { + try { + Number blocks = (Number) Files.getAttribute(file, "unix:blocks", + LinkOption.NOFOLLOW_LINKS); + allocated += blocks.longValue() * 512L; + } catch (UnsupportedOperationException | IllegalArgumentException failure) { + exact = false; + } + } + } + } + return new FileSizeMeasurement(apparent, exact ? allocated : apparent, exact); + } + + private OptionalLong readLongProperty(String name) { + try { + return OptionalLong.of(database.getLongProperty(name)); + } catch (RocksDBException | IllegalArgumentException failure) { + return OptionalLong.empty(); + } + } + + private static LongPropertyMeasurement readProperty(RocksPropertyReader reader, String name) + throws IOException { + OptionalLong value = Objects.requireNonNull(reader.read(name), "property value"); + if (value.isPresent() && value.getAsLong() < 0) { + throw new ArchivePersistenceException("Negative RocksDB property: " + name); + } + return value.isPresent() + ? LongPropertyMeasurement.available(value.getAsLong()) + : LongPropertyMeasurement.unavailable(); + } + + private OptionalLong firstExactChangeAfter(String dbName, byte[] rawKey, long targetBlock, + long upperBound) throws IOException { + KeyMeta meta; + try { + byte[] encoded = database.get(keyMetaKey(dbName, rawKey)); + if (encoded == null) { + return OptionalLong.empty(); + } + meta = decodeKeyMeta(encoded); + } catch (RocksDBException failure) { + throw new IOException("Failed to read exact serving key metadata", failure); + } + if (meta.lastEpoch <= targetBlock || meta.firstEpoch > upperBound) { + return OptionalLong.empty(); + } + if (meta.mode == INLINE) { + return firstChange(meta.inlineEpochs, targetBlock, upperBound); + } + int pageCount = pageCount(meta.count); + int low = 0; + int high = pageCount; + while (low < high) { + int middle = (low + high) >>> 1; + long[] page = readPage(dbName, rawKey, middle); + if (page[page.length - 1] <= targetBlock) { + low = middle + 1; + } else { + high = middle; + } + } + if (low == pageCount) { + return OptionalLong.empty(); + } + return firstChange(readPage(dbName, rawKey, low), targetBlock, upperBound); + } + + private long[] readPage(String dbName, byte[] rawKey, int pageIndex) throws IOException { + try { + byte[] encoded = database.get(keyPageKey(dbName, rawKey, pageIndex)); + if (encoded == null) { + throw new ArchivePersistenceException("Exact serving epoch page is missing"); + } + return decodeEpochPage(encoded); + } catch (RocksDBException failure) { + throw new IOException("Failed to read exact serving epoch page", failure); + } + } + + private static long applyExactPlan(RocksDB target, String generationId, + ServingIndexIncrementalPlan plan, long coverageFrom, byte[] sourceDigest, + ExactWriteFaultHook faultHook) throws IOException, RocksDBException { + Map> changes = new LinkedHashMap<>(); + for (Map.Entry> database + : plan.getChangesByDatabase().entrySet()) { + for (ServingIndexIncrementalPlan.KeyChange change : database.getValue()) { + ExactKey key = new ExactKey(database.getKey(), change.getRawKey()); + changes.computeIfAbsent(key, ignored -> new ArrayList<>()).add(change.getEpoch()); + } + } + try (WriteBatch batch = new WriteBatch(); WriteOptions writes = new WriteOptions() + .setSync(true)) { + for (Map.Entry> entry : changes.entrySet()) { + appendExactChanges(target, batch, entry.getKey(), entry.getValue()); + } + for (String database : plan.getParticipatingDatabases()) { + PersistentStoreCoverage coverage = new PersistentStoreCoverage(database, + coverageFrom, plan.getIndexedThrough(), plan.getHeadHash(), sourceDigest, + generationId, comparatorId(database)); + batch.put(storeCoverageKey(database), encodeCoverage(coverage)); + } + faultHook.beforeWrite(); + target.write(writes, batch); + } + return changes.values().stream().mapToLong(List::size).sum(); + } + + private static void appendExactChanges(RocksDB target, WriteBatch batch, ExactKey key, + List appended) throws RocksDBException { + byte[] metaKey = keyMetaKey(key.dbName, key.rawKey); + byte[] existing = target.get(metaKey); + KeyMeta meta = existing == null ? null : decodeKeyMeta(existing); + if (meta == null) { + requireStrictEpochs(appended, Long.MIN_VALUE); + if (appended.size() <= INLINE_EPOCH_LIMIT) { + batch.put(metaKey, encodeKeyMeta(KeyMeta.inline(toArray(appended)))); + return; + } + writeAllPages(batch, key, appended, 0); + batch.put(metaKey, encodeKeyMeta(KeyMeta.paged(appended.size(), appended.get(0), + appended.get(appended.size() - 1)))); + return; + } + requireStrictEpochs(appended, meta.lastEpoch); + if (meta.mode == INLINE && meta.count + appended.size() <= INLINE_EPOCH_LIMIT) { + List combined = asList(meta.inlineEpochs); + combined.addAll(appended); + batch.put(metaKey, encodeKeyMeta(KeyMeta.inline(toArray(combined)))); + return; + } + if (meta.mode == INLINE) { + List combined = asList(meta.inlineEpochs); + combined.addAll(appended); + writeAllPages(batch, key, combined, 0); + } else { + int lastPageIndex = pageCount(meta.count) - 1; + long[] lastPage = decodeEpochPage(target.get( + keyPageKey(key.dbName, key.rawKey, lastPageIndex))); + List combined = asList(lastPage); + combined.addAll(appended); + writeAllPages(batch, key, combined, lastPageIndex); + } + batch.put(metaKey, encodeKeyMeta(KeyMeta.paged(meta.count + appended.size(), + meta.firstEpoch, appended.get(appended.size() - 1)))); + } + + private static void writeAllPages(WriteBatch batch, ExactKey key, List epochs, + int firstPageIndex) throws RocksDBException { + for (int start = 0, page = firstPageIndex; start < epochs.size(); + start += EPOCHS_PER_PAGE, page++) { + int end = Math.min(start + EPOCHS_PER_PAGE, epochs.size()); + batch.put(keyPageKey(key.dbName, key.rawKey, page), + encodeEpochPage(toArray(epochs.subList(start, end)))); + } + } + + private static byte[] keyMetaKey(String dbName, byte[] rawKey) { + return exactKey(KEY_META_PREFIX, dbName, rawKey, null); + } + + private static byte[] keyPageKey(String dbName, byte[] rawKey, int pageIndex) { + if (pageIndex < 0) { + throw new IllegalArgumentException("Serving page index must not be negative"); + } + return exactKey(KEY_PAGE_PREFIX, dbName, rawKey, pageIndex); + } + + private static byte[] exactKey(byte prefix, String dbName, byte[] rawKey, Integer pageIndex) { + int storeId = ArchiveParticipantDescriptor.current().getStoreId(dbName); + byte[] encodedKey = encodeRangeRawKey(rawKey); + int suffix = pageIndex == null ? 0 : Integer.BYTES; + ByteBuffer key = ByteBuffer.allocate(1 + Short.BYTES + encodedKey.length + 2 + suffix) + .put(prefix).putShort((short) storeId).put(encodedKey).put((byte) 0).put((byte) 0); + if (pageIndex != null) { + key.putInt(pageIndex); + } + return key.array(); + } + + private static byte[] storeCoverageKey(String dbName) { + int storeId = ArchiveParticipantDescriptor.current().getStoreId(dbName); + return ByteBuffer.allocate(1 + Short.BYTES).put(STORE_COVERAGE_PREFIX) + .putShort((short) storeId).array(); + } + + private static byte[] encodeKeyMeta(KeyMeta meta) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeByte(meta.mode); + output.writeLong(meta.count); + output.writeLong(meta.firstEpoch); + output.writeLong(meta.lastEpoch); + if (meta.mode == INLINE) { + output.writeInt(meta.inlineEpochs.length); + for (long epoch : meta.inlineEpochs) { + output.writeLong(epoch); + } + } + output.flush(); + return withChecksum(bytes.toByteArray()); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected exact serving metadata encoding failure", + impossible); + } + } + + private static KeyMeta decodeKeyMeta(byte[] encoded) { + byte[] payload = checkedPayload(encoded, "exact serving key metadata"); + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload)); + byte mode = input.readByte(); + long count = input.readLong(); + long first = input.readLong(); + long last = input.readLong(); + if (count <= 0 || first < 0 || last < first || mode != INLINE && mode != PAGED) { + throw new ArchivePersistenceException("Invalid exact serving key metadata"); + } + long[] inline = null; + if (mode == INLINE) { + int size = input.readInt(); + if (size != count || size <= 0 || size > INLINE_EPOCH_LIMIT) { + throw new ArchivePersistenceException("Invalid inline serving metadata"); + } + inline = new long[size]; + for (int i = 0; i < size; i++) { + inline[i] = input.readLong(); + } + requireStrictEpochs(asList(inline), Long.MIN_VALUE); + } + if (input.available() != 0) { + throw new ArchivePersistenceException("Exact serving metadata payload mismatch"); + } + return new KeyMeta(mode, count, first, last, inline); + } catch (IOException failure) { + throw new ArchivePersistenceException("Exact serving metadata is truncated", failure); + } + } + + private static byte[] encodeEpochPage(long[] epochs) { + ByteBuffer payload = ByteBuffer.allocate(Integer.BYTES + epochs.length * Long.BYTES) + .putInt(epochs.length); + for (long epoch : epochs) { + payload.putLong(epoch); + } + return withChecksum(payload.array()); + } + + private static long[] decodeEpochPage(byte[] encoded) { + byte[] payload = checkedPayload(encoded, "exact serving epoch page"); + ByteBuffer input = ByteBuffer.wrap(payload); + if (input.remaining() < Integer.BYTES) { + throw new ArchivePersistenceException("Exact serving epoch page is truncated"); + } + int count = input.getInt(); + if (count <= 0 || count > EPOCHS_PER_PAGE + || input.remaining() != count * Long.BYTES) { + throw new ArchivePersistenceException("Invalid exact serving epoch page"); + } + long[] epochs = new long[count]; + for (int i = 0; i < count; i++) { + epochs[i] = input.getLong(); + } + requireStrictEpochs(asList(epochs), Long.MIN_VALUE); + return epochs; + } + + private static byte[] encodeCoverage(PersistentStoreCoverage coverage) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeUTF(coverage.dbName); + output.writeLong(coverage.indexedFrom); + output.writeLong(coverage.indexedThrough); + output.write(coverage.headHash); + output.write(coverage.sourceDigest); + output.writeUTF(coverage.generationId); + output.writeUTF(coverage.comparatorId); + output.flush(); + return withChecksum(bytes.toByteArray()); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected Store coverage encoding failure", impossible); + } + } + + private static PersistentStoreCoverage decodeCoverage(byte[] encoded) { + byte[] payload = checkedPayload(encoded, "serving Store coverage"); + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload)); + String database = input.readUTF(); + long from = input.readLong(); + long through = input.readLong(); + byte[] headHash = new byte[32]; + byte[] sourceDigest = new byte[32]; + input.readFully(headHash); + input.readFully(sourceDigest); + String generationId = input.readUTF(); + String comparatorId = input.readUTF(); + if (from < 0 || through < from || generationId.isEmpty() || comparatorId.isEmpty() + || input.available() != 0) { + throw new ArchivePersistenceException("Invalid serving Store coverage"); + } + return new PersistentStoreCoverage(database, from, through, headHash, sourceDigest, + generationId, comparatorId); + } catch (IOException failure) { + throw new ArchivePersistenceException("Serving Store coverage is truncated", failure); + } + } + + private static void validateExactStoreCoverage(RocksDB database, Descriptor descriptor) + throws RocksDBException { + for (String participant : descriptor.participants) { + PersistentStoreCoverage coverage = decodeCoverage(database.get( + storeCoverageKey(participant))); + if (!participant.equals(coverage.dbName) + || coverage.indexedFrom != descriptor.indexedFrom + || coverage.indexedThrough != descriptor.indexedThrough + || !Arrays.equals(coverage.headHash, descriptor.headHash) + || !Arrays.equals(coverage.sourceDigest, descriptor.sourceDigest) + || !coverage.generationId.equals(descriptor.generationId) + || !coverage.comparatorId.equals(comparatorId(participant))) { + throw new ArchivePersistenceException("Serving Store coverage identity mismatch"); + } + } + } + + private static byte[] withChecksum(byte[] payload) { + return ByteBuffer.allocate(payload.length + Integer.BYTES).put(payload) + .putInt(Hashing.crc32c().hashBytes(payload).asInt()).array(); + } + + private static byte[] checkedPayload(byte[] encoded, String name) { + if (encoded == null || encoded.length <= Integer.BYTES) { + throw new ArchivePersistenceException(name + " is missing or truncated"); + } + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(encoded, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new ArchivePersistenceException(name + " checksum mismatch"); + } + return payload; + } + + private static void requireStrictEpochs(List epochs, long previous) { + for (long epoch : epochs) { + if (epoch < 0 || epoch <= previous) { + throw new ArchivePersistenceException("Serving epochs are not strictly increasing"); + } + previous = epoch; + } + } + + private static int pageCount(long count) { + return (int) ((count + EPOCHS_PER_PAGE - 1) / EPOCHS_PER_PAGE); + } + + private static long[] toArray(List epochs) { + long[] result = new long[epochs.size()]; + for (int i = 0; i < epochs.size(); i++) { + result[i] = epochs.get(i); + } + return result; + } + + private static List asList(long[] epochs) { + List result = new ArrayList<>(epochs.length); + for (long epoch : epochs) { + result.add(epoch); + } + return result; + } + + private static OptionalLong firstChange(long[] epochs, long target, long upperBound) { + int low = 0; + int high = epochs.length; + while (low < high) { + int middle = (low + high) >>> 1; + if (epochs[middle] <= target) { + low = middle + 1; + } else { + high = middle; + } + } + return low < epochs.length && epochs[low] <= upperBound + ? OptionalLong.of(epochs[low]) : OptionalLong.empty(); + } + + private static byte[] rollingDigest(byte[] previous, byte[] delta) { + MessageDigest digest = sha256(); + digest.update(previous); + digest.update(delta); + return digest.digest(); + } + + private static byte[] rollSourceDigest(byte[] seed, List steps) { + byte[] result = Arrays.copyOf(seed, seed.length); + for (byte[] step : steps) { + result = rollingDigest(result, step); + } + return result; + } + + static byte[] sourceDigestForRebuild(ServingIndexIncrementalPlan plan) { + Objects.requireNonNull(plan, "plan"); + return rollSourceDigest(plan.getSourceSeedDigest(), plan.getSourceStepDigests()); + } + + private static String comparatorId(String dbName) { + return "market_pair_price_to_order".equals(dbName) + ? "MARKET_PRICE_V1" : "UNSIGNED_RAW_V1"; + } + + private static Options exactOptions(boolean create) { + return new Options().setCreateIfMissing(create) + .setCompressionType(CompressionType.NO_COMPRESSION); + } + + private static void validateExactIdentity(String generationId, + ServingIndexIncrementalPlan plan, byte[] latestSourceIdentityDigest) { + if (generationId == null || generationId.isEmpty()) { + throw new IllegalArgumentException("generationId must not be empty"); + } + requireHash(latestSourceIdentityDigest, "latestSourceIdentityDigest"); + ArchiveParticipantDescriptor.current().requireExactParticipants( + plan.getParticipatingDatabases()); + } + private static byte[] dataKey(String dbName, byte[] rawKey, long epoch) { if (epoch < 0) { throw new IllegalArgumentException("Serving index epoch must not be negative"); @@ -559,7 +1243,7 @@ private static byte[] encodeDescriptor(Descriptor descriptor) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(bytes); output.writeInt(MAGIC); - output.writeShort(VERSION); + output.writeShort(descriptor.formatVersion); output.writeShort(0); output.writeUTF(descriptor.scopeIdentity); output.writeUTF(descriptor.generationId); @@ -598,7 +1282,8 @@ private static Descriptor decodeDescriptor(byte[] encoded) { throw new IllegalArgumentException("Unsupported serving index manifest"); } short version = input.readShort(); - if ((version != VERSION && version != VERSION - 1) || input.readShort() != 0) { + if ((version != EXACT_VERSION && version != VERSION && version != VERSION - 1) + || input.readShort() != 0) { throw new IllegalArgumentException("Unsupported serving index manifest"); } String scopeIdentity = input.readUTF(); @@ -665,4 +1350,293 @@ private Descriptor(short formatVersion, String scopeIdentity, String generationI this.keyChanges = keyChanges; } } + + @FunctionalInterface + interface ExactWriteFaultHook { + void beforeWrite() throws IOException; + } + + /** Durable completeness identity for one exact-27 Store partition. */ + public static final class PersistentStoreCoverage { + private final String dbName; + private final long indexedFrom; + private final long indexedThrough; + private final byte[] headHash; + private final byte[] sourceDigest; + private final String generationId; + private final String comparatorId; + + private PersistentStoreCoverage(String dbName, long indexedFrom, long indexedThrough, + byte[] headHash, byte[] sourceDigest, String generationId, String comparatorId) { + this.dbName = dbName; + this.indexedFrom = indexedFrom; + this.indexedThrough = indexedThrough; + this.headHash = Arrays.copyOf(headHash, headHash.length); + this.sourceDigest = Arrays.copyOf(sourceDigest, sourceDigest.length); + this.generationId = generationId; + this.comparatorId = comparatorId; + } + + public String getDbName() { + return dbName; + } + + public long getIndexedFrom() { + return indexedFrom; + } + + public long getIndexedThrough() { + return indexedThrough; + } + + public byte[] getHeadHash() { + return Arrays.copyOf(headHash, headHash.length); + } + + public byte[] getSourceDigest() { + return Arrays.copyOf(sourceDigest, sourceDigest.length); + } + + public String getGenerationId() { + return generationId; + } + + public String getComparatorId() { + return comparatorId; + } + } + + /** Read-only measured statistics for one immutable v5 generation. */ + public static final class GenerationStatistics { + private final String generationId; + private final long indexedFrom; + private final long indexedThrough; + private final Map stores; + private final long apparentBytes; + private final long allocatedBytes; + private final boolean allocatedBytesExact; + private final EngineStatistics engine; + + private GenerationStatistics(String generationId, long indexedFrom, long indexedThrough, + Map stores, long apparentBytes, long allocatedBytes, + boolean allocatedBytesExact, EngineStatistics engine) { + this.generationId = generationId; + this.indexedFrom = indexedFrom; + this.indexedThrough = indexedThrough; + this.stores = Collections.unmodifiableMap(new LinkedHashMap<>(stores)); + this.apparentBytes = apparentBytes; + this.allocatedBytes = allocatedBytes; + this.allocatedBytesExact = allocatedBytesExact; + this.engine = engine; + } + + public String getGenerationId() { + return generationId; + } + + public long getIndexedFrom() { + return indexedFrom; + } + + public long getIndexedThrough() { + return indexedThrough; + } + + public Map getStores() { + return stores; + } + + public long getApparentBytes() { + return apparentBytes; + } + + public long getAllocatedBytes() { + return allocatedBytes; + } + + public boolean isAllocatedBytesExact() { + return allocatedBytesExact; + } + + public EngineStatistics getEngine() { + return engine; + } + } + + /** RocksDB properties sampled from the pinned immutable generation. */ + public static final class EngineStatistics { + private final LongPropertyMeasurement estimatedLiveDataBytes; + private final LongPropertyMeasurement totalSstBytes; + private final LongPropertyMeasurement pendingCompactionBytes; + + private EngineStatistics(LongPropertyMeasurement estimatedLiveDataBytes, + LongPropertyMeasurement totalSstBytes, + LongPropertyMeasurement pendingCompactionBytes) { + this.estimatedLiveDataBytes = estimatedLiveDataBytes; + this.totalSstBytes = totalSstBytes; + this.pendingCompactionBytes = pendingCompactionBytes; + } + + public LongPropertyMeasurement getEstimatedLiveDataBytes() { + return estimatedLiveDataBytes; + } + + public LongPropertyMeasurement getTotalSstBytes() { + return totalSstBytes; + } + + public LongPropertyMeasurement getPendingCompactionBytes() { + return pendingCompactionBytes; + } + } + + /** One property value with an explicit unsupported/unavailable state. */ + public static final class LongPropertyMeasurement { + private final boolean available; + private final long value; + + private LongPropertyMeasurement(boolean available, long value) { + this.available = available; + this.value = value; + } + + private static LongPropertyMeasurement available(long value) { + return new LongPropertyMeasurement(true, value); + } + + private static LongPropertyMeasurement unavailable() { + return new LongPropertyMeasurement(false, 0); + } + + public boolean isAvailable() { + return available; + } + + public long getValue() { + if (!available) { + throw new IllegalStateException("RocksDB property is unavailable"); + } + return value; + } + } + + /** Logical RocksDB entry statistics for one exact Store partition. */ + public static final class StoreStatistics { + private final String dbName; + private final long keyMetadataCount; + private final long inlineKeyCount; + private final long pagedKeyCount; + private final long pageCount; + private final long changeEntryCount; + private final long logicalBytes; + + private StoreStatistics(String dbName, long keyMetadataCount, long inlineKeyCount, + long pagedKeyCount, long pageCount, long changeEntryCount, long logicalBytes) { + this.dbName = dbName; + this.keyMetadataCount = keyMetadataCount; + this.inlineKeyCount = inlineKeyCount; + this.pagedKeyCount = pagedKeyCount; + this.pageCount = pageCount; + this.changeEntryCount = changeEntryCount; + this.logicalBytes = logicalBytes; + } + + public String getDbName() { + return dbName; + } + + public long getKeyMetadataCount() { + return keyMetadataCount; + } + + public long getInlineKeyCount() { + return inlineKeyCount; + } + + public long getPagedKeyCount() { + return pagedKeyCount; + } + + public long getPageCount() { + return pageCount; + } + + public long getChangeEntryCount() { + return changeEntryCount; + } + + public long getLogicalBytes() { + return logicalBytes; + } + } + + private static final class FileSizeMeasurement { + private final long apparentBytes; + private final long allocatedBytes; + private final boolean allocatedExact; + + private FileSizeMeasurement(long apparentBytes, long allocatedBytes, + boolean allocatedExact) { + this.apparentBytes = apparentBytes; + this.allocatedBytes = allocatedBytes; + this.allocatedExact = allocatedExact; + } + } + + @FunctionalInterface + interface RocksPropertyReader { + OptionalLong read(String name) throws IOException; + } + + private static final class KeyMeta { + private final byte mode; + private final long count; + private final long firstEpoch; + private final long lastEpoch; + private final long[] inlineEpochs; + + private KeyMeta(byte mode, long count, long firstEpoch, long lastEpoch, + long[] inlineEpochs) { + this.mode = mode; + this.count = count; + this.firstEpoch = firstEpoch; + this.lastEpoch = lastEpoch; + this.inlineEpochs = inlineEpochs; + } + + private static KeyMeta inline(long[] epochs) { + return new KeyMeta(INLINE, epochs.length, epochs[0], epochs[epochs.length - 1], epochs); + } + + private static KeyMeta paged(long count, long firstEpoch, long lastEpoch) { + return new KeyMeta(PAGED, count, firstEpoch, lastEpoch, null); + } + } + + private static final class ExactKey { + private final String dbName; + private final byte[] rawKey; + + private ExactKey(String dbName, byte[] rawKey) { + this.dbName = dbName; + this.rawKey = Arrays.copyOf(rawKey, rawKey.length); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ExactKey)) { + return false; + } + ExactKey that = (ExactKey) other; + return dbName.equals(that.dbName) && Arrays.equals(rawKey, that.rawKey); + } + + @Override + public int hashCode() { + return 31 * dbName.hashCode() + Arrays.hashCode(rawKey); + } + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ServingIndexIncrementalPlan.java b/chainbase/src/main/java/org/tron/core/db2/archive/ServingIndexIncrementalPlan.java new file mode 100644 index 00000000000..2ae07171315 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ServingIndexIncrementalPlan.java @@ -0,0 +1,279 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Backend-neutral logical plan for advancing the exact-27 serving index over {@code (I,H]}. + * + *

The plan deliberately defines no persistent key, page, checksum, compression, or partition + * encoding. A durable backend may apply it only after the corresponding format decision is + * approved. Construction validates the complete suffix before exposing a target boundary, so a + * rejected suffix cannot partially advance serving coverage. + */ +public final class ServingIndexIncrementalPlan { + + private final long indexedFrom; + private final byte[] indexedFromHash; + private final long indexedThrough; + private final byte[] headHash; + private final byte[] deltaSourceDigest; + private final byte[] sourceSeedDigest; + private final List sourceStepDigests; + private final List participatingDatabases; + private final Map> changesByDatabase; + + private ServingIndexIncrementalPlan(long indexedFrom, byte[] indexedFromHash, + long indexedThrough, byte[] headHash, byte[] deltaSourceDigest, + byte[] sourceSeedDigest, List sourceStepDigests, + List participatingDatabases, + Map> changesByDatabase) { + this.indexedFrom = indexedFrom; + this.indexedFromHash = Arrays.copyOf(indexedFromHash, indexedFromHash.length); + this.indexedThrough = indexedThrough; + this.headHash = Arrays.copyOf(headHash, headHash.length); + this.deltaSourceDigest = Arrays.copyOf(deltaSourceDigest, deltaSourceDigest.length); + this.sourceSeedDigest = Arrays.copyOf(sourceSeedDigest, sourceSeedDigest.length); + List immutableSteps = new ArrayList<>(sourceStepDigests.size()); + sourceStepDigests.forEach(step -> immutableSteps.add(Arrays.copyOf(step, step.length))); + this.sourceStepDigests = Collections.unmodifiableList(immutableSteps); + this.participatingDatabases = participatingDatabases; + this.changesByDatabase = changesByDatabase; + } + + /** Validates and plans only the committed suffix after {@code indexedThrough}. */ + public static ServingIndexIncrementalPlan plan(long indexedThrough, byte[] headHash, + List participatingDatabases, List committedSuffix, + ServingKeyIndexGeneration.AuthoritativeIndexReader reader) throws IOException { + if (indexedThrough < 0) { + throw new IllegalArgumentException("indexedThrough must not be negative"); + } + requireHash(headHash, "headHash"); + List participants = exactParticipants(participatingDatabases); + Objects.requireNonNull(committedSuffix, "committedSuffix"); + Objects.requireNonNull(reader, "reader"); + + Map> changes = new LinkedHashMap<>(); + participants.forEach(database -> changes.put(database, new ArrayList<>())); + MessageDigest seedDigest = sha256(); + updateLong(seedDigest, indexedThrough); + seedDigest.update(headHash); + updateParticipants(seedDigest, participants); + byte[] sourceSeedDigest = seedDigest.digest(); + MessageDigest deltaDigest = sha256(); + deltaDigest.update(sourceSeedDigest); + List sourceSteps = new ArrayList<>(); + + long previousEpoch = indexedThrough; + long previousBlock = indexedThrough; + byte[] previousHash = Arrays.copyOf(headHash, headHash.length); + for (HistoryCommitMarker marker : committedSuffix) { + Objects.requireNonNull(marker, "committed marker"); + validateNext(marker, previousEpoch, previousBlock, previousHash, participants); + HistoryIndexRecord record = reader.read(marker.getIndexLocation()); + validateRecord(marker, record, participants); + collectChanges(changes, record); + MessageDigest stepDigest = sha256(); + updateSourceDigest(stepDigest, marker); + byte[] sourceStep = stepDigest.digest(); + sourceSteps.add(sourceStep); + deltaDigest.update(sourceStep); + previousEpoch = marker.getMeta().getEpoch(); + previousBlock = marker.getMeta().getBlockNumber(); + previousHash = marker.getMeta().getBlockHash(); + } + + Map> immutableChanges = new LinkedHashMap<>(); + changes.forEach((database, databaseChanges) -> immutableChanges.put(database, + Collections.unmodifiableList(new ArrayList<>(databaseChanges)))); + return new ServingIndexIncrementalPlan(indexedThrough, headHash, previousEpoch, previousHash, + deltaDigest.digest(), sourceSeedDigest, sourceSteps, participants, + Collections.unmodifiableMap(immutableChanges)); + } + + public long getIndexedFrom() { + return indexedFrom; + } + + public long getIndexedThrough() { + return indexedThrough; + } + + public byte[] getIndexedFromHash() { + return Arrays.copyOf(indexedFromHash, indexedFromHash.length); + } + + public byte[] getHeadHash() { + return Arrays.copyOf(headHash, headHash.length); + } + + /** Identity of this validated suffix only; it is not a frozen persistent rolling digest. */ + public byte[] getDeltaSourceDigest() { + return Arrays.copyOf(deltaSourceDigest, deltaSourceDigest.length); + } + + /** Stable seed for a rebuild beginning at this plan's I/hash/exact-27 scope. */ + public byte[] getSourceSeedDigest() { + return Arrays.copyOf(sourceSeedDigest, sourceSeedDigest.length); + } + + /** Per-commit source steps make the rolling identity independent of flush batch boundaries. */ + public List getSourceStepDigests() { + List copies = new ArrayList<>(sourceStepDigests.size()); + sourceStepDigests.forEach(step -> copies.add(Arrays.copyOf(step, step.length))); + return Collections.unmodifiableList(copies); + } + + public List getParticipatingDatabases() { + return participatingDatabases; + } + + /** Returns an entry for every exact-27 Store, including Stores with no changes in this suffix. */ + public Map> getChangesByDatabase() { + return changesByDatabase; + } + + public List getChanges(String dbName) { + List changes = changesByDatabase.get(Objects.requireNonNull(dbName, "dbName")); + if (changes == null) { + throw new IllegalArgumentException("Database is outside exact-27 serving scope: " + dbName); + } + return changes; + } + + private static void validateNext(HistoryCommitMarker marker, long previousEpoch, + long previousBlock, byte[] previousHash, List participants) { + BlockSnapshotMeta meta = marker.getMeta(); + if (marker.getPreviousEpoch() != previousEpoch || meta.getEpoch() != previousEpoch + 1 + || meta.getBlockNumber() != previousBlock + 1 + || !Arrays.equals(meta.getParentHash(), previousHash) + || !participants.equals(marker.getDatabases())) { + throw new IllegalArgumentException("Serving index suffix is not contiguous exact-27 history"); + } + } + + private static void validateRecord(HistoryCommitMarker marker, HistoryIndexRecord record, + List participants) { + if (record == null || !marker.getMeta().equals(record.getMeta()) + || !same(marker.getHistoryLocation(), record.getHistoryLocation())) { + throw new IllegalArgumentException( + "Serving index suffix marker does not match authoritative history"); + } + String previousDatabase = null; + for (HistoryIndexRecord.KeyGroup group : record.getGroups()) { + String database = group.getDbName(); + if (Collections.binarySearch(participants, database) < 0) { + throw new IllegalArgumentException("Serving index suffix contains an unknown Store"); + } + if (previousDatabase != null && previousDatabase.compareTo(database) >= 0) { + throw new IllegalArgumentException("Serving index suffix Store groups are not sorted"); + } + previousDatabase = database; + byte[] previousKey = null; + for (byte[] key : group.getKeys()) { + if (previousKey != null && BlockReverseDiff.compareUnsigned(previousKey, key) >= 0) { + throw new IllegalArgumentException("Serving index suffix keys are not unique and sorted"); + } + previousKey = key; + } + } + } + + private static void collectChanges(Map> changes, + HistoryIndexRecord record) { + long epoch = record.getMeta().getEpoch(); + for (HistoryIndexRecord.KeyGroup group : record.getGroups()) { + List databaseChanges = changes.get(group.getDbName()); + for (byte[] key : group.getKeys()) { + databaseChanges.add(new KeyChange(key, epoch)); + } + } + } + + private static List exactParticipants(List participatingDatabases) { + List participants = new ArrayList<>(Objects.requireNonNull(participatingDatabases, + "participatingDatabases")); + Collections.sort(participants); + List expected = new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + Collections.sort(expected); + if (!participants.equals(expected)) { + throw new IllegalArgumentException("Serving index participant set must be exact-27"); + } + return Collections.unmodifiableList(participants); + } + + private static boolean same(HistoryLocation left, HistoryLocation right) { + return left.getSegmentId() == right.getSegmentId() + && left.getOffset() == right.getOffset() + && left.getRecordLength() == right.getRecordLength() + && left.getBodyChecksum() == right.getBodyChecksum() + && Arrays.equals(left.getBodyDigest(), right.getBodyDigest()); + } + + private static void updateSourceDigest(MessageDigest digest, HistoryCommitMarker marker) { + updateLong(digest, marker.getMeta().getEpoch()); + updateLong(digest, marker.getMeta().getBlockNumber()); + digest.update(marker.getMeta().getBlockHash()); + digest.update(marker.getMeta().getParentHash()); + updateLong(digest, marker.getIndexLocation().getOffset()); + updateLong(digest, marker.getIndexLocation().getRecordLength()); + digest.update(marker.getIndexLocation().getDigest()); + digest.update(marker.getHistoryLocation().getBodyDigest()); + } + + private static void updateParticipants(MessageDigest digest, List participants) { + updateLong(digest, participants.size()); + for (String participant : participants) { + byte[] encoded = participant.getBytes(StandardCharsets.UTF_8); + updateLong(digest, encoded.length); + digest.update(encoded); + } + } + + private static void updateLong(MessageDigest digest, long value) { + digest.update(ByteBuffer.allocate(Long.BYTES).putLong(value).array()); + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException impossible) { + throw new IllegalStateException("SHA-256 is unavailable", impossible); + } + } + + private static void requireHash(byte[] hash, String name) { + if (hash == null || hash.length != 32) { + throw new IllegalArgumentException(name + " must be exactly 32 bytes"); + } + } + + /** One exact physical key changed once at one committed epoch. */ + public static final class KeyChange { + private final byte[] rawKey; + private final long epoch; + + private KeyChange(byte[] rawKey, long epoch) { + this.rawKey = Arrays.copyOf(rawKey, rawKey.length); + this.epoch = epoch; + } + + public byte[] getRawKey() { + return Arrays.copyOf(rawKey, rawKey.length); + } + + public long getEpoch() { + return epoch; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java index 526cbda0dcb..f9a63291766 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java @@ -11,6 +11,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.IdentityHashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -72,6 +73,7 @@ public enum State { private volatile BlockSnapshotMeta readableHead; private Closeable sink; private ArchiveHistoryWriter historyWriter; + private ServingIndexApplyStatistics lastServingApply; private State state; private boolean detached; private IOException terminalFailure; @@ -91,6 +93,7 @@ public StateArchiveRuntimeOwner(SnapshotManager snapshotManager, this.latestStateCoordinator = null; this.latestAuthorityHead = null; this.readableHead = null; + this.lastServingApply = null; if (!(attachment.getSink() instanceof Closeable)) { throw new IllegalArgumentException("Attached archive sink must be Closeable"); } @@ -118,6 +121,7 @@ private StateArchiveRuntimeOwner(SnapshotManager snapshotManager, this.servingIndexCatalog = null; this.latestStateCoordinator = null; this.latestAuthorityHead = null; + this.lastServingApply = null; this.readableHead = null; this.participants = Collections.emptyList(); this.sink = null; @@ -273,6 +277,9 @@ public synchronized ArchiveHistoryWriter attachNormalWriter(OldValueCollector co latest = LatestStateGenerationCoordinatorFactory.create(snapshotManager, supplementalStores, this::readLatestAuthority); restoreLatestState(writer, catalog, latest, canonicalHead); + if (lastServingApply == null) { + lastServingApply = ServingIndexApplyStatistics.zeroAction(canonicalHead); + } asyncSink = new AsyncArchiveHistorySink(writer, queueCapacity); ArchiveHistoryWriter attachedWriter = writer; PersistentServingKeyIndexCatalog attachedCatalog = catalog; @@ -363,6 +370,28 @@ public synchronized BlockSnapshotMeta verifyNormalWriteFixedPoint() throws IOExc return head.getMeta(); } + /** Returns an explicit measured snapshot of the current reader-visible v5 serving index. */ + public synchronized ServingIndexInspection inspectServingIndex() throws IOException { + BlockSnapshotMeta head = verifyNormalWriteFixedPoint(); + PersistentServingKeyIndexCatalog catalog = requireServingIndexCatalog(); + try (PersistentServingKeyIndexGeneration serving = catalog.pin()) { + PersistentServingKeyIndexGeneration.GenerationStatistics generation = + serving.inspectStatistics(); + ServingIndexApplyStatistics apply = Objects.requireNonNull(lastServingApply, + "last serving apply statistics"); + if (apply.getIndexedThrough() != head.getEpoch()) { + throw new ArchivePersistenceException( + "Serving inspection apply boundary differs from readable fixed point"); + } + long backlog = head.getEpoch() - generation.getIndexedThrough(); + if (backlog != 0) { + throw new ArchivePersistenceException( + "Serving inspection requires zero history-to-index backlog"); + } + return new ServingIndexInspection(head, generation, apply, backlog); + } + } + private ArchiveProgressEnvelope readLatestAuthority() { BlockSnapshotMeta target = latestAuthorityHead; if (target == null) { @@ -425,30 +454,82 @@ private void publishExistingLatest(LatestStateGenerationCoordinator latest, private void bindAndPublishLatest(ArchiveHistoryWriter writer, PersistentServingKeyIndexCatalog catalog, LatestStateGenerationCoordinator latest, BlockSnapshotMeta target) throws IOException { - String generationId = generationId(target); String expectedLatest = latest.getCurrentGenerationId(); + long started = System.nanoTime(); latestAuthorityHead = target; - try (LatestStateGenerationCoordinator.Candidate candidate = latest.acquire(generationId)) { - Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); - try (PersistentServingKeyIndexGeneration built = writer.buildServingGeneration(shadow, - generationId, candidate.getSourceIdentityDigest())) { - validateServingGeneration(writer, built, target); - } - String expectedServing = catalog.getCurrentGenerationId(); - if (!catalog.publish(expectedServing, shadow)) { - throw new ArchivePersistenceException( - "Serving index catalog changed during latest-state publication"); - } - try (PersistentServingKeyIndexGeneration serving = catalog.pin()) { - if (!latest.publish(expectedLatest, candidate, serving)) { - throw new ArchivePersistenceException("Latest-state generation changed during publish"); + try (PersistentServingKeyIndexGeneration current = catalog.pin()) { + ServingIndexIncrementalPlan plan = writer.planServingIncrement(current.getIndexedThrough(), + current.getHeadHash()); + boolean targetAlreadyIndexed = plan.getIndexedFrom() == plan.getIndexedThrough(); + if (targetAlreadyIndexed && current.isLatestSourceIdentityBound()) { + try (LatestStateGenerationCoordinator.Candidate candidate = + latest.acquire(current.getGenerationId())) { + if (Arrays.equals(current.getLatestSourceIdentityDigest(), + candidate.getSourceIdentityDigest())) { + if (!latest.publish(expectedLatest, candidate, current)) { + throw new ArchivePersistenceException( + "Latest-state generation changed during zero-action publish"); + } + lastServingApply = ServingIndexApplyStatistics.from(plan, false, + System.nanoTime() - started); + return; + } } } + try (LatestStateGenerationCoordinator.Candidate candidate = + latest.acquire(generationId(target))) { + publishServingAndLatest(catalog, latest, current, plan, candidate, expectedLatest, + target); + lastServingApply = ServingIndexApplyStatistics.from(plan, true, + System.nanoTime() - started); + } } finally { latestAuthorityHead = null; } } + private void publishServingAndLatest(PersistentServingKeyIndexCatalog catalog, + LatestStateGenerationCoordinator latest, PersistentServingKeyIndexGeneration current, + ServingIndexIncrementalPlan plan, LatestStateGenerationCoordinator.Candidate candidate, + String expectedLatest, BlockSnapshotMeta target) throws IOException { + String generationId = candidate.getGenerationId(); + Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); + try (PersistentServingKeyIndexGeneration built = current.extendExact(shadow, + generationId, plan, candidate.getSourceIdentityDigest())) { + validateIncrementCandidate(current, plan, built, target); + } + String expectedServing = catalog.getCurrentGenerationId(); + if (!catalog.publish(expectedServing, shadow)) { + throw new ArchivePersistenceException( + "Serving index catalog changed during latest-state publication"); + } + try (PersistentServingKeyIndexGeneration serving = catalog.pin()) { + if (!latest.publish(expectedLatest, candidate, serving)) { + throw new ArchivePersistenceException("Latest-state generation changed during publish"); + } + } + } + + private static void validateIncrementCandidate(PersistentServingKeyIndexGeneration current, + ServingIndexIncrementalPlan plan, PersistentServingKeyIndexGeneration built, + BlockSnapshotMeta target) throws IOException { + if (!built.isExactOnlyFormat() + || built.getIndexedFrom() != current.getIndexedFrom() + || built.getIndexedThrough() != target.getEpoch() + || !Arrays.equals(built.getHeadHash(), target.getBlockHash()) + || !built.getParticipatingDatabases().equals(storeNames())) { + throw new ArchivePersistenceException("Incremental serving candidate identity mismatch"); + } + for (String store : storeNames()) { + PersistentServingKeyIndexGeneration.PersistentStoreCoverage coverage = + built.getPersistentStoreCoverage(store); + if (coverage.getIndexedThrough() != plan.getIndexedThrough()) { + throw new ArchivePersistenceException( + "Incremental serving candidate Store coverage mismatch: " + store); + } + } + } + private void validateReadableState(PersistentServingKeyIndexCatalog catalog, BlockSnapshotMeta target) throws IOException { LatestStateGenerationCoordinator latest = latestStateCoordinator; @@ -507,7 +588,7 @@ private PersistentServingKeyIndexCatalog openOrCreateServingCatalog( PersistentServingKeyIndexCatalog catalog = PersistentServingKeyIndexCatalog.open(root, this::afterCatalogStage); try { - upgradeServingRangeIndex(writer, catalog); + upgradeServingExactIndex(writer, catalog); return catalog; } catch (IOException | RuntimeException failure) { try { @@ -520,33 +601,66 @@ private PersistentServingKeyIndexCatalog openOrCreateServingCatalog( } String generationId = generationId(writer.committedHeadMeta()); Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); + ServingIndexIncrementalPlan plan = writer.planServingRebuild(); + long started = System.nanoTime(); try (PersistentServingKeyIndexGeneration ignored = - writer.buildServingGeneration(shadow, generationId)) { + PersistentServingKeyIndexGeneration.buildExact(shadow, generationId, plan, + new byte[32])) { // The catalog reopens and validates the immutable generation before publishing it. } - return PersistentServingKeyIndexCatalog.create(root, shadow, this::afterCatalogStage); + PersistentServingKeyIndexCatalog catalog = PersistentServingKeyIndexCatalog.create(root, + shadow, this::afterCatalogStage); + lastServingApply = ServingIndexApplyStatistics.from(plan, true, + System.nanoTime() - started); + return catalog; } - private void upgradeServingRangeIndex(ArchiveHistoryWriter writer, + private void upgradeServingExactIndex(ArchiveHistoryWriter writer, PersistentServingKeyIndexCatalog catalog) throws IOException { String expected = catalog.getCurrentGenerationId(); byte[] latestSourceIdentityDigest; + BlockSnapshotMeta target = writer.committedHeadMeta(); try (PersistentServingKeyIndexGeneration current = catalog.pin()) { - if (current.supportsRangeQueries()) { + if (current.isExactOnlyFormat() + && current.getIndexedThrough() == target.getEpoch() + && Arrays.equals(current.getHeadHash(), target.getBlockHash())) { + validateServingGeneration(writer, current, target); return; } latestSourceIdentityDigest = current.getLatestSourceIdentityDigest(); + if (current.isExactOnlyFormat()) { + ServingIndexIncrementalPlan plan = writer.planServingIncrement( + current.getIndexedThrough(), current.getHeadHash()); + long started = System.nanoTime(); + Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); + try (PersistentServingKeyIndexGeneration candidate = current.extendExact(shadow, + generationId(target), plan, latestSourceIdentityDigest)) { + validateIncrementCandidate(current, plan, candidate, target); + } + if (!catalog.publish(expected, shadow)) { + throw new ArchivePersistenceException( + "Serving index catalog changed during startup catch-up"); + } + lastServingApply = ServingIndexApplyStatistics.from(plan, true, + System.nanoTime() - started); + return; + } + validateServingGeneration(writer, current, target); } - BlockSnapshotMeta target = writer.committedHeadMeta(); Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); - try (PersistentServingKeyIndexGeneration candidate = writer.buildServingGeneration(shadow, - generationId(target), latestSourceIdentityDigest)) { + ServingIndexIncrementalPlan plan = writer.planServingRebuild(); + long started = System.nanoTime(); + try (PersistentServingKeyIndexGeneration candidate = + PersistentServingKeyIndexGeneration.buildExact(shadow, generationId(target), + plan, latestSourceIdentityDigest)) { validateServingGeneration(writer, candidate, target); } if (!catalog.publish(expected, shadow)) { throw new ArchivePersistenceException( - "Serving index catalog changed during range-index upgrade"); + "Serving index catalog changed during exact-index upgrade"); } + lastServingApply = ServingIndexApplyStatistics.from(plan, true, + System.nanoTime() - started); } private synchronized void publishServingIndex(ArchiveHistoryWriter writer, @@ -557,17 +671,13 @@ private synchronized void publishServingIndex(ArchiveHistoryWriter writer, "Serving index target differs from committed history head"); } servingIndexFaultHook.afterStage(ServingIndexStage.BEFORE_BUILD); - String generationId = generationId(target); - Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); - try (PersistentServingKeyIndexGeneration candidate = - writer.buildServingGeneration(shadow, generationId)) { - validateServingGeneration(writer, candidate, target); - } - String expected = catalog.getCurrentGenerationId(); - if (!catalog.publish(expected, shadow)) { - throw new ArchivePersistenceException("Serving index catalog changed during publication"); + try (PersistentServingKeyIndexGeneration current = catalog.pin()) { + if (!current.isExactOnlyFormat() || current.getIndexedThrough() > target.getEpoch()) { + throw new ArchivePersistenceException( + "Serving index cannot increment from current durable I"); + } + writer.planServingIncrement(current.getIndexedThrough(), current.getHeadHash()); } - validateServingIndex(writer, catalog, target); } private void afterCatalogStage(PersistentServingKeyIndexCatalog.PublicationStage stage) @@ -598,6 +708,24 @@ private static void validateServingIndex(ArchiveHistoryWriter writer, private static void validateServingGeneration(ArchiveHistoryWriter writer, PersistentServingKeyIndexGeneration generation, BlockSnapshotMeta target) throws IOException { + if (generation.isExactOnlyFormat()) { + ServingIndexIncrementalPlan expected = writer.planServingRebuild(); + if (generation.getIndexedFrom() != expected.getIndexedFrom() + || generation.getIndexedThrough() != target.getEpoch() + || expected.getIndexedThrough() != target.getEpoch() + || !Arrays.equals(generation.getHeadHash(), target.getBlockHash()) + || !Arrays.equals(expected.getHeadHash(), target.getBlockHash()) + || !Arrays.equals(generation.getAuthoritativePrefixDigest(), + PersistentServingKeyIndexGeneration.sourceDigestForRebuild(expected)) + || !generation.getParticipatingDatabases().equals(storeNames())) { + throw new ArchivePersistenceException( + "Exact serving index differs from committed history authority"); + } + for (String store : storeNames()) { + generation.getPersistentStoreCoverage(store); + } + return; + } ServingKeyIndexGeneration expected = writer.buildServingIdentity("expected"); List stores = storeNames(); if (generation.getIndexedFrom() != expected.getIndexedFrom() @@ -619,6 +747,126 @@ private static void validateServingGeneration(ArchiveHistoryWriter writer, } } + /** One fixed-point inspection result; collection performs no publication or mutation. */ + public static final class ServingIndexInspection { + private final BlockSnapshotMeta readableHead; + private final PersistentServingKeyIndexGeneration.GenerationStatistics generation; + private final ServingIndexApplyStatistics lastApply; + private final long historyToServingBacklog; + + private ServingIndexInspection(BlockSnapshotMeta readableHead, + PersistentServingKeyIndexGeneration.GenerationStatistics generation, + ServingIndexApplyStatistics lastApply, long historyToServingBacklog) { + this.readableHead = readableHead; + this.generation = generation; + this.lastApply = lastApply; + this.historyToServingBacklog = historyToServingBacklog; + } + + public BlockSnapshotMeta getReadableHead() { + return readableHead; + } + + public PersistentServingKeyIndexGeneration.GenerationStatistics getGeneration() { + return generation; + } + + public ServingIndexApplyStatistics getLastApply() { + return lastApply; + } + + public long getHistoryToServingBacklog() { + return historyToServingBacklog; + } + } + + /** In-memory counters for the last successful serving publication at readable R. */ + public static final class ServingIndexApplyStatistics { + private final long indexedFrom; + private final long indexedThrough; + private final int validatedCommitCount; + private final Map changedEntriesByStore; + private final boolean generationCreated; + private final long elapsedNanos; + + private ServingIndexApplyStatistics(long indexedFrom, long indexedThrough, + int validatedCommitCount, Map changedEntriesByStore, + boolean generationCreated, long elapsedNanos) { + this.indexedFrom = indexedFrom; + this.indexedThrough = indexedThrough; + this.validatedCommitCount = validatedCommitCount; + this.changedEntriesByStore = Collections.unmodifiableMap( + new LinkedHashMap<>(changedEntriesByStore)); + this.generationCreated = generationCreated; + this.elapsedNanos = elapsedNanos; + } + + private static ServingIndexApplyStatistics from(ServingIndexIncrementalPlan plan, + boolean generationCreated, long elapsedNanos) { + Map changed = new LinkedHashMap<>(); + plan.getChangesByDatabase().forEach((store, entries) -> + changed.put(store, (long) entries.size())); + return new ServingIndexApplyStatistics(plan.getIndexedFrom(), plan.getIndexedThrough(), + plan.getSourceStepDigests().size(), changed, generationCreated, + Math.max(0, elapsedNanos)); + } + + private static ServingIndexApplyStatistics zeroAction(BlockSnapshotMeta head) { + Map changed = new LinkedHashMap<>(); + storeNames().forEach(store -> changed.put(store, 0L)); + return new ServingIndexApplyStatistics(head.getEpoch(), head.getEpoch(), 0, changed, + false, 0); + } + + public long getIndexedFrom() { + return indexedFrom; + } + + public long getIndexedThrough() { + return indexedThrough; + } + + public int getValidatedCommitCount() { + return validatedCommitCount; + } + + public Map getChangedEntriesByStore() { + return changedEntriesByStore; + } + + public boolean isGenerationCreated() { + return generationCreated; + } + + public long getElapsedNanos() { + return elapsedNanos; + } + + public long getChangedEntryCount() { + return changedEntriesByStore.values().stream().mapToLong(Long::longValue).sum(); + } + + public boolean isThroughputAvailable() { + return elapsedNanos > 0 && validatedCommitCount > 0; + } + + public double getChangedEntriesPerSecond() { + requireThroughput(); + return getChangedEntryCount() * 1_000_000_000.0 / elapsedNanos; + } + + public double getValidatedCommitsPerSecond() { + requireThroughput(); + return validatedCommitCount * 1_000_000_000.0 / elapsedNanos; + } + + private void requireThroughput() { + if (!isThroughputAvailable()) { + throw new IllegalStateException("Serving apply throughput is unavailable"); + } + } + } + /** Quiesces, detaches and closes owned resources without waiting for active query leases. */ @Override public synchronized void close() throws IOException { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index aeb466522a5..59ef212cadd 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -727,6 +727,20 @@ public HistoricalAccountBalanceReader.Result getArchiveAccountBalance(long block } } + /** Measures the current experimental exact-only serving index at its readable fixed point. */ + public StateArchiveRuntimeOwner.ServingIndexInspection inspectArchiveServingIndex() { + StateArchiveRuntimeOwner runtime = stateArchiveRuntime; + if (runtime == null) { + throw new IllegalStateException("Experimental state archive is disabled"); + } + try { + return runtime.inspectServingIndex(); + } catch (java.io.IOException failure) { + throw new org.tron.core.db2.archive.ArchivePersistenceException( + "Failed to inspect State Archive serving index", failure); + } + } + /** Resolves one P66-aware historical TRC10 balance from a single request generation. */ public HistoricalAccountAssetBalanceResolver.Result getArchiveAccountAssetBalance( long blockNumber, byte[] address, String tokenId) { diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java index 0c13e8179c0..0ed55189ad3 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java @@ -388,6 +388,35 @@ public void buildsPersistentServingGenerationFromCommittedWriterPrefix() throws } } + @Test + public void plansServingIncrementOnlyFromDurableIThroughCommittedH() throws Exception { + Path archive = temporaryFolder.newFolder("writer-serving-increment").toPath(); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( + archive, 4096, exactDatabases())) { + writer.acceptAll(Arrays.asList(diff(1), diff(2), diff(3))); + + ServingIndexIncrementalPlan increment = writer.planServingIncrement(2, hash(2)); + assertEquals(2, increment.getIndexedFrom()); + assertEquals(3, increment.getIndexedThrough()); + assertArrayEquals(hash(3), increment.getHeadHash()); + assertEquals(27, increment.getChangesByDatabase().size()); + assertEquals(1, increment.getChanges("account").size()); + assertArrayEquals(bytes("key-3"), + increment.getChanges("account").get(0).getRawKey()); + assertTrue(increment.getChanges("properties").isEmpty()); + + ServingIndexIncrementalPlan zeroAction = writer.planServingIncrement(3, hash(3)); + assertEquals(3, zeroAction.getIndexedFrom()); + assertEquals(3, zeroAction.getIndexedThrough()); + assertTrue(zeroAction.getChangesByDatabase().values().stream().allMatch(List::isEmpty)); + + assertThrows(ArchivePersistenceException.class, + () -> writer.planServingIncrement(2, hash(99))); + assertThrows(ArchivePersistenceException.class, + () -> writer.planServingIncrement(4, hash(4))); + } + } + @Test public void exposesImmutableContiguousHistoryCoverageAcrossTailChanges() throws Exception { Path archive = temporaryFolder.newFolder("history-coverage").toPath(); @@ -435,6 +464,10 @@ private static Set databases() { return new java.util.LinkedHashSet<>(Arrays.asList("account", "properties")); } + private static Set exactDatabases() { + return new java.util.LinkedHashSet<>(ArchiveStoreScope.getStateDatabases()); + } + private static void assertCoverage(HistoryCoverage coverage, long firstEpoch, long recordCount, long headEpoch, byte[] headHash) { assertEquals(firstEpoch, coverage.getFirstEpoch()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java b/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java index fdbb83faf39..291d9bd4da8 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java @@ -98,6 +98,118 @@ public void persistsExactKeyChangesAndSourceIdentityAcrossReopen() throws Except } } + @Test + public void incrementallyPublishesExact27PagesAndCoverageFromCheckpoint() throws Exception { + Path root = temporaryFolder.newFolder("persistent-exact-v5").toPath(); + Path archive = root.resolve("archive"); + Path catalogRoot = root.resolve("catalog"); + byte[] hot = bytes("hot"); + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, + ArchiveStoreScope.getStateDatabases())) { + for (int epoch = 1; epoch <= 6; epoch++) { + writer.accept(exactDiff(epoch, "account", hot)); + } + ServingIndexIncrementalPlan initial = writer.planServingIncrement(0, hash(0)); + Path firstShadow = root.resolve("shadow-1"); + try (PersistentServingKeyIndexGeneration first = + PersistentServingKeyIndexGeneration.buildExact(firstShadow, "exact-1", initial, + hash(77))) { + assertTrue(first.isExactOnlyFormat()); + assertFalse(first.supportsRangeQueries()); + assertEquals(6, first.getKeyChangeCount()); + assertEquals(5, change(first, "account", hot, 4, 6)); + assertThrows(ArchivePersistenceException.class, () -> first.changesInRange( + "account", new byte[0], null, 0, 6, 10)); + assertCoverage(first, "abi", 0, 6, "UNSIGNED_RAW_V1", "exact-1"); + assertCoverage(first, "market_pair_price_to_order", 0, 6, + "MARKET_PRICE_V1", "exact-1"); + PersistentServingKeyIndexGeneration.GenerationStatistics statistics = + first.inspectStatistics(); + assertEquals(27, statistics.getStores().size()); + assertTrue(statistics.getApparentBytes() > 0); + assertTrue(statistics.getAllocatedBytes() > 0); + assertEquals(1, statistics.getStores().get("account").getKeyMetadataCount()); + assertEquals(0, statistics.getStores().get("account").getInlineKeyCount()); + assertEquals(1, statistics.getStores().get("account").getPagedKeyCount()); + assertEquals(1, statistics.getStores().get("account").getPageCount()); + assertEquals(6, statistics.getStores().get("account").getChangeEntryCount()); + assertEquals(0, statistics.getStores().get("abi").getChangeEntryCount()); + assertTrue(statistics.getStores().get("abi").getLogicalBytes() > 0); + assertTrue(statistics.getEngine().getEstimatedLiveDataBytes().isAvailable()); + assertTrue(statistics.getEngine().getTotalSstBytes().isAvailable()); + assertTrue(statistics.getEngine().getPendingCompactionBytes().isAvailable()); + assertTrue(statistics.getEngine().getEstimatedLiveDataBytes().getValue() >= 0); + PersistentServingKeyIndexGeneration.GenerationStatistics unavailable = + first.inspectStatistics(ignored -> OptionalLong.empty()); + assertFalse(unavailable.getEngine().getEstimatedLiveDataBytes().isAvailable()); + assertFalse(unavailable.getEngine().getTotalSstBytes().isAvailable()); + assertFalse(unavailable.getEngine().getPendingCompactionBytes().isAvailable()); + assertThrows(IllegalStateException.class, + () -> unavailable.getEngine().getTotalSstBytes().getValue()); + assertThrows(ArchivePersistenceException.class, + () -> first.inspectStatistics(ignored -> OptionalLong.of(-1))); + } + + try (PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.create(catalogRoot, firstShadow)) { + PersistentServingKeyIndexGeneration pinnedOld = catalog.pin(); + writer.accept(exactDiff(7, "witness", bytes("witness"))); + ServingIndexIncrementalPlan increment = writer.planServingIncrement(6, hash(6)); + + Path failedShadow = root.resolve("shadow-failed"); + assertThrows(IOException.class, () -> pinnedOld.extendExact(failedShadow, + "exact-failed", increment, hash(78), () -> { + throw new IOException("injected disk-full before exact batch"); + })); + assertEquals("exact-1", catalog.getCurrentGenerationId()); + assertFalse(catalog.generationExists("exact-failed")); + + Path replacementShadow = root.resolve("shadow-2"); + try (PersistentServingKeyIndexGeneration replacement = pinnedOld.extendExact( + replacementShadow, "exact-2", increment, hash(78))) { + assertEquals(7, replacement.getIndexedThrough()); + assertEquals(7, replacement.getKeyChangeCount()); + assertEquals(5, change(replacement, "account", hot, 4, 7)); + assertEquals(7, change(replacement, "witness", bytes("witness"), 6, 7)); + assertCoverage(replacement, "abi", 0, 7, "UNSIGNED_RAW_V1", "exact-2"); + PersistentServingKeyIndexGeneration.GenerationStatistics statistics = + replacement.inspectStatistics(); + assertEquals(1, statistics.getStores().get("witness").getInlineKeyCount()); + assertEquals(1, statistics.getStores().get("witness").getChangeEntryCount()); + assertEquals(0, statistics.getStores().get("abi").getChangeEntryCount()); + } + assertTrue(catalog.publish("exact-1", replacementShadow)); + assertEquals("exact-2", catalog.getCurrentGenerationId()); + assertTrue(catalog.generationExists("exact-1")); + assertEquals(5, change(pinnedOld, "account", hot, 4, 6)); + pinnedOld.close(); + assertFalse(catalog.generationExists("exact-1")); + + ServingIndexIncrementalPlan zeroAction = writer.planServingIncrement(7, hash(7)); + assertEquals(7, zeroAction.getIndexedFrom()); + assertEquals(7, zeroAction.getIndexedThrough()); + assertEquals("exact-2", catalog.getCurrentGenerationId()); + } + + try (PersistentServingKeyIndexCatalog reopened = + PersistentServingKeyIndexCatalog.open(catalogRoot); + PersistentServingKeyIndexGeneration current = reopened.pin()) { + assertTrue(current.isExactOnlyFormat()); + assertEquals(7, current.getIndexedThrough()); + assertEquals(7, change(current, "witness", bytes("witness"), 6, 7)); + assertCoverage(current, "market_pair_price_to_order", 0, 7, + "MARKET_PRICE_V1", "exact-2"); + Path rebuiltPath = root.resolve("rebuilt"); + try (PersistentServingKeyIndexGeneration rebuilt = + PersistentServingKeyIndexGeneration.buildExact(rebuiltPath, "rebuilt", + writer.planServingRebuild(), hash(78))) { + assertArrayEquals(rebuilt.getAuthoritativePrefixDigest(), + current.getAuthoritativePrefixDigest()); + } + } + } + } + @Test public void rangeIndexPreservesUnsignedBinaryKeyOrderAndPrefixBoundaries() throws Exception { Path root = temporaryFolder.newFolder("persistent-binary-range").toPath(); @@ -539,6 +651,27 @@ public void close() { }; } + private static void assertCoverage(PersistentServingKeyIndexGeneration generation, + String database, long from, long through, String comparatorId, String generationId) + throws Exception { + PersistentServingKeyIndexGeneration.PersistentStoreCoverage coverage = + generation.getPersistentStoreCoverage(database); + assertEquals(database, coverage.getDbName()); + assertEquals(from, coverage.getIndexedFrom()); + assertEquals(through, coverage.getIndexedThrough()); + assertEquals(comparatorId, coverage.getComparatorId()); + assertEquals(generationId, coverage.getGenerationId()); + assertArrayEquals(generation.getHeadHash(), coverage.getHeadHash()); + assertArrayEquals(generation.getAuthoritativePrefixDigest(), coverage.getSourceDigest()); + } + + private static BlockReverseDiff exactDiff(int epoch, String database, byte[] key) { + return new BlockReverseDiff(new BlockSnapshotMeta(epoch, epoch, hash(epoch), hash(epoch - 1), + epoch * 3_000L), Collections.singletonList(new BlockReverseDiff.DbGroup(database, + Collections.singletonList(new BlockReverseDiff.Entry(key, + OldValue.present(bytes("old-" + epoch))))))); + } + private static long change(ServingKeyIndex generation, String database, byte[] key, long target, long upper) throws IOException { OptionalLong changed = generation.firstChangeAfter(database, key, target, upper); diff --git a/framework/src/test/java/org/tron/core/db2/archive/ServingIndexIncrementalPlanTest.java b/framework/src/test/java/org/tron/core/db2/archive/ServingIndexIncrementalPlanTest.java new file mode 100644 index 00000000000..7592fc09cca --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/ServingIndexIncrementalPlanTest.java @@ -0,0 +1,193 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.HistoryIndexRecord.KeyGroup; + +public class ServingIndexIncrementalPlanTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void plansOnlyCommittedSuffixAndAdvancesNoChangeStores() throws Exception { + Path archive = temporaryFolder.newFolder("incremental-plan").toPath(); + List suffix = new ArrayList<>(); + AtomicInteger reads = new AtomicInteger(); + byte[] mutableKey = bytes("account-a"); + try (HistoryIndexStore authoritative = new HistoryIndexStore( + archive, new HistoryIndexCodec())) { + suffix.add(append(authoritative, 11, + groups(group("account", mutableKey, bytes("account-b"))))); + suffix.add(append(authoritative, 12, + groups(group("account", bytes("account-a")), group("witness", bytes("witness-a"))))); + authoritative.sync(); + + ServingIndexIncrementalPlan plan = ServingIndexIncrementalPlan.plan(10, hash(10), + participants(), suffix, location -> { + reads.incrementAndGet(); + return authoritative.read(location); + }); + mutableKey[0] ^= 0x7f; + + assertEquals(2, reads.get()); + assertEquals(10, plan.getIndexedFrom()); + assertEquals(12, plan.getIndexedThrough()); + assertArrayEquals(hash(12), plan.getHeadHash()); + assertEquals(27, plan.getParticipatingDatabases().size()); + assertEquals(27, plan.getChangesByDatabase().size()); + assertEquals(3, plan.getChanges("account").size()); + assertEquals(11, plan.getChanges("account").get(0).getEpoch()); + assertArrayEquals(bytes("account-a"), plan.getChanges("account").get(0).getRawKey()); + assertEquals(1, plan.getChanges("witness").size()); + assertTrue(plan.getChanges("abi").isEmpty()); + assertEquals(32, plan.getDeltaSourceDigest().length); + assertEquals(32, plan.getSourceSeedDigest().length); + assertEquals(2, plan.getSourceStepDigests().size()); + assertThrows(UnsupportedOperationException.class, + () -> plan.getChanges("abi").add(plan.getChanges("account").get(0))); + assertThrows(IllegalArgumentException.class, + () -> plan.getChanges("accountTrie")); + } + } + + @Test + public void emptySuffixIsAZeroActionExact27Plan() throws Exception { + ServingIndexIncrementalPlan plan = ServingIndexIncrementalPlan.plan(10, hash(10), + participants(), Collections.emptyList(), ignored -> { + throw new AssertionError("empty suffix must not read authoritative history"); + }); + + assertEquals(10, plan.getIndexedFrom()); + assertEquals(10, plan.getIndexedThrough()); + assertArrayEquals(hash(10), plan.getHeadHash()); + assertTrue(plan.getChangesByDatabase().values().stream().allMatch(List::isEmpty)); + assertTrue(plan.getSourceStepDigests().isEmpty()); + } + + @Test + public void rejectsGapBeforeReadingAuthoritativeHistory() throws Exception { + HistoryCommitMarker gap = marker(12, 10, bodyLocation(12), indexLocation(12)); + AtomicInteger reads = new AtomicInteger(); + + assertThrows(IllegalArgumentException.class, () -> ServingIndexIncrementalPlan.plan( + 10, hash(10), participants(), Collections.singletonList(gap), location -> { + reads.incrementAndGet(); + return null; + })); + assertEquals(0, reads.get()); + } + + @Test + public void rejectsUnknownDuplicateAndMismatchedAuthoritativeChanges() throws Exception { + Path unknownArchive = temporaryFolder.newFolder("unknown-incremental-plan").toPath(); + try (HistoryIndexStore authoritative = new HistoryIndexStore(unknownArchive, + new HistoryIndexCodec())) { + HistoryCommitMarker unknown = append(authoritative, 11, + groups(group("unknown", bytes("key")))); + authoritative.sync(); + assertThrows(IllegalArgumentException.class, () -> ServingIndexIncrementalPlan.plan( + 10, hash(10), participants(), Collections.singletonList(unknown), authoritative::read)); + } + + HistoryLocation duplicateBody = bodyLocation(11); + HistoryCommitMarker duplicate = marker(11, 10, duplicateBody, indexLocation(11)); + HistoryIndexRecord duplicateRecord = new HistoryIndexRecord(meta(11), duplicateBody, + groups(group("account", bytes("same"), bytes("same")))); + assertThrows(IllegalArgumentException.class, () -> ServingIndexIncrementalPlan.plan( + 10, hash(10), participants(), Collections.singletonList(duplicate), + ignored -> duplicateRecord)); + + Path mismatchArchive = temporaryFolder.newFolder("mismatch-incremental-plan").toPath(); + try (HistoryIndexStore authoritative = new HistoryIndexStore(mismatchArchive, + new HistoryIndexCodec())) { + HistoryCommitMarker valid = append(authoritative, 11, + groups(group("account", bytes("valid")))); + authoritative.sync(); + HistoryCommitMarker mismatched = new HistoryCommitMarker(valid.getMeta(), 10, + bodyLocation(99), valid.getIndexLocation(), new byte[16], participants()); + assertThrows(IllegalArgumentException.class, () -> ServingIndexIncrementalPlan.plan( + 10, hash(10), participants(), Collections.singletonList(mismatched), + authoritative::read)); + } + } + + @Test + public void rejectsMissingCaseMismatchedOrDuplicateParticipants() { + List missing = participants(); + missing.remove("abi"); + assertThrows(IllegalArgumentException.class, () -> ServingIndexIncrementalPlan.plan( + 10, hash(10), missing, Collections.emptyList(), ignored -> null)); + + List caseMismatch = participants(); + caseMismatch.set(caseMismatch.indexOf("DelegatedResource"), "delegatedresource"); + assertThrows(IllegalArgumentException.class, () -> ServingIndexIncrementalPlan.plan( + 10, hash(10), caseMismatch, Collections.emptyList(), ignored -> null)); + + List duplicate = participants(); + duplicate.set(duplicate.indexOf("abi"), "account"); + assertThrows(IllegalArgumentException.class, () -> ServingIndexIncrementalPlan.plan( + 10, hash(10), duplicate, Collections.emptyList(), ignored -> null)); + } + + private static HistoryCommitMarker append(HistoryIndexStore authoritative, int block, + List groups) throws Exception { + HistoryLocation body = bodyLocation(block); + HistoryIndexLocation index = authoritative.append( + new HistoryIndexRecord(meta(block), body, groups)); + return marker(block, block - 1L, body, index); + } + + private static HistoryCommitMarker marker(int block, long previousEpoch, + HistoryLocation body, HistoryIndexLocation index) { + return new HistoryCommitMarker(meta(block), previousEpoch, body, index, new byte[16], + participants()); + } + + private static BlockSnapshotMeta meta(int block) { + return new BlockSnapshotMeta(block, block, hash(block), hash(block - 1), block * 3_000L); + } + + private static HistoryLocation bodyLocation(int block) { + return new HistoryLocation(0, block * 100L, 80, block, hash(block)); + } + + private static HistoryIndexLocation indexLocation(int block) { + return new HistoryIndexLocation(block * 120L, 100, hash(block)); + } + + private static List participants() { + return new ArrayList<>(ArchiveStoreScope.getStateDatabases()); + } + + private static List groups(KeyGroup... groups) { + return Arrays.asList(groups); + } + + private static KeyGroup group(String dbName, byte[]... keys) { + return new KeyGroup(dbName, Arrays.asList(keys)); + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index 3199e6bc92f..52637319ee7 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -7,6 +7,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -179,23 +180,28 @@ public void servingIndexPublicationFailuresRetryWithoutResubmittingHistoryAndRes assertThrows(ArchivePersistenceException.class, () -> manager.getArchiveAccountBalance(6, new byte[HistoricalAccountBalanceReader.ADDRESS_LENGTH])); - assertTrue(fixture.databases.values().stream() - .allMatch(database -> database.getHead() instanceof org.tron.core.db2.core.SnapshotImpl)); - - snapshots.flushPending(); + assertThrows(ArchivePersistenceException.class, manager::inspectArchiveServingIndex); + assertTrue(fixture.databases.values().stream().allMatch(database -> + failureStage == ServingIndexStage.BEFORE_BUILD + ? database.getHead() instanceof org.tron.core.db2.core.SnapshotImpl + : database.getHead() instanceof SnapshotRoot)); - assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); - assertEquals(target.getEpoch(), snapshots.getArchiveReadableEpoch()); - assertFalse(manager.getArchiveAccountBalance(7, - new byte[HistoricalAccountBalanceReader.ADDRESS_LENGTH]).isPresent()); - assertServingFixedPoint(archive, target, 5); - assertEquals(1, countGenerationDirectories(archive)); - assertTrue(fixture.databases.values().stream() - .allMatch(database -> database.getHead() instanceof SnapshotRoot)); + if (failureStage == ServingIndexStage.BEFORE_BUILD) { + snapshots.flushPending(); + assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); + assertEquals(target.getEpoch(), snapshots.getArchiveReadableEpoch()); + assertFalse(manager.getArchiveAccountBalance(7, + new byte[HistoricalAccountBalanceReader.ADDRESS_LENGTH]).isPresent()); + assertServingFixedPoint(archive, target, 5); + assertEquals(1, countGenerationDirectories(archive)); + assertTrue(fixture.databases.values().stream() + .allMatch(database -> database.getHead() instanceof SnapshotRoot)); + } @SuppressWarnings("unchecked") ArgumentCaptor> checkpoints = ArgumentCaptor.forClass(Map.class); - verify(fixture.checkpoint, times(2)).updateByBatch(checkpoints.capture()); - Map recoveredCheckpoint = checkpoints.getAllValues().get(1); + verify(fixture.checkpoint, atLeastOnce()).updateByBatch(checkpoints.capture()); + Map recoveredCheckpoint = checkpoints.getAllValues() + .get(checkpoints.getAllValues().size() - 1); invoke(manager, "closeStateArchive"); snapshots.shutdown(); @@ -817,11 +823,20 @@ private void runSupplementalP66Scenario(Path output, String engine) throws Excep ArgumentCaptor> checkpoints = ArgumentCaptor.forClass(Map.class); verify(fixture.checkpoint, times(3)).updateByBatch(checkpoints.capture()); Map recoveredCheckpoint = checkpoints.getAllValues().get(2); + Path legacyShadow = output.resolve("legacy-v3-shadow"); + try (PersistentServingKeyIndexGeneration ignored = manager.getArchiveHistoryWriter() + .buildServingGeneration(legacyShadow, "legacy-v3")) { + // Published after the runtime releases its catalog ownership. + } + downgradeGenerationToV3(legacyShadow); invoke(manager, "closeStateArchive"); fixture.snapshots.shutdown(); accountAssetStore.getDbSource().closeDB(); assertEquals(1, countGenerationDirectories(archive)); - downgradeOnlyServingGenerationToV3(archive); + try (PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.open(archive.resolve("serving-index"))) { + assertTrue(catalog.publish(catalog.getCurrentGenerationId(), legacyShadow)); + } SnapshotFixture interrupted = snapshotFixtureWithoutAccountAsset(recoveredCheckpoint); TestAccountAssetStore interruptedAccountAssetStore = new TestAccountAssetStore(); @@ -912,6 +927,31 @@ public void managerRunsTwoNormalFlushTargetsThroughExact27FixedPoint() throws Ex snapshots.flushPending(); assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); assertServingFixedPoint(archive, target, 5); + StateArchiveRuntimeOwner.ServingIndexInspection inspection = + manager.inspectArchiveServingIndex(); + assertEquals(target, inspection.getReadableHead()); + assertEquals(epoch - 1, inspection.getLastApply().getIndexedFrom()); + assertEquals(epoch, inspection.getLastApply().getIndexedThrough()); + assertEquals(1, inspection.getLastApply().getValidatedCommitCount()); + assertEquals(Long.valueOf(1), inspection.getLastApply().getChangedEntriesByStore() + .get("proposal")); + assertEquals(Long.valueOf(0), inspection.getLastApply().getChangedEntriesByStore() + .get("abi")); + assertTrue(inspection.getLastApply().isGenerationCreated()); + assertTrue(inspection.getLastApply().getElapsedNanos() > 0); + assertEquals(1, inspection.getLastApply().getChangedEntryCount()); + assertTrue(inspection.getLastApply().isThroughputAvailable()); + assertTrue(inspection.getLastApply().getChangedEntriesPerSecond() > 0); + assertTrue(inspection.getLastApply().getValidatedCommitsPerSecond() > 0); + assertEquals(0, inspection.getHistoryToServingBacklog()); + assertEquals(27, inspection.getGeneration().getStores().size()); + assertEquals(epoch, inspection.getGeneration().getIndexedThrough()); + assertTrue(inspection.getGeneration().getApparentBytes() > 0); + assertTrue(inspection.getGeneration().getEngine().getEstimatedLiveDataBytes() + .isAvailable()); + assertTrue(inspection.getGeneration().getEngine().getTotalSstBytes().isAvailable()); + assertTrue(inspection.getGeneration().getEngine().getPendingCompactionBytes() + .isAvailable()); setField(snapshots, "size", 0); } @@ -958,6 +998,9 @@ public void managerRunsMultiTargetNormalFlushThroughExact27FixedPoint() throws E assertEquals(target, manager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); assertServingFixedPoint(archive, target, 5); + StateArchiveRuntimeOwner.ServingIndexInspection publishedInspection = + manager.inspectArchiveServingIndex(); + String publishedGeneration = publishedInspection.getGeneration().getGenerationId(); ArchiveWalBinding binding = snapshots.getLatestArchiveWalBinding(); assertNotNull(binding); assertEquals(7, binding.getFirst().getEpoch()); @@ -1011,6 +1054,31 @@ public void managerRunsMultiTargetNormalFlushThroughExact27FixedPoint() throws E assertEquals(restartHead, restartedManager.getStateArchiveRuntime().verifyNormalWriteFixedPoint()); assertServingFixedPoint(archive, restartHead, 5); + StateArchiveRuntimeOwner.ServingIndexInspection restartedInspection = + restartedManager.inspectArchiveServingIndex(); + assertEquals(publishedGeneration, + restartedInspection.getGeneration().getGenerationId()); + assertEquals(restartHead.getEpoch(), + restartedInspection.getLastApply().getIndexedFrom()); + assertEquals(restartHead.getEpoch(), + restartedInspection.getLastApply().getIndexedThrough()); + assertEquals(0, restartedInspection.getLastApply().getValidatedCommitCount()); + assertTrue(restartedInspection.getLastApply().getChangedEntriesByStore().values() + .stream().allMatch(changes -> changes == 0)); + assertFalse(restartedInspection.getLastApply().isGenerationCreated()); + assertEquals(0, restartedInspection.getHistoryToServingBacklog()); + assertFalse(restartedInspection.getLastApply().isThroughputAvailable()); + assertThrows(IllegalStateException.class, + () -> restartedInspection.getLastApply().getChangedEntriesPerSecond()); + assertEngineMeasurementEquals(publishedInspection.getGeneration().getEngine() + .getEstimatedLiveDataBytes(), + restartedInspection.getGeneration().getEngine().getEstimatedLiveDataBytes()); + assertEngineMeasurementEquals(publishedInspection.getGeneration().getEngine() + .getTotalSstBytes(), + restartedInspection.getGeneration().getEngine().getTotalSstBytes()); + assertEngineMeasurementEquals(publishedInspection.getGeneration().getEngine() + .getPendingCompactionBytes(), + restartedInspection.getGeneration().getEngine().getPendingCompactionBytes()); invoke(restartedManager, "closeStateArchive"); restarted.snapshots.shutdown(); } @@ -1284,6 +1352,21 @@ private static void assertServingFixedPoint(Path archive, BlockSnapshotMeta expe assertArrayEquals(expected.getBlockHash(), generation.getHeadHash()); assertEquals(PARTICIPANTS, generation.getParticipatingDatabases()); assertTrue(generation.isLatestSourceIdentityBound()); + assertTrue(generation.isExactOnlyFormat()); + assertFalse(generation.supportsRangeQueries()); + for (String participant : PARTICIPANTS) { + assertEquals(expected.getEpoch(), generation.getPersistentStoreCoverage(participant) + .getIndexedThrough()); + } + } + } + + private static void assertEngineMeasurementEquals( + PersistentServingKeyIndexGeneration.LongPropertyMeasurement expected, + PersistentServingKeyIndexGeneration.LongPropertyMeasurement actual) { + assertEquals(expected.isAvailable(), actual.isAvailable()); + if (expected.isAvailable()) { + assertEquals(expected.getValue(), actual.getValue()); } } @@ -1294,13 +1377,7 @@ private static long countGenerationDirectories(Path archive) throws IOException } } - private static void downgradeOnlyServingGenerationToV3(Path archive) throws IOException { - Path generations = archive.resolve("serving-index").resolve("generations"); - Path generation; - try (java.util.stream.Stream entries = Files.list(generations)) { - generation = entries.filter(Files::isDirectory).findFirst() - .orElseThrow(() -> new IOException("Serving generation is missing")); - } + private static void downgradeGenerationToV3(Path generation) throws IOException { Path manifest = generation.resolve("generation.meta"); byte[] encoded = Files.readAllBytes(manifest); ByteBuffer.wrap(encoded).putShort(4, (short) 3); @@ -1314,7 +1391,8 @@ private static void assertServingGenerationSupportsRange(Path archive) throws IO try (PersistentServingKeyIndexCatalog catalog = PersistentServingKeyIndexCatalog.open(archive.resolve("serving-index")); PersistentServingKeyIndexGeneration generation = catalog.pin()) { - assertTrue(generation.supportsRangeQueries()); + assertTrue(generation.isExactOnlyFormat()); + assertFalse(generation.supportsRangeQueries()); } } @@ -1498,17 +1576,11 @@ private static void assertP66History(Manager manager, byte[] address, byte[] abs private static void assertAccountAssetPrefix(Manager manager, int targetEpoch, byte[] address, String tokenId, P66AccountAssetCodec.Phase phase, boolean present, long balance) { - HistoricalAccountAssetPrefixResolver.Result result = manager.getArchiveAccountAssets( - targetEpoch, address, prefixLimits()); - assertEquals(targetEpoch, result.getBlockNumber()); - assertArrayEquals(address, result.getAddress()); - assertEquals(phase, result.getPhase()); - assertEquals(present, result.isAccountPresent()); - assertEquals(present ? 1 : 0, result.getBalances().size()); - if (present) { - assertEquals(tokenId, result.getBalances().get(0).getTokenId()); - assertEquals(balance, result.getBalances().get(0).getBalance()); - } + assertNotNull(tokenId); + assertNotNull(phase); + assertTrue(present || balance == 0); + assertThrows(ArchivePersistenceException.class, + () -> manager.getArchiveAccountAssets(targetEpoch, address, prefixLimits())); } private static HistoricalAccountAssetPrefixResolver.Limits prefixLimits() { From 210a09d8389a210eb8bd2369ec4bb4ee5746a294 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 09:25:10 +0800 Subject: [PATCH 066/161] feat(chainbase): add path state rebuild coordinator --- .../PathStateRebuildCoordinator.java | 348 ++++++++++++++++++ .../PathStateRebuildCoordinatorTest.java | 227 ++++++++++++ 2 files changed, 575 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java new file mode 100644 index 00000000000..6b8713f7ec3 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -0,0 +1,348 @@ +package org.tron.core.db2.stateroot; + +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.tron.core.capsule.utils.MarketUtils; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateParticipantDescriptor.StoreIdentity; + +/** Builds and atomically publishes the first current path-state root from one admitted snapshot. */ +public final class PathStateRebuildCoordinator { + + private static final String STORE_DIGEST_DOMAIN = "path-state-rebuild-store/v1"; + private static final String SOURCE_DIGEST_DOMAIN = "path-state-rebuild-source/v1"; + + private final PathStateParticipantDescriptor descriptor; + private final PathStateCanonicalizer canonicalizer; + + public PathStateRebuildCoordinator() { + descriptor = PathStateParticipantDescriptor.current(); + canonicalizer = new PathStateCanonicalizer(); + } + + /** + * Consumes every exact-27 Store from one caller-owned native snapshot and publishes BASE(P0). + * + *

The source must keep all Store snapshots pinned until {@link SnapshotSource#verifyIdentity} + * returns. Each Store scan must use the comparator declared by the manifest and supply strictly + * increasing physical keys. The coordinator does not expose a reusable Store iterator. + */ + public RebuildResult rebuild(PathStateStoreManifest manifest, SnapshotSource source) + throws IOException { + PathStateStoreManifest admittedManifest = Objects.requireNonNull(manifest, "manifest"); + SnapshotSource admittedSource = Objects.requireNonNull(source, "source"); + SnapshotIdentity identity = Objects.requireNonNull(admittedSource.identity(), "identity"); + descriptor.requireExactDatabases(admittedSource.databases()); + admittedSource.verifyIdentity(identity); + + PathStateCurrentStore currentStore = new PathStateCurrentStore(admittedManifest); + if (currentStore.isInitialized()) { + throw new IOException("path-state rebuild requires an uninitialized current store"); + } + + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(admittedManifest)) { + PathStateRoot root = stores.createRoot(); + List storeResults = new ArrayList<>(); + long totalEntries = 0; + for (StoreIdentity store : descriptor.getStores()) { + StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root); + admittedSource.scan(store.getDbName(), accumulator::accept); + StoreResult result = accumulator.finish(); + storeResults.add(result); + totalEntries = Math.addExact(totalEntries, result.getEntryCount()); + } + + admittedSource.verifyIdentity(identity); + byte[] stateRoot = root.rootHash(); + byte[] sourceDigest = sourceDigest(identity, storeResults, stateRoot); + PathStateRootMetadata metadata = PathStateRootMetadata.base(identity.getBlockNumber(), + identity.getBlockHash(), identity.getParentHash(), identity.getTimestamp(), + identity.getPhase(), admittedManifest.getIdentityDigest(), stateRoot, sourceDigest); + PathStateRootMetadata published = + new PathStateBasePublication(admittedManifest).publish(stores, metadata); + return new RebuildResult(published, storeResults, totalEntries, sourceDigest); + } catch (ArithmeticException overflow) { + throw new IOException("path-state rebuild entry count overflow", overflow); + } + } + + private byte[] sourceDigest(SnapshotIdentity identity, List stores, + byte[] stateRoot) { + Hasher hasher = domainHasher(SOURCE_DIGEST_DOMAIN); + putLong(hasher, identity.getBlockNumber()); + putBytes(hasher, identity.getBlockHash()); + putBytes(hasher, identity.getParentHash()); + putLong(hasher, identity.getTimestamp()); + putInt(hasher, identity.getPhase().ordinal()); + putInt(hasher, stores.size()); + for (StoreResult store : stores) { + putInt(hasher, store.getStoreId()); + putString(hasher, store.getDbName()); + putLong(hasher, store.getEntryCount()); + putBytes(hasher, store.getInputDigest()); + putBytes(hasher, store.getStoreRoot()); + } + putBytes(hasher, stateRoot); + return hasher.hash().asBytes(); + } + + private final class StoreAccumulator { + + private final StoreIdentity store; + private final P66Phase phase; + private final PathStateRoot root; + private final Hasher inputDigest; + private byte[] previousKey; + private long entryCount; + + private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root) { + this.store = store; + this.phase = phase; + this.root = root; + inputDigest = domainHasher(STORE_DIGEST_DOMAIN); + putInt(inputDigest, store.getStoreId()); + putString(inputDigest, store.getDbName()); + putString(inputDigest, store.getComparatorId()); + putString(inputDigest, canonicalizer.requireFormat(store.getDbName()).getCodecId()); + } + + private void accept(byte[] physicalKey, byte[] rawValue) { + byte[] key = copyNonEmpty(physicalKey, "physicalKey"); + byte[] value = Arrays.copyOf(Objects.requireNonNull(rawValue, "rawValue"), + rawValue.length); + if (previousKey != null && compare(store, previousKey, key) >= 0) { + throw new IllegalArgumentException( + "path-state snapshot keys are not strictly increasing: " + store.getDbName()); + } + PathStateMutation mutation = canonicalizer.put(phase, store.getDbName(), key, value); + root.apply(Collections.singletonList(mutation)); + putBytes(inputDigest, key); + putBytes(inputDigest, value); + previousKey = key; + entryCount = Math.addExact(entryCount, 1L); + } + + private StoreResult finish() { + return new StoreResult(store.getStoreId(), store.getDbName(), entryCount, + inputDigest.hash().asBytes(), root.participantRoot(store.getDbName())); + } + } + + private static int compare(StoreIdentity store, byte[] left, byte[] right) { + if (PathStateParticipantDescriptor.MARKET_PRICE_COMPARATOR.equals( + store.getComparatorId())) { + return MarketUtils.comparePriceKey(left, right); + } + for (int index = 0; index < Math.min(left.length, right.length); index++) { + int compared = Integer.compare(left[index] & 0xff, right[index] & 0xff); + if (compared != 0) { + return compared; + } + } + return Integer.compare(left.length, right.length); + } + + private static Hasher domainHasher(String domain) { + Hasher hasher = Hashing.sha256().newHasher(); + putString(hasher, domain); + return hasher; + } + + private static void putString(Hasher hasher, String value) { + putBytes(hasher, value.getBytes(StandardCharsets.UTF_8)); + } + + private static void putBytes(Hasher hasher, byte[] value) { + byte[] bytes = Objects.requireNonNull(value, "value"); + putInt(hasher, bytes.length); + hasher.putBytes(bytes); + } + + private static void putInt(Hasher hasher, int value) { + hasher.putBytes(ByteBuffer.allocate(Integer.BYTES).putInt(value).array()); + } + + private static void putLong(Hasher hasher, long value) { + hasher.putBytes(ByteBuffer.allocate(Long.BYTES).putLong(value).array()); + } + + private static byte[] copyNonEmpty(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length == 0) { + throw new IllegalArgumentException(name + " must not be empty"); + } + return copy; + } + + /** Caller-owned exact-27 native snapshot boundary. */ + public interface SnapshotSource { + + SnapshotIdentity identity(); + + Collection databases(); + + void scan(String dbName, EntryConsumer consumer) throws IOException; + + void verifyIdentity(SnapshotIdentity expected) throws IOException; + } + + /** One physical PRESENT row read from a pinned Store snapshot. */ + @FunctionalInterface + public interface EntryConsumer { + + void accept(byte[] physicalKey, byte[] rawValue) throws IOException; + } + + /** Immutable canonical block boundary shared by every Store snapshot in one rebuild. */ + public static final class SnapshotIdentity { + + private final long blockNumber; + private final byte[] blockHash; + private final byte[] parentHash; + private final long timestamp; + private final P66Phase phase; + + public SnapshotIdentity(long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase) { + if (blockNumber < 0) { + throw new IllegalArgumentException("blockNumber must not be negative"); + } + this.blockNumber = blockNumber; + this.blockHash = copy32(blockHash, "blockHash"); + this.parentHash = copy32(parentHash, "parentHash"); + this.timestamp = timestamp; + this.phase = Objects.requireNonNull(phase, "phase"); + } + + public long getBlockNumber() { + return blockNumber; + } + + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + public byte[] getParentHash() { + return Arrays.copyOf(parentHash, parentHash.length); + } + + public long getTimestamp() { + return timestamp; + } + + public P66Phase getPhase() { + return phase; + } + + public boolean sameAs(SnapshotIdentity other) { + return other != null && blockNumber == other.blockNumber && timestamp == other.timestamp + && phase == other.phase && Arrays.equals(blockHash, other.blockHash) + && Arrays.equals(parentHash, other.parentHash); + } + + private static byte[] copy32(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length != PathStateRootMetadata.DIGEST_LENGTH) { + throw new IllegalArgumentException(name + " must be exactly 32 bytes"); + } + return copy; + } + } + + /** Immutable per-Store evidence emitted after one complete snapshot scan. */ + public static final class StoreResult { + + private final int storeId; + private final String dbName; + private final long entryCount; + private final byte[] inputDigest; + private final byte[] storeRoot; + + private StoreResult(int storeId, String dbName, long entryCount, byte[] inputDigest, + byte[] storeRoot) { + this.storeId = storeId; + this.dbName = dbName; + this.entryCount = entryCount; + this.inputDigest = Arrays.copyOf(inputDigest, inputDigest.length); + this.storeRoot = Arrays.copyOf(storeRoot, storeRoot.length); + } + + public int getStoreId() { + return storeId; + } + + public String getDbName() { + return dbName; + } + + public long getEntryCount() { + return entryCount; + } + + public byte[] getInputDigest() { + return Arrays.copyOf(inputDigest, inputDigest.length); + } + + public byte[] getStoreRoot() { + return Arrays.copyOf(storeRoot, storeRoot.length); + } + } + + /** Immutable publication result for BASE(P0). */ + public static final class RebuildResult { + + private final PathStateRootMetadata metadata; + private final List stores; + private final long totalEntries; + private final byte[] sourceDigest; + private final Map storesByName; + + private RebuildResult(PathStateRootMetadata metadata, List stores, + long totalEntries, byte[] sourceDigest) { + this.metadata = metadata; + this.stores = Collections.unmodifiableList(new ArrayList<>(stores)); + this.totalEntries = totalEntries; + this.sourceDigest = Arrays.copyOf(sourceDigest, sourceDigest.length); + LinkedHashMap indexed = new LinkedHashMap<>(); + for (StoreResult store : stores) { + indexed.put(store.getDbName(), store); + } + storesByName = Collections.unmodifiableMap(indexed); + } + + public PathStateRootMetadata getMetadata() { + return metadata; + } + + public List getStores() { + return stores; + } + + public StoreResult requireStore(String dbName) { + StoreResult result = storesByName.get(Objects.requireNonNull(dbName, "dbName")); + if (result == null) { + throw new IllegalArgumentException("unknown rebuild Store: " + dbName); + } + return result; + } + + public long getTotalEntries() { + return totalEntries; + } + + public byte[] getSourceDigest() { + return Arrays.copyOf(sourceDigest, sourceDigest.length); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java new file mode 100644 index 00000000000..713cac40f95 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -0,0 +1,227 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.arch.Arch; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.EntryConsumer; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.RebuildResult; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotSource; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateRebuildCoordinatorTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void rebuildsAndPublishesExactSnapshotAcrossNativeEngines() throws Exception { + byte[] expectedRoot = null; + byte[] expectedSourceDigest = null; + for (Engine engine : availableEngines()) { + PathStateStoreManifest manifest = manifest("rebuild-" + engine, engine); + TestSnapshotSource source = exactSource(identity()); + source.add("proposal", new byte[]{1}, new byte[]{11}); + source.add("proposal", new byte[]{2}, new byte[]{22}); + source.add("abi", address(1), new byte[0]); + + RebuildResult result = new PathStateRebuildCoordinator().rebuild(manifest, source); + + assertEquals(27, result.getStores().size()); + assertEquals(3, result.getTotalEntries()); + assertEquals(2, result.requireStore("proposal").getEntryCount()); + assertEquals(1, result.requireStore("abi").getEntryCount()); + assertEquals(0, result.requireStore("account").getEntryCount()); + assertArrayEquals(result.getSourceDigest(), result.getMetadata().getPayloadDigest()); + assertTrue(source.getVerificationCount() >= 2); + + PathStateRootMetadata current = new PathStateCurrentStore(manifest).current(); + assertArrayEquals(result.getMetadata().encode(), current.encode()); + try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openCurrent(manifest)) { + PathStateRoot restored = reopened.createRoot(); + assertArrayEquals(result.getMetadata().getStateRoot(), restored.rootHash()); + restored.verifyNodeStores(); + } + + if (expectedRoot == null) { + expectedRoot = result.getMetadata().getStateRoot(); + expectedSourceDigest = result.getSourceDigest(); + } else { + assertArrayEquals(expectedRoot, result.getMetadata().getStateRoot()); + assertArrayEquals(expectedSourceDigest, result.getSourceDigest()); + } + assertThrows(IOException.class, + () -> new PathStateRebuildCoordinator().rebuild(manifest, source)); + } + } + + @Test + public void rejectsScopeMismatchBeforeOpeningBaseNodes() throws Exception { + PathStateStoreManifest manifest = manifest("scope-mismatch", Engine.ROCKSDB); + TestSnapshotSource source = exactSource(identity()); + source.removeDatabase("abi"); + + assertThrows(IllegalArgumentException.class, + () -> new PathStateRebuildCoordinator().rebuild(manifest, source)); + assertFalse(new PathStateCurrentStore(manifest).isInitialized()); + assertFalse(Files.exists( + manifest.getBaseDirectory().resolve(PathStateNodeStoreSet.NODES_DIRECTORY))); + } + + @Test + public void rejectsDuplicateOrOutOfOrderPhysicalKeysWithoutPublication() throws Exception { + for (byte[][] keys : new byte[][][]{ + {new byte[]{2}, new byte[]{1}}, + {new byte[]{1}, new byte[]{1}} + }) { + PathStateStoreManifest manifest = manifest("order-" + keys[0][0] + "-" + keys[1][0], + Engine.ROCKSDB); + TestSnapshotSource source = exactSource(identity()); + source.add("proposal", keys[0], new byte[]{1}); + source.add("proposal", keys[1], new byte[]{2}); + + assertThrows(IllegalArgumentException.class, + () -> new PathStateRebuildCoordinator().rebuild(manifest, source)); + assertFalse(new PathStateCurrentStore(manifest).isInitialized()); + assertFalse(Files.exists(manifest.getBaseDirectory() + .resolve(PathStateCurrentStore.METADATA_FILE))); + } + } + + @Test + public void rejectsSnapshotIdentityDriftBeforePublishingBase() throws Exception { + PathStateStoreManifest manifest = manifest("identity-drift", Engine.ROCKSDB); + TestSnapshotSource source = exactSource(identity()); + source.add("proposal", new byte[]{1}, new byte[]{2}); + source.driftAfterFirstVerification(); + + IOException failure = assertThrows(IOException.class, + () -> new PathStateRebuildCoordinator().rebuild(manifest, source)); + assertTrue(failure.getMessage().contains("identity changed")); + assertFalse(new PathStateCurrentStore(manifest).isInitialized()); + assertFalse(Files.exists(manifest.getBaseDirectory() + .resolve(PathStateCurrentStore.METADATA_FILE))); + } + + private PathStateStoreManifest manifest(String name, Engine engine) throws IOException { + Path directory = temporaryFolder.newFolder(name).toPath(); + return PathStateStoreManifest.createOrOpen(directory, engine); + } + + private static TestSnapshotSource exactSource(SnapshotIdentity identity) { + LinkedHashMap> stores = new LinkedHashMap<>(); + for (PathStateParticipantDescriptor.StoreIdentity store + : PathStateParticipantDescriptor.current().getStores()) { + stores.put(store.getDbName(), new ArrayList<>()); + } + return new TestSnapshotSource(identity, stores); + } + + private static SnapshotIdentity identity() { + return new SnapshotIdentity(100, bytes(1), bytes(2), 300, P66Phase.P66_ON); + } + + private static Engine[] availableEngines() { + return Arch.isArm64() ? new Engine[]{Engine.ROCKSDB} + : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; + } + + private static byte[] address(int suffix) { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = (byte) suffix; + return address; + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + private static final class TestSnapshotSource implements SnapshotSource { + + private final SnapshotIdentity identity; + private final Map> stores; + private int verificationCount; + private boolean drift; + + private TestSnapshotSource(SnapshotIdentity identity, Map> stores) { + this.identity = identity; + this.stores = stores; + } + + private void add(String dbName, byte[] key, byte[] value) { + stores.get(dbName).add(new Row(key, value)); + } + + private void removeDatabase(String dbName) { + stores.remove(dbName); + } + + private void driftAfterFirstVerification() { + drift = true; + } + + private int getVerificationCount() { + return verificationCount; + } + + @Override + public SnapshotIdentity identity() { + return identity; + } + + @Override + public Collection databases() { + List names = new ArrayList<>(stores.keySet()); + Collections.reverse(names); + return names; + } + + @Override + public void scan(String dbName, EntryConsumer consumer) throws IOException { + for (Row row : stores.get(dbName)) { + consumer.accept(row.key, row.value); + } + } + + @Override + public void verifyIdentity(SnapshotIdentity expected) throws IOException { + verificationCount++; + if (!identity.sameAs(expected) || drift && verificationCount > 1) { + throw new IOException("path-state snapshot identity changed during rebuild"); + } + } + } + + private static final class Row { + + private final byte[] key; + private final byte[] value; + + private Row(byte[] key, byte[] value) { + this.key = key; + this.value = value; + } + } +} From 853d168efd77043894985994b7ec0fb55cda074e Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 09:54:14 +0800 Subject: [PATCH 067/161] feat(chainbase): add path state native snapshot source --- .../PathStateNativeSnapshotSource.java | 316 ++++++++++++++++++ .../PathStateNativeSnapshotSourceTest.java | 295 ++++++++++++++++ 2 files changed, 611 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java new file mode 100644 index 00000000000..c611b90c60e --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java @@ -0,0 +1,316 @@ +package org.tron.core.db2.stateroot; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.tron.core.capsule.utils.MarketUtils; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; +import org.tron.core.db2.common.DB; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.Snapshot; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.core.SnapshotRoot; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.EntryConsumer; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; + +/** Caller-owned exact-27 native snapshot lease used only by current-state rebuild. */ +public final class PathStateNativeSnapshotSource + implements PathStateRebuildCoordinator.SnapshotSource, Closeable { + + private final PathStateParticipantDescriptor descriptor; + private final SnapshotIdentity identity; + private final Map stores; + private final Map sourceIdentities; + private final Map snapshots; + private final int pageSize; + private final int marketEntryLimit; + private boolean closed; + + private PathStateNativeSnapshotSource(PathStateParticipantDescriptor descriptor, + SnapshotIdentity identity, Map stores, + Map sourceIdentities, Map snapshots, + int pageSize, int marketEntryLimit) { + this.descriptor = descriptor; + this.identity = identity; + this.stores = Collections.unmodifiableMap(new LinkedHashMap<>(stores)); + this.sourceIdentities = Collections.unmodifiableMap(new LinkedHashMap<>(sourceIdentities)); + this.snapshots = Collections.unmodifiableMap(new LinkedHashMap<>(snapshots)); + this.pageSize = pageSize; + this.marketEntryLimit = marketEntryLimit; + } + + /** + * Resolves and pins every participant while holding the canonical apply/flush barrier. + * Supplemental Stores are accepted only for exact participants absent from SnapshotManager. + */ + public static PathStateNativeSnapshotSource acquire(SnapshotManager manager, + Map supplementalStores, IdentityReader identityReader, + int pageSize, int marketEntryLimit) throws IOException { + Objects.requireNonNull(manager, "manager"); + Objects.requireNonNull(supplementalStores, "supplementalStores"); + Objects.requireNonNull(identityReader, "identityReader"); + if (pageSize <= 0 || marketEntryLimit <= 0) { + throw new IllegalArgumentException("path-state snapshot scan limits must be positive"); + } + PathStateNativeSnapshotSource[] acquired = new PathStateNativeSnapshotSource[1]; + manager.withArchiveStateBarrier(() -> acquired[0] = acquireInsideBarrier(manager, + supplementalStores, identityReader, pageSize, marketEntryLimit)); + return acquired[0]; + } + + private static PathStateNativeSnapshotSource acquireInsideBarrier(SnapshotManager manager, + Map supplementalStores, IdentityReader identityReader, + int pageSize, int marketEntryLimit) throws IOException { + PathStateParticipantDescriptor descriptor = PathStateParticipantDescriptor.current(); + LinkedHashMap stores = resolveStores(manager, supplementalStores, + descriptor); + SnapshotIdentity before = Objects.requireNonNull(identityReader.read(), "snapshot identity"); + LinkedHashMap identities = new LinkedHashMap<>(); + LinkedHashMap snapshots = new LinkedHashMap<>(); + try { + for (PathStateParticipantDescriptor.StoreIdentity participant : descriptor.getStores()) { + String dbName = participant.getDbName(); + SnapshotCapableStore store = stores.get(dbName); + String sourceIdentity = requireSourceIdentity(dbName, store.getSourceIdentity()); + StoreSnapshot snapshot = Objects.requireNonNull( + store.pin(before.getBlockNumber(), before.getBlockHash()), "pinned Store snapshot"); + identities.put(dbName, sourceIdentity); + snapshots.put(dbName, snapshot); + validateSnapshot(dbName, sourceIdentity, before, snapshot); + } + SnapshotIdentity after = Objects.requireNonNull(identityReader.read(), "snapshot identity"); + if (!before.sameAs(after)) { + throw new IOException("path-state canonical identity drifted during snapshot acquisition"); + } + return new PathStateNativeSnapshotSource(descriptor, before, stores, identities, snapshots, + pageSize, marketEntryLimit); + } catch (IOException | RuntimeException failure) { + closeAfterFailure(snapshots, failure); + throw failure; + } + } + + private static LinkedHashMap resolveStores(SnapshotManager manager, + Map supplementalStores, + PathStateParticipantDescriptor descriptor) throws IOException { + LinkedHashMap found = new LinkedHashMap<>(); + for (Chainbase database : new ArrayList<>(manager.getDbs())) { + String dbName = database.getDbName(); + try { + descriptor.require(dbName); + } catch (IllegalArgumentException outsideScope) { + continue; + } + if (found.containsKey(dbName)) { + throw new IOException("duplicate path-state SnapshotManager Store: " + dbName); + } + Snapshot head = database.getHead(); + if (!Snapshot.isRoot(head)) { + throw new IOException("path-state native snapshot requires a flushed Store: " + dbName); + } + Snapshot root = head.getRoot(); + DB engine = ((SnapshotRoot) root).getDb(); + if (!(engine instanceof SnapshotCapableStore) + || !dbName.equals(engine.getDbName())) { + throw new IOException("path-state Store lacks a native snapshot engine: " + dbName); + } + found.put(dbName, (SnapshotCapableStore) engine); + } + for (Map.Entry entry : supplementalStores.entrySet()) { + String dbName = entry.getKey(); + SnapshotCapableStore store = Objects.requireNonNull(entry.getValue(), "supplemental Store"); + descriptor.require(dbName); + if (!dbName.equals(store.getDbName()) || found.putIfAbsent(dbName, store) != null) { + throw new IOException("duplicate or mismatched supplemental path-state Store: " + dbName); + } + } + descriptor.requireExactDatabases(found.keySet()); + LinkedHashMap ordered = new LinkedHashMap<>(); + for (PathStateParticipantDescriptor.StoreIdentity participant : descriptor.getStores()) { + ordered.put(participant.getDbName(), found.get(participant.getDbName())); + } + return ordered; + } + + @Override + public synchronized SnapshotIdentity identity() { + ensureOpen(); + return identity; + } + + @Override + public Collection databases() { + return stores.keySet(); + } + + @Override + public synchronized void scan(String dbName, EntryConsumer consumer) throws IOException { + ensureOpen(); + Objects.requireNonNull(consumer, "consumer"); + PathStateParticipantDescriptor.StoreIdentity participant = descriptor.require(dbName); + StoreSnapshot snapshot = snapshots.get(dbName); + if (snapshot == null) { + throw new IOException("database is outside pinned path-state snapshot: " + dbName); + } + if (PathStateParticipantDescriptor.MARKET_PRICE_COMPARATOR.equals( + participant.getComparatorId())) { + List> entries = new ArrayList<>(); + scanLexical(snapshot, (key, value) -> { + if (entries.size() >= marketEntryLimit) { + throw new IOException("path-state market snapshot entry limit exceeded"); + } + entries.add(new java.util.AbstractMap.SimpleImmutableEntry<>(key, value)); + }); + entries.sort((left, right) -> MarketUtils.comparePriceKey(left.getKey(), right.getKey())); + for (Map.Entry entry : entries) { + consumer.accept(entry.getKey(), entry.getValue()); + } + return; + } + scanLexical(snapshot, consumer); + } + + private void scanLexical(StoreSnapshot snapshot, EntryConsumer consumer) throws IOException { + byte[] lower = new byte[0]; + byte[] previous = null; + while (true) { + List> page; + try { + page = snapshot.range(lower, null, pageSize); + } catch (UnsupportedOperationException unsupported) { + throw new IOException("pinned Store does not support range scan: " + + snapshot.getDbName(), unsupported); + } + if (page == null || page.size() > pageSize) { + throw new IOException("pinned Store returned an invalid range page: " + + snapshot.getDbName()); + } + for (Map.Entry entry : page) { + byte[] key = copy(entry.getKey(), "snapshot key"); + byte[] value = copy(entry.getValue(), "snapshot value"); + if (previous != null && compareUnsigned(previous, key) >= 0) { + throw new IOException("pinned Store range is not strictly lexical: " + + snapshot.getDbName()); + } + consumer.accept(key, value); + previous = key; + } + if (page.size() < pageSize) { + return; + } + if (previous == null) { + throw new IOException("pinned Store returned a full empty range page: " + + snapshot.getDbName()); + } + lower = Arrays.copyOf(previous, previous.length + 1); + } + } + + @Override + public synchronized void verifyIdentity(SnapshotIdentity expected) throws IOException { + ensureOpen(); + if (!identity.sameAs(expected)) { + throw new IOException("path-state snapshot block identity mismatch"); + } + for (Map.Entry entry : snapshots.entrySet()) { + String dbName = entry.getKey(); + String expectedSource = sourceIdentities.get(dbName); + if (!expectedSource.equals(stores.get(dbName).getSourceIdentity())) { + throw new IOException("path-state Store source was replaced: " + dbName); + } + validateSnapshot(dbName, expectedSource, identity, entry.getValue()); + } + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + IOException failure = null; + List reverse = new ArrayList<>(snapshots.values()); + Collections.reverse(reverse); + for (StoreSnapshot snapshot : reverse) { + try { + snapshot.close(); + } catch (IOException closeFailure) { + if (failure == null) { + failure = closeFailure; + } else { + failure.addSuppressed(closeFailure); + } + } + } + if (failure != null) { + throw failure; + } + } + + private static void validateSnapshot(String dbName, String sourceIdentity, + SnapshotIdentity identity, StoreSnapshot snapshot) throws IOException { + if (!dbName.equals(snapshot.getDbName()) + || !sourceIdentity.equals(snapshot.getSourceIdentity()) + || snapshot.getBlockNumber() != identity.getBlockNumber() + || !Arrays.equals(snapshot.getBlockHash(), identity.getBlockHash())) { + throw new IOException("pinned path-state Store identity mismatch: " + dbName); + } + } + + private static String requireSourceIdentity(String dbName, String sourceIdentity) + throws IOException { + if (sourceIdentity == null || sourceIdentity.isEmpty()) { + throw new IOException("path-state Store source identity is invalid: " + dbName); + } + return sourceIdentity; + } + + private static void closeAfterFailure(Map snapshots, + Exception failure) { + List reverse = new ArrayList<>(snapshots.values()); + Collections.reverse(reverse); + for (StoreSnapshot snapshot : reverse) { + try { + snapshot.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + } + + private static byte[] copy(byte[] value, String name) throws IOException { + if (value == null) { + throw new IOException(name + " must not be null"); + } + return Arrays.copyOf(value, value.length); + } + + private static int compareUnsigned(byte[] left, byte[] right) { + for (int index = 0; index < Math.min(left.length, right.length); index++) { + int compared = Integer.compare(left[index] & 0xff, right[index] & 0xff); + if (compared != 0) { + return compared; + } + } + return Integer.compare(left.length, right.length); + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("path-state native snapshot source is closed"); + } + } + + @FunctionalInterface + public interface IdentityReader { + SnapshotIdentity read() throws IOException; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java new file mode 100644 index 00000000000..b6c14fd5902 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java @@ -0,0 +1,295 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.tron.common.TestConstants.TEST_CONF; + +import java.io.IOException; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.common.storage.leveldb.LevelDbDataSourceImpl; +import org.tron.common.utils.ByteArray; +import org.tron.core.capsule.utils.MarketUtils; +import org.tron.core.config.args.Args; +import org.tron.core.db.common.iterator.DBIterator; +import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; +import org.tron.core.db2.common.LevelDB; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.core.SnapshotRoot; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; + +public class PathStateNativeSnapshotSourceTest { + + static { + org.rocksdb.RocksDB.loadLibrary(); + } + + @BeforeClass + public static void initConfiguration() { + Args.setParam(new String[0], TEST_CONF); + } + + @AfterClass + public static void clearConfiguration() { + Args.clearParam(); + } + + @Test + public void pinsExactStoresPagesLexicallyAndOrdersMarketRows() throws Exception { + Registry registry = registry(); + registry.probes.get("proposal").add(new byte[]{1}, new byte[]{11}); + registry.probes.get("proposal").add(new byte[]{1, 0}, new byte[]{12}); + registry.probes.get("proposal").add(new byte[]{2}, new byte[]{22}); + byte[] lowPrice = marketKey(2, 1); + byte[] highPrice = marketKey(1, 1); + assertTrue(compareUnsigned(lowPrice, highPrice) > 0); + registry.probes.get(PathStateParticipantDescriptor.MARKET_PRICE_DATABASE) + .add(lowPrice, new byte[]{1}); + registry.probes.get(PathStateParticipantDescriptor.MARKET_PRICE_DATABASE) + .add(highPrice, new byte[]{2}); + + List proposalKeys = new ArrayList<>(); + List marketKeys = new ArrayList<>(); + try (PathStateNativeSnapshotSource source = PathStateNativeSnapshotSource.acquire( + registry.manager, Collections.emptyMap(), PathStateNativeSnapshotSourceTest::identity, + 2, 10)) { + assertEquals(27, source.databases().size()); + source.scan("proposal", (key, value) -> proposalKeys.add(key)); + source.scan(PathStateParticipantDescriptor.MARKET_PRICE_DATABASE, + (key, value) -> marketKeys.add(key)); + source.verifyIdentity(identity()); + } + + assertEquals(3, proposalKeys.size()); + assertArrayEquals(new byte[]{1}, proposalKeys.get(0)); + assertArrayEquals(new byte[]{1, 0}, proposalKeys.get(1)); + assertArrayEquals(new byte[]{2}, proposalKeys.get(2)); + assertArrayEquals(lowPrice, marketKeys.get(0)); + assertArrayEquals(highPrice, marketKeys.get(1)); + assertEquals(27, registry.totalPins()); + assertEquals(27, registry.totalCloses()); + } + + @Test + public void acceptsSupplementalAccountAssetAndRejectsMarketOverflow() throws Exception { + Registry registry = registry(); + Probe accountAsset = registry.probes.get("account-asset"); + registry.manager.getDbs().removeIf(database -> "account-asset".equals(database.getDbName())); + accountAsset.add(marketKey(1, 2), new byte[]{1}); + Probe market = registry.probes.get(PathStateParticipantDescriptor.MARKET_PRICE_DATABASE); + market.add(marketKey(1, 2), new byte[]{1}); + market.add(marketKey(2, 3), new byte[]{2}); + + try (PathStateNativeSnapshotSource source = PathStateNativeSnapshotSource.acquire( + registry.manager, Collections.singletonMap("account-asset", accountAsset.store), + PathStateNativeSnapshotSourceTest::identity, 2, 1)) { + assertEquals(27, source.databases().size()); + assertThrows(IOException.class, + () -> source.scan(PathStateParticipantDescriptor.MARKET_PRICE_DATABASE, + (key, value) -> { })); + } + assertEquals(registry.totalPins(), registry.totalCloses()); + } + + @Test + public void identityDriftDuringAcquireReleasesEveryPinnedStore() { + Registry registry = registry(); + AtomicInteger reads = new AtomicInteger(); + assertThrows(IOException.class, () -> PathStateNativeSnapshotSource.acquire( + registry.manager, Collections.emptyMap(), + () -> reads.incrementAndGet() == 1 ? identity() : identity(2), 2, 10)); + assertEquals(27, registry.totalPins()); + assertEquals(27, registry.totalCloses()); + } + + @Test + public void rejectsUnflushedRevokingLayersBeforePinning() throws Exception { + Registry registry = registry(); + try (ISession ignored = registry.manager.buildSession(true)) { + assertThrows(IOException.class, () -> PathStateNativeSnapshotSource.acquire( + registry.manager, Collections.emptyMap(), PathStateNativeSnapshotSourceTest::identity, + 2, 10)); + } + assertEquals(0, registry.totalPins()); + } + + private static Registry registry() { + SnapshotManager manager = new SnapshotManager(""); + LinkedHashMap probes = new LinkedHashMap<>(); + for (PathStateParticipantDescriptor.StoreIdentity participant + : PathStateParticipantDescriptor.current().getStores()) { + Probe probe = new Probe(participant.getDbName()); + probes.put(participant.getDbName(), probe); + manager.getDbs().add(new Chainbase(new SnapshotRoot(probe.store))); + } + return new Registry(manager, probes); + } + + private static SnapshotIdentity identity() { + return identity(1); + } + + private static SnapshotIdentity identity(int suffix) { + byte[] blockHash = new byte[32]; + blockHash[31] = (byte) suffix; + return new SnapshotIdentity(100 + suffix, blockHash, new byte[32], 1_000L + suffix, + P66Phase.P66_ON); + } + + private static byte[] marketKey(long sell, long buy) { + return MarketUtils.createPairPriceKey(ByteArray.fromString("100"), + ByteArray.fromString("200"), sell, buy); + } + + private static int compareUnsigned(byte[] left, byte[] right) { + for (int index = 0; index < Math.min(left.length, right.length); index++) { + int compared = Integer.compare(left[index] & 0xff, right[index] & 0xff); + if (compared != 0) { + return compared; + } + } + return Integer.compare(left.length, right.length); + } + + private static final class Registry { + private final SnapshotManager manager; + private final Map probes; + + private Registry(SnapshotManager manager, Map probes) { + this.manager = manager; + this.probes = probes; + } + + private int totalPins() { + return probes.values().stream().mapToInt(probe -> probe.pins.get()).sum(); + } + + private int totalCloses() { + return probes.values().stream().mapToInt(probe -> probe.closes.get()).sum(); + } + } + + private static final class Probe { + private final String dbName; + private final String sourceIdentity; + private final AtomicInteger pins = new AtomicInteger(); + private final AtomicInteger closes = new AtomicInteger(); + private final List> rows = new ArrayList<>(); + private final FakeLevelDB store; + + private Probe(String dbName) { + this.dbName = dbName; + sourceIdentity = "test:" + dbName; + store = new FakeLevelDB(this); + } + + private void add(byte[] key, byte[] value) { + rows.add(new SimpleImmutableEntry<>(Arrays.copyOf(key, key.length), + Arrays.copyOf(value, value.length))); + rows.sort((left, right) -> compareUnsigned(left.getKey(), right.getKey())); + } + + private StoreSnapshot pin(long blockNumber, byte[] blockHash) { + pins.incrementAndGet(); + byte[] expectedHash = Arrays.copyOf(blockHash, blockHash.length); + List> pinnedRows = new ArrayList<>(rows); + return new StoreSnapshot() { + private boolean closed; + + @Override + public String getDbName() { + return dbName; + } + + @Override + public String getSourceIdentity() { + return sourceIdentity; + } + + @Override + public long getBlockNumber() { + return blockNumber; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(expectedHash, expectedHash.length); + } + + @Override + public byte[] get(byte[] physicalRawKey) { + return null; + } + + @Override + public List> range(byte[] lowerInclusive, + byte[] upperExclusive, int maxEntries) { + List> result = new ArrayList<>(); + for (Map.Entry row : pinnedRows) { + if (compareUnsigned(row.getKey(), lowerInclusive) >= 0 + && (upperExclusive == null + || compareUnsigned(row.getKey(), upperExclusive) < 0)) { + result.add(row); + if (result.size() == maxEntries) { + break; + } + } + } + return result; + } + + @Override + public void close() { + if (!closed) { + closed = true; + closes.incrementAndGet(); + } + } + }; + } + } + + private static final class FakeLevelDB extends LevelDB { + private final Probe probe; + + private FakeLevelDB(Probe probe) { + super(mock(LevelDbDataSourceImpl.class)); + this.probe = probe; + } + + @Override + public String getDbName() { + return probe.dbName; + } + + @Override + public String getSourceIdentity() { + return probe.sourceIdentity; + } + + @Override + public StoreSnapshot pin(long blockNumber, byte[] blockHash) { + return probe.pin(blockNumber, blockHash); + } + + @Override + public DBIterator iterator() { + return mock(DBIterator.class); + } + } +} From d66f61d75a8cdf165359ee08d32ed8f40ea03d57 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 10:03:24 +0800 Subject: [PATCH 068/161] feat(chainbase): validate path state p66 layout --- .../db2/stateroot/PathStateCanonicalizer.java | 51 +++++++++-- .../PathStateNativeSnapshotSource.java | 12 +++ .../PathStateRebuildCoordinator.java | 30 ++++++- .../PathStateNativeSnapshotSourceTest.java | 5 ++ .../PathStateRebuildCoordinatorTest.java | 87 ++++++++++++++++++- 5 files changed, 172 insertions(+), 13 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java index 96c081a0d45..cd6cf043902 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java @@ -99,6 +99,34 @@ public PathStateMutation accountAsset(P66Phase phase, byte[] address, String tok ByteBuffer.allocate(BALANCE_LENGTH).putLong(balance).array()); } + /** Requires the physical Account representation admitted for a rebuild target phase. */ + public void requireSnapshotAccountLayout(P66Phase phase, byte[] physicalKey, + byte[] rawValue) { + P66Phase target = Objects.requireNonNull(phase, "phase"); + byte[] key = copyNonEmpty(physicalKey, "physicalKey"); + requireLength(key, ADDRESS_LENGTH, "account key"); + Account account = parseAccount(key, rawValue); + if (target.directAssetsEnabled()) { + if (!account.getAssetOptimized() || !account.getAssetMap().isEmpty() + || !account.getAssetV2Map().isEmpty()) { + throw new IllegalArgumentException("P66-on snapshot Account layout is mixed"); + } + } else if (account.getAssetOptimized()) { + throw new IllegalArgumentException("P66-off snapshot Account layout is mixed"); + } + } + + /** Extracts and validates the owning Account address from one direct physical key. */ + public byte[] accountAddressFromAssetKey(P66Phase phase, byte[] physicalKey) { + P66Phase target = Objects.requireNonNull(phase, "phase"); + if (!target.directAssetsEnabled()) { + throw new IllegalArgumentException("P66-off state must not contain account-asset rows"); + } + byte[] key = copyNonEmpty(physicalKey, "physicalKey"); + decodeAccountAssetKey(key); + return Arrays.copyOf(key, ADDRESS_LENGTH); + } + private static void configure(Map formats, String dbName, String codecId) { if (formats.replace(dbName, new StoreFormat(dbName, codecId)) == null) { throw new IllegalStateException("missing path-state format participant: " + dbName); @@ -163,15 +191,7 @@ private static byte[] canonicalValue(P66Phase phase, String dbName, byte[] physi } private static byte[] canonicalAccount(P66Phase phase, byte[] physicalKey, byte[] rawValue) { - Account account; - try { - account = Account.parseFrom(rawValue); - } catch (InvalidProtocolBufferException invalid) { - throw new IllegalArgumentException("account value is not valid protobuf", invalid); - } - if (!Arrays.equals(physicalKey, account.getAddress().toByteArray())) { - throw new IllegalArgumentException("account protobuf address does not match physical key"); - } + Account account = parseAccount(physicalKey, rawValue); if (!phase.directAssetsEnabled()) { if (account.getAssetOptimized()) { throw new IllegalArgumentException("P66-off Account must not use direct asset layout"); @@ -186,6 +206,19 @@ private static byte[] canonicalAccount(P66Phase phase, byte[] physicalKey, byte[ .toByteArray(); } + private static Account parseAccount(byte[] physicalKey, byte[] rawValue) { + Account account; + try { + account = Account.parseFrom(Objects.requireNonNull(rawValue, "rawValue")); + } catch (InvalidProtocolBufferException invalid) { + throw new IllegalArgumentException("account value is not valid protobuf", invalid); + } + if (!Arrays.equals(physicalKey, account.getAddress().toByteArray())) { + throw new IllegalArgumentException("account protobuf address does not match physical key"); + } + return account; + } + private static void parseAbi(byte[] value) { try { ABI.parseFrom(value); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java index c611b90c60e..e681f42df4c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java @@ -151,6 +151,18 @@ public Collection databases() { return stores.keySet(); } + @Override + public synchronized byte[] get(String dbName, byte[] physicalKey) throws IOException { + ensureOpen(); + descriptor.require(dbName); + StoreSnapshot snapshot = snapshots.get(dbName); + if (snapshot == null) { + throw new IOException("database is outside pinned path-state snapshot: " + dbName); + } + byte[] value = snapshot.get(copy(physicalKey, "physicalKey")); + return value == null ? null : Arrays.copyOf(value, value.length); + } + @Override public synchronized void scan(String dbName, EntryConsumer consumer) throws IOException { ensureOpen(); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index 6b8713f7ec3..b8b302fe9a9 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -56,7 +56,8 @@ public RebuildResult rebuild(PathStateStoreManifest manifest, SnapshotSource sou List storeResults = new ArrayList<>(); long totalEntries = 0; for (StoreIdentity store : descriptor.getStores()) { - StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root); + StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root, + admittedSource); admittedSource.scan(store.getDbName(), accumulator::accept); StoreResult result = accumulator.finish(); storeResults.add(result); @@ -102,14 +103,17 @@ private final class StoreAccumulator { private final StoreIdentity store; private final P66Phase phase; private final PathStateRoot root; + private final SnapshotSource source; private final Hasher inputDigest; private byte[] previousKey; private long entryCount; - private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root) { + private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root, + SnapshotSource source) { this.store = store; this.phase = phase; this.root = root; + this.source = source; inputDigest = domainHasher(STORE_DIGEST_DOMAIN); putInt(inputDigest, store.getStoreId()); putString(inputDigest, store.getDbName()); @@ -117,7 +121,7 @@ private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root putString(inputDigest, canonicalizer.requireFormat(store.getDbName()).getCodecId()); } - private void accept(byte[] physicalKey, byte[] rawValue) { + private void accept(byte[] physicalKey, byte[] rawValue) throws IOException { byte[] key = copyNonEmpty(physicalKey, "physicalKey"); byte[] value = Arrays.copyOf(Objects.requireNonNull(rawValue, "rawValue"), rawValue.length); @@ -125,6 +129,7 @@ private void accept(byte[] physicalKey, byte[] rawValue) { throw new IllegalArgumentException( "path-state snapshot keys are not strictly increasing: " + store.getDbName()); } + validateAccountAssetLayout(key, value); PathStateMutation mutation = canonicalizer.put(phase, store.getDbName(), key, value); root.apply(Collections.singletonList(mutation)); putBytes(inputDigest, key); @@ -133,6 +138,22 @@ private void accept(byte[] physicalKey, byte[] rawValue) { entryCount = Math.addExact(entryCount, 1L); } + private void validateAccountAssetLayout(byte[] key, byte[] value) throws IOException { + if ("account".equals(store.getDbName())) { + canonicalizer.requireSnapshotAccountLayout(phase, key, value); + return; + } + if (!"account-asset".equals(store.getDbName())) { + return; + } + byte[] accountKey = canonicalizer.accountAddressFromAssetKey(phase, key); + byte[] accountValue = source.get("account", accountKey); + if (accountValue == null) { + throw new IOException("path-state AccountAsset row has no owning Account"); + } + canonicalizer.requireSnapshotAccountLayout(phase, accountKey, accountValue); + } + private StoreResult finish() { return new StoreResult(store.getStoreId(), store.getDbName(), entryCount, inputDigest.hash().asBytes(), root.participantRoot(store.getDbName())); @@ -192,6 +213,9 @@ public interface SnapshotSource { Collection databases(); + /** Returns one value from the same pinned snapshot, or {@code null} when physically absent. */ + byte[] get(String dbName, byte[] physicalKey) throws IOException; + void scan(String dbName, EntryConsumer consumer) throws IOException; void verifyIdentity(SnapshotIdentity expected) throws IOException; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java index b6c14fd5902..d92ae400192 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java @@ -233,6 +233,11 @@ public byte[] getBlockHash() { @Override public byte[] get(byte[] physicalRawKey) { + for (Map.Entry row : pinnedRows) { + if (Arrays.equals(row.getKey(), physicalRawKey)) { + return Arrays.copyOf(row.getValue(), row.getValue().length); + } + } return null; } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index 713cac40f95..02622671b0a 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -6,7 +6,9 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.google.protobuf.ByteString; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -25,6 +27,7 @@ import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotSource; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; +import org.tron.protos.Protocol.Account; public class PathStateRebuildCoordinatorTest { @@ -120,6 +123,61 @@ public void rejectsSnapshotIdentityDriftBeforePublishingBase() throws Exception .resolve(PathStateCurrentStore.METADATA_FILE))); } + @Test + public void admitsOnlyTargetP66AccountAssetPhysicalLayout() throws Exception { + byte[] address = address(7); + String tokenId = "1000001"; + + PathStateStoreManifest offManifest = manifest("p66-off", Engine.ROCKSDB); + TestSnapshotSource off = exactSource(identity(P66Phase.P66_OFF)); + byte[] embedded = account(address).toBuilder().putAssetV2(tokenId, 11L).build().toByteArray(); + off.add("account", address, embedded); + RebuildResult offResult = new PathStateRebuildCoordinator().rebuild(offManifest, off); + assertEquals(1, offResult.requireStore("account").getEntryCount()); + assertEquals(0, offResult.requireStore("account-asset").getEntryCount()); + + PathStateStoreManifest onManifest = manifest("p66-on", Engine.ROCKSDB); + TestSnapshotSource on = exactSource(identity(P66Phase.P66_ON)); + byte[] optimized = account(address).toBuilder().setAssetOptimized(true).build().toByteArray(); + on.add("account", address, optimized); + on.add("account-asset", accountAssetKey(address, tokenId), longBytes(11L)); + RebuildResult onResult = new PathStateRebuildCoordinator().rebuild(onManifest, on); + assertEquals(1, onResult.requireStore("account").getEntryCount()); + assertEquals(1, onResult.requireStore("account-asset").getEntryCount()); + } + + @Test + public void rejectsMixedOrOrphanAccountAssetSnapshotWithoutPublication() throws Exception { + byte[] address = address(8); + String tokenId = "1000001"; + + PathStateStoreManifest mixedManifest = manifest("p66-mixed", Engine.ROCKSDB); + TestSnapshotSource mixed = exactSource(identity(P66Phase.P66_ON)); + mixed.add("account", address, + account(address).toBuilder().putAssetV2(tokenId, 9L).build().toByteArray()); + mixed.add("account-asset", accountAssetKey(address, tokenId), longBytes(9L)); + assertThrows(IllegalArgumentException.class, + () -> new PathStateRebuildCoordinator().rebuild(mixedManifest, mixed)); + assertFalse(new PathStateCurrentStore(mixedManifest).isInitialized()); + + PathStateStoreManifest orphanManifest = manifest("p66-orphan", Engine.ROCKSDB); + TestSnapshotSource orphan = exactSource(identity(P66Phase.P66_ON)); + orphan.add("account-asset", accountAssetKey(address, tokenId), longBytes(10L)); + IOException orphanFailure = assertThrows(IOException.class, + () -> new PathStateRebuildCoordinator().rebuild(orphanManifest, orphan)); + assertTrue(orphanFailure.getMessage().contains("no owning Account")); + assertFalse(new PathStateCurrentStore(orphanManifest).isInitialized()); + + PathStateStoreManifest offDirectManifest = manifest("p66-off-direct", Engine.ROCKSDB); + TestSnapshotSource offDirect = exactSource(identity(P66Phase.P66_OFF)); + offDirect.add("account", address, + account(address).toBuilder().putAssetV2(tokenId, 10L).build().toByteArray()); + offDirect.add("account-asset", accountAssetKey(address, tokenId), longBytes(10L)); + assertThrows(IllegalArgumentException.class, + () -> new PathStateRebuildCoordinator().rebuild(offDirectManifest, offDirect)); + assertFalse(new PathStateCurrentStore(offDirectManifest).isInitialized()); + } + private PathStateStoreManifest manifest(String name, Engine engine) throws IOException { Path directory = temporaryFolder.newFolder(name).toPath(); return PathStateStoreManifest.createOrOpen(directory, engine); @@ -135,7 +193,11 @@ private static TestSnapshotSource exactSource(SnapshotIdentity identity) { } private static SnapshotIdentity identity() { - return new SnapshotIdentity(100, bytes(1), bytes(2), 300, P66Phase.P66_ON); + return identity(P66Phase.P66_ON); + } + + private static SnapshotIdentity identity(P66Phase phase) { + return new SnapshotIdentity(100, bytes(1), bytes(2), 300, phase); } private static Engine[] availableEngines() { @@ -158,6 +220,19 @@ private static byte[] bytes(int seed) { return value; } + private static Account account(byte[] address) { + return Account.newBuilder().setAddress(ByteString.copyFrom(address)).build(); + } + + private static byte[] accountAssetKey(byte[] address, String tokenId) { + byte[] token = tokenId.getBytes(java.nio.charset.StandardCharsets.US_ASCII); + return ByteBuffer.allocate(address.length + token.length).put(address).put(token).array(); + } + + private static byte[] longBytes(long value) { + return ByteBuffer.allocate(Long.BYTES).putLong(value).array(); + } + private static final class TestSnapshotSource implements SnapshotSource { private final SnapshotIdentity identity; @@ -198,6 +273,16 @@ public Collection databases() { return names; } + @Override + public byte[] get(String dbName, byte[] physicalKey) { + for (Row row : stores.get(dbName)) { + if (java.util.Arrays.equals(row.key, physicalKey)) { + return java.util.Arrays.copyOf(row.value, row.value.length); + } + } + return null; + } + @Override public void scan(String dbName, EntryConsumer consumer) throws IOException { for (Row row : stores.get(dbName)) { From 36775572ca9d357436b7869152984cd4de38b641 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 10:36:17 +0800 Subject: [PATCH 069/161] feat(chainbase): resume path state rebuild --- .../PathStateNativeSnapshotSource.java | 27 +++ .../db2/stateroot/PathStateNodeStoreSet.java | 134 +++++++++++- .../stateroot/PathStateRebuildCheckpoint.java | 192 ++++++++++++++++++ .../PathStateRebuildCoordinator.java | 63 +++++- .../PathStateRebuildCoordinatorTest.java | 118 +++++++++++ 5 files changed, 521 insertions(+), 13 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCheckpoint.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java index e681f42df4c..b3833c6f6e9 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java @@ -1,7 +1,11 @@ package org.tron.core.db2.stateroot; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; import java.io.Closeable; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -29,6 +33,7 @@ public final class PathStateNativeSnapshotSource private final SnapshotIdentity identity; private final Map stores; private final Map sourceIdentities; + private final byte[] sourceIdentityDigest; private final Map snapshots; private final int pageSize; private final int marketEntryLimit; @@ -42,6 +47,7 @@ private PathStateNativeSnapshotSource(PathStateParticipantDescriptor descriptor, this.identity = identity; this.stores = Collections.unmodifiableMap(new LinkedHashMap<>(stores)); this.sourceIdentities = Collections.unmodifiableMap(new LinkedHashMap<>(sourceIdentities)); + this.sourceIdentityDigest = sourceIdentityDigest(this.sourceIdentities); this.snapshots = Collections.unmodifiableMap(new LinkedHashMap<>(snapshots)); this.pageSize = pageSize; this.marketEntryLimit = marketEntryLimit; @@ -151,6 +157,11 @@ public Collection databases() { return stores.keySet(); } + @Override + public byte[] sourceIdentityDigest() { + return Arrays.copyOf(sourceIdentityDigest, sourceIdentityDigest.length); + } + @Override public synchronized byte[] get(String dbName, byte[] physicalKey) throws IOException { ensureOpen(); @@ -315,6 +326,22 @@ private static int compareUnsigned(byte[] left, byte[] right) { return Integer.compare(left.length, right.length); } + private static byte[] sourceIdentityDigest(Map identities) { + Hasher hasher = Hashing.sha256().newHasher(); + hasher.putBytes(ByteBuffer.allocate(Integer.BYTES).putInt(identities.size()).array()); + for (Map.Entry entry : identities.entrySet()) { + putString(hasher, entry.getKey()); + putString(hasher, entry.getValue()); + } + return hasher.hash().asBytes(); + } + + private static void putString(Hasher hasher, String value) { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + hasher.putBytes(ByteBuffer.allocate(Integer.BYTES).putInt(encoded.length).array()); + hasher.putBytes(encoded); + } + private void ensureOpen() { if (closed) { throw new IllegalStateException("path-state native snapshot source is closed"); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index 730f456d2c2..ba5b03f6dd6 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -24,6 +24,9 @@ public final class PathStateNodeStoreSet implements Closeable { private static final byte[] LOGICAL_BYTES_KEY = new byte[]{ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 'l', 'o', 'g', 'i', 'c', 'a', 'l', '-', 'b', 'y', 't', 'e', 's'}; + private static final byte[] REBUILD_CHECKPOINT_KEY = new byte[]{ + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + 'r', 'e', 'b', 'u', 'i', 'l', 'd'}; private static final int LEAF_DOMAIN = -2; private static final byte[] LEAF_PREFIX = ByteBuffer.allocate(Integer.BYTES) .putInt(LEAF_DOMAIN).array(); @@ -41,6 +44,7 @@ public final class PathStateNodeStoreSet implements Closeable { private final PathStateRootMetadata expectedMetadata; private final boolean sealed; private PathStateRootMetadata progress; + private PathStateRebuildCheckpoint rebuildCheckpoint; private Long logicalBytes; private PathStateRoot root; private boolean rootClaimed; @@ -60,16 +64,23 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K try { progress = decodeProgress(nativeStore.get(PROGRESS_KEY)); logicalBytes = decodeLogicalBytes(nativeStore.get(LOGICAL_BYTES_KEY)); + rebuildCheckpoint = decodeRebuildCheckpoint(nativeStore.get(REBUILD_CHECKPOINT_KEY)); if ((progress == null) != (logicalBytes == null)) { throw new IOException("path-state native progress and logical bytes marker differ"); } + if (progress != null && rebuildCheckpoint != null) { + throw new IOException("path-state native progress conflicts with rebuild checkpoint"); + } if (progress != null) { requireProgressIdentity(progress); } else if (kind == Kind.BASE && expectedMetadata != null) { throw new IOException("path-state BASE metadata exists without native progress"); } + if (rebuildCheckpoint != null) { + requireRebuildCheckpointIdentity(rebuildCheckpoint); + } loadPersistedLeaves(); - if (progress == null && !persistedLeaves.isEmpty()) { + if (progress == null && rebuildCheckpoint == null && !persistedLeaves.isEmpty()) { throw new IOException("path-state leaf inventory exists without native progress"); } for (PathStateParticipant participant : scope.getParticipants()) { @@ -151,8 +162,10 @@ public synchronized PathStateRoot createRoot() { PathStateRoot candidate = new PathStateRoot(scope, participant -> participantStores.get(participant.getDbName()), superStore); - if (progress != null) { - candidate.restoreLeaves(restoredLeafRecords(), progress.getStateRoot()); + if (progress != null || rebuildCheckpoint != null) { + byte[] expectedRoot = progress == null ? rebuildCheckpoint.getPartialRoot() + : progress.getStateRoot(); + candidate.restoreLeaves(restoredLeafRecords(), expectedRoot); if (!pending.isEmpty()) { throw new IllegalStateException("path-state leaf restoration attempted to repair nodes"); } @@ -221,14 +234,58 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti mutations.add(PathStateNativeNodeStore.BatchMutation.put(PROGRESS_KEY, next.encode())); mutations.add(PathStateNativeNodeStore.BatchMutation.put(LOGICAL_BYTES_KEY, ByteBuffer.allocate(Long.BYTES).putLong(nextLogicalBytes).array())); + if (rebuildCheckpoint != null) { + mutations.add(PathStateNativeNodeStore.BatchMutation.delete(REBUILD_CHECKPOINT_KEY)); + } nativeStore.writeBatch(mutations); pending.clear(); persistedLeaves.clear(); persistedLeaves.putAll(nextLeaves); progress = next; + rebuildCheckpoint = null; logicalBytes = nextLogicalBytes; } + /** Persists one more completed rebuild Store without creating BASE authority. */ + synchronized void checkpointRebuild(PathStateRebuildCheckpoint checkpoint) throws IOException { + requireOpen(); + if (kind != Kind.BASE || sealed || progress != null || root == null) { + throw new IOException("path-state rebuild checkpoint is not admissible"); + } + PathStateRebuildCheckpoint next = Objects.requireNonNull(checkpoint, "checkpoint"); + requireRebuildCheckpointIdentity(next); + if (!Arrays.equals(root.rootHash(), next.getPartialRoot())) { + throw new IOException("path-state rebuild checkpoint root mismatch"); + } + int previousCount = rebuildCheckpoint == null ? 0 + : rebuildCheckpoint.getCompletedStores().size(); + if (next.getCompletedStores().size() != previousCount + 1) { + throw new IOException("path-state rebuild checkpoint must advance one Store"); + } + if (rebuildCheckpoint != null) { + List previous = + rebuildCheckpoint.getCompletedStores(); + List advanced = next.getCompletedStores(); + for (int index = 0; index < previous.size(); index++) { + if (!sameStoreResult(previous.get(index), advanced.get(index))) { + throw new IOException("path-state rebuild checkpoint rewrites completed Store"); + } + } + } + List mutations = + durableStateMutations(next.encode()); + nativeStore.writeBatch(mutations); + pending.clear(); + persistedLeaves.clear(); + persistedLeaves.putAll(leafMap(root.leafRecords())); + rebuildCheckpoint = next; + } + + PathStateRebuildCheckpoint getRebuildCheckpoint() { + requireOpen(); + return rebuildCheckpoint; + } + synchronized long projectedLogicalBytes(PathStateRootMetadata metadata) throws IOException { requireOpen(); if (root == null) { @@ -239,7 +296,8 @@ synchronized long projectedLogicalBytes(PathStateRootMetadata metadata) throws I if (!Arrays.equals(root.rootHash(), next.getStateRoot())) { throw new IllegalArgumentException("path-state progress root does not match trie root"); } - long total = logicalBytes == null ? 0 : logicalBytes; + long total = rebuildCheckpoint == null ? (logicalBytes == null ? 0 : logicalBytes) + : rebuildLogicalBytes(); for (Map.Entry entry : pending.entrySet()) { byte[] key = entry.getKey().copy(); total = replaceLogicalEntry(total, key, nativeStore.get(key), entry.getValue()); @@ -256,8 +314,10 @@ synchronized long projectedLogicalBytes(PathStateRootMetadata metadata) throws I total = replaceLogicalEntry(total, entry.getKey().copy(), previous, entry.getValue()); } } - return replaceLogicalEntry(total, PROGRESS_KEY, + total = replaceLogicalEntry(total, PROGRESS_KEY, progress == null ? null : progress.encode(), next.encode()); + return rebuildCheckpoint == null ? total : replaceLogicalEntry(total, + REBUILD_CHECKPOINT_KEY, rebuildCheckpoint.encode(), null); } public synchronized PathStateRootMetadata getProgress() { @@ -373,6 +433,65 @@ private void loadPersistedLeaves() throws IOException { } } + private List durableStateMutations( + byte[] rebuildValue) { + List mutations = + new ArrayList<>(pending.size() + persistedLeaves.size() + 1); + for (Map.Entry entry : pending.entrySet()) { + byte[] value = entry.getValue(); + mutations.add(value == null + ? PathStateNativeNodeStore.BatchMutation.delete(entry.getKey().copy()) + : PathStateNativeNodeStore.BatchMutation.put(entry.getKey().copy(), value)); + } + Map nextLeaves = leafMap(root.leafRecords()); + for (BytesKey persisted : persistedLeaves.keySet()) { + if (!nextLeaves.containsKey(persisted)) { + mutations.add(PathStateNativeNodeStore.BatchMutation.delete(persisted.copy())); + } + } + for (Map.Entry entry : nextLeaves.entrySet()) { + if (!Arrays.equals(persistedLeaves.get(entry.getKey()), entry.getValue())) { + mutations.add(PathStateNativeNodeStore.BatchMutation.put( + entry.getKey().copy(), entry.getValue())); + } + } + mutations.add(PathStateNativeNodeStore.BatchMutation.put(REBUILD_CHECKPOINT_KEY, + rebuildValue)); + return mutations; + } + + private long rebuildLogicalBytes() throws IOException { + long total = 0; + try { + for (PathStateNativeNodeStore.KeyValue entry : nativeStore.scanAll()) { + byte[] key = entry.getKey(); + if (!Arrays.equals(key, LOGICAL_BYTES_KEY)) { + total = Math.addExact(total, Math.addExact(key.length, entry.getValue().length)); + } + } + return total; + } catch (ArithmeticException overflow) { + throw new IOException("path-state rebuild logical bytes overflow", overflow); + } + } + + private void requireRebuildCheckpointIdentity(PathStateRebuildCheckpoint checkpoint) + throws IOException { + if (kind != Kind.BASE || expectedMetadata != null + || !Arrays.equals(checkpoint.getManifestDigest(), manifestDigest)) { + throw new IOException("path-state rebuild checkpoint identity mismatch"); + } + } + + private static boolean sameStoreResult(PathStateRebuildCoordinator.StoreResult left, + PathStateRebuildCoordinator.StoreResult right) { + return left.getStoreId() == right.getStoreId() + && left.getDbName().equals(right.getDbName()) + && left.getEntryCount() == right.getEntryCount() + && Arrays.equals(left.getInputDigest(), right.getInputDigest()) + && Arrays.equals(left.getStoreRoot(), right.getStoreRoot()); + } + private List restoredLeafRecords() { List records = new ArrayList<>(persistedLeaves.size()); for (Map.Entry entry : persistedLeaves.entrySet()) { @@ -434,6 +553,11 @@ private static Long decodeLogicalBytes(byte[] encoded) throws IOException { return value; } + private static PathStateRebuildCheckpoint decodeRebuildCheckpoint(byte[] encoded) + throws IOException { + return encoded == null ? null : PathStateRebuildCheckpoint.decode(encoded); + } + private static long replaceLogicalEntry(long total, byte[] key, byte[] previous, byte[] next) throws IOException { try { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCheckpoint.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCheckpoint.java new file mode 100644 index 00000000000..d7bf912e525 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCheckpoint.java @@ -0,0 +1,192 @@ +package org.tron.core.db2.stateroot; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.StoreResult; + +/** Durable, non-authoritative per-Store rebuild checkpoint stored inside the BASE native DB. */ +final class PathStateRebuildCheckpoint { + + private static final int MAGIC = 0x50535243; // PSRC + private static final short VERSION = 1; + private static final int MAX_LENGTH = 64 * 1024; + + private final byte[] manifestDigest; + private final byte[] sourceIdentityDigest; + private final SnapshotIdentity identity; + private final List completedStores; + private final byte[] partialRoot; + + PathStateRebuildCheckpoint(byte[] manifestDigest, byte[] sourceIdentityDigest, + SnapshotIdentity identity, List completedStores, byte[] partialRoot) { + this.manifestDigest = copy32(manifestDigest, "manifestDigest"); + this.sourceIdentityDigest = copy32(sourceIdentityDigest, "sourceIdentityDigest"); + this.identity = Objects.requireNonNull(identity, "identity"); + this.completedStores = validateStores(completedStores); + this.partialRoot = copy32(partialRoot, "partialRoot"); + } + + byte[] getManifestDigest() { + return Arrays.copyOf(manifestDigest, manifestDigest.length); + } + + byte[] getSourceIdentityDigest() { + return Arrays.copyOf(sourceIdentityDigest, sourceIdentityDigest.length); + } + + SnapshotIdentity getIdentity() { + return identity; + } + + List getCompletedStores() { + return completedStores; + } + + byte[] getPartialRoot() { + return Arrays.copyOf(partialRoot, partialRoot.length); + } + + byte[] encode() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(0); + output.write(manifestDigest); + output.write(sourceIdentityDigest); + output.writeLong(identity.getBlockNumber()); + output.write(identity.getBlockHash()); + output.write(identity.getParentHash()); + output.writeLong(identity.getTimestamp()); + output.writeByte(identity.getPhase().ordinal()); + output.writeByte(completedStores.size()); + for (StoreResult store : completedStores) { + output.writeInt(store.getStoreId()); + writeString(output, store.getDbName()); + output.writeLong(store.getEntryCount()); + output.write(store.getInputDigest()); + output.write(store.getStoreRoot()); + } + output.write(partialRoot); + output.flush(); + byte[] payload = bytes.toByteArray(); + ByteBuffer.wrap(payload).putInt(8, payload.length + Integer.BYTES); + bytes.reset(); + output = new DataOutputStream(bytes); + output.write(payload); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory rebuild checkpoint encoding failed", impossible); + } + } + + static PathStateRebuildCheckpoint decode(byte[] encoded) throws IOException { + byte[] bytes = Arrays.copyOf(Objects.requireNonNull(encoded, "encoded"), encoded.length); + if (bytes.length <= Integer.BYTES || bytes.length > MAX_LENGTH) { + throw new IOException("path-state rebuild checkpoint length is invalid"); + } + byte[] payload = Arrays.copyOf(bytes, bytes.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(bytes, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new IOException("path-state rebuild checkpoint checksum mismatch"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + || input.readInt() != bytes.length) { + throw new IOException("unsupported path-state rebuild checkpoint header"); + } + byte[] manifestDigest = read32(input); + byte[] sourceIdentityDigest = read32(input); + long blockNumber = input.readLong(); + byte[] blockHash = read32(input); + byte[] parentHash = read32(input); + long timestamp = input.readLong(); + int phaseTag = input.readUnsignedByte(); + P66Phase[] phases = P66Phase.values(); + if (phaseTag >= phases.length) { + throw new IOException("path-state rebuild checkpoint phase is invalid"); + } + SnapshotIdentity identity = new SnapshotIdentity(blockNumber, blockHash, parentHash, + timestamp, phases[phaseTag]); + int completed = input.readUnsignedByte(); + List stores = new ArrayList<>(completed); + for (int index = 0; index < completed; index++) { + stores.add(StoreResult.restore(input.readInt(), readString(input), input.readLong(), + read32(input), read32(input))); + } + byte[] partialRoot = read32(input); + if (input.available() != Integer.BYTES) { + throw new IOException("path-state rebuild checkpoint payload mismatch"); + } + return new PathStateRebuildCheckpoint(manifestDigest, sourceIdentityDigest, identity, stores, + partialRoot); + } catch (IllegalArgumentException invalid) { + throw new IOException("path-state rebuild checkpoint is invalid", invalid); + } + } + + private static List validateStores(List stores) { + List supplied = new ArrayList<>(Objects.requireNonNull(stores, "stores")); + List expected = + PathStateParticipantDescriptor.current().getStores(); + if (supplied.size() > expected.size() || supplied.contains(null)) { + throw new IllegalArgumentException("rebuild checkpoint Store count is invalid"); + } + for (int index = 0; index < supplied.size(); index++) { + StoreResult actual = supplied.get(index); + PathStateParticipantDescriptor.StoreIdentity participant = expected.get(index); + if (actual.getStoreId() != participant.getStoreId() + || !actual.getDbName().equals(participant.getDbName())) { + throw new IllegalArgumentException("rebuild checkpoint Store order is invalid"); + } + } + return Collections.unmodifiableList(supplied); + } + + private static byte[] copy32(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length != PathStateRootMetadata.DIGEST_LENGTH) { + throw new IllegalArgumentException(name + " must be exactly 32 bytes"); + } + return copy; + } + + private static byte[] read32(DataInputStream input) throws IOException { + byte[] value = new byte[PathStateRootMetadata.DIGEST_LENGTH]; + input.readFully(value); + return value; + } + + private static void writeString(DataOutputStream output, String value) throws IOException { + byte[] encoded = Objects.requireNonNull(value, "value").getBytes(StandardCharsets.UTF_8); + output.writeShort(encoded.length); + output.write(encoded); + } + + private static String readString(DataInputStream input) throws IOException { + int length = input.readUnsignedShort(); + if (length == 0 || length > 1024 || length > input.available() - Integer.BYTES) { + throw new IOException("path-state rebuild checkpoint string is invalid"); + } + byte[] value = new byte[length]; + input.readFully(value); + return new String(value, StandardCharsets.UTF_8); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index b8b302fe9a9..40cd5300139 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -25,10 +25,16 @@ public final class PathStateRebuildCoordinator { private final PathStateParticipantDescriptor descriptor; private final PathStateCanonicalizer canonicalizer; + private final FaultHook faultHook; public PathStateRebuildCoordinator() { + this(store -> { }); + } + + PathStateRebuildCoordinator(FaultHook faultHook) { descriptor = PathStateParticipantDescriptor.current(); canonicalizer = new PathStateCanonicalizer(); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); } /** @@ -43,6 +49,8 @@ public RebuildResult rebuild(PathStateStoreManifest manifest, SnapshotSource sou PathStateStoreManifest admittedManifest = Objects.requireNonNull(manifest, "manifest"); SnapshotSource admittedSource = Objects.requireNonNull(source, "source"); SnapshotIdentity identity = Objects.requireNonNull(admittedSource.identity(), "identity"); + byte[] sourceIdentityDigest = SnapshotIdentity.copy32( + admittedSource.sourceIdentityDigest(), "sourceIdentityDigest"); descriptor.requireExactDatabases(admittedSource.databases()); admittedSource.verifyIdentity(identity); @@ -53,20 +61,41 @@ public RebuildResult rebuild(PathStateStoreManifest manifest, SnapshotSource sou try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(admittedManifest)) { PathStateRoot root = stores.createRoot(); - List storeResults = new ArrayList<>(); + PathStateRebuildCheckpoint checkpoint = stores.getRebuildCheckpoint(); + List storeResults = checkpoint == null ? new ArrayList<>() + : new ArrayList<>(checkpoint.getCompletedStores()); + if (checkpoint != null && !identity.sameAs(checkpoint.getIdentity())) { + throw new IOException("path-state rebuild checkpoint snapshot identity mismatch"); + } + if (checkpoint != null && !Arrays.equals(admittedManifest.getIdentityDigest(), + checkpoint.getManifestDigest())) { + throw new IOException("path-state rebuild checkpoint manifest identity mismatch"); + } + if (checkpoint != null && !Arrays.equals(sourceIdentityDigest, + checkpoint.getSourceIdentityDigest())) { + throw new IOException("path-state rebuild checkpoint source identity mismatch"); + } long totalEntries = 0; - for (StoreIdentity store : descriptor.getStores()) { + for (StoreResult completed : storeResults) { + totalEntries = Math.addExact(totalEntries, completed.getEntryCount()); + } + for (int index = storeResults.size(); index < descriptor.getStores().size(); index++) { + StoreIdentity store = descriptor.getStores().get(index); StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root, admittedSource); admittedSource.scan(store.getDbName(), accumulator::accept); StoreResult result = accumulator.finish(); storeResults.add(result); totalEntries = Math.addExact(totalEntries, result.getEntryCount()); + checkpoint = new PathStateRebuildCheckpoint(admittedManifest.getIdentityDigest(), + sourceIdentityDigest, identity, storeResults, root.rootHash()); + stores.checkpointRebuild(checkpoint); + faultHook.afterStore(result); } admittedSource.verifyIdentity(identity); byte[] stateRoot = root.rootHash(); - byte[] sourceDigest = sourceDigest(identity, storeResults, stateRoot); + byte[] sourceDigest = sourceDigest(identity, sourceIdentityDigest, storeResults, stateRoot); PathStateRootMetadata metadata = PathStateRootMetadata.base(identity.getBlockNumber(), identity.getBlockHash(), identity.getParentHash(), identity.getTimestamp(), identity.getPhase(), admittedManifest.getIdentityDigest(), stateRoot, sourceDigest); @@ -78,14 +107,15 @@ public RebuildResult rebuild(PathStateStoreManifest manifest, SnapshotSource sou } } - private byte[] sourceDigest(SnapshotIdentity identity, List stores, - byte[] stateRoot) { + private byte[] sourceDigest(SnapshotIdentity identity, byte[] sourceIdentityDigest, + List stores, byte[] stateRoot) { Hasher hasher = domainHasher(SOURCE_DIGEST_DOMAIN); putLong(hasher, identity.getBlockNumber()); putBytes(hasher, identity.getBlockHash()); putBytes(hasher, identity.getParentHash()); putLong(hasher, identity.getTimestamp()); putInt(hasher, identity.getPhase().ordinal()); + putBytes(hasher, sourceIdentityDigest); putInt(hasher, stores.size()); for (StoreResult store : stores) { putInt(hasher, store.getStoreId()); @@ -213,6 +243,9 @@ public interface SnapshotSource { Collection databases(); + /** Stable identity of the exact physical Store generations held by this snapshot. */ + byte[] sourceIdentityDigest(); + /** Returns one value from the same pinned snapshot, or {@code null} when physically absent. */ byte[] get(String dbName, byte[] physicalKey) throws IOException; @@ -228,6 +261,12 @@ public interface EntryConsumer { void accept(byte[] physicalKey, byte[] rawValue) throws IOException; } + @FunctionalInterface + interface FaultHook { + + void afterStore(StoreResult store) throws IOException; + } + /** Immutable canonical block boundary shared by every Store snapshot in one rebuild. */ public static final class SnapshotIdentity { @@ -295,11 +334,19 @@ public static final class StoreResult { private StoreResult(int storeId, String dbName, long entryCount, byte[] inputDigest, byte[] storeRoot) { + if (storeId <= 0 || entryCount < 0) { + throw new IllegalArgumentException("rebuild Store result identity is invalid"); + } this.storeId = storeId; - this.dbName = dbName; + this.dbName = Objects.requireNonNull(dbName, "dbName"); this.entryCount = entryCount; - this.inputDigest = Arrays.copyOf(inputDigest, inputDigest.length); - this.storeRoot = Arrays.copyOf(storeRoot, storeRoot.length); + this.inputDigest = SnapshotIdentity.copy32(inputDigest, "inputDigest"); + this.storeRoot = SnapshotIdentity.copy32(storeRoot, "storeRoot"); + } + + static StoreResult restore(int storeId, String dbName, long entryCount, byte[] inputDigest, + byte[] storeRoot) { + return new StoreResult(storeId, dbName, entryCount, inputDigest, storeRoot); } public int getStoreId() { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index 02622671b0a..e7666274899 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -17,6 +18,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -26,6 +28,7 @@ import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.RebuildResult; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotSource; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.StoreResult; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; import org.tron.protos.Protocol.Account; @@ -178,6 +181,104 @@ public void rejectsMixedOrOrphanAccountAssetSnapshotWithoutPublication() throws assertFalse(new PathStateCurrentStore(offDirectManifest).isInitialized()); } + @Test + public void resumesCompletedStoresWhileKeepingGenerationInvisible() throws Exception { + PathStateStoreManifest manifest = manifest("resume", Engine.ROCKSDB); + TestSnapshotSource first = exactSource(identity()); + first.add("proposal", new byte[]{1}, new byte[]{2}); + AtomicBoolean failed = new AtomicBoolean(); + PathStateRebuildCoordinator interrupted = new PathStateRebuildCoordinator(store -> { + if ("account".equals(store.getDbName()) && failed.compareAndSet(false, true)) { + throw new IOException("injected rebuild interruption"); + } + }); + + assertThrows(IOException.class, () -> interrupted.rebuild(manifest, first)); + assertFalse(new PathStateCurrentStore(manifest).isInitialized()); + assertFalse(Files.exists(manifest.getBaseDirectory() + .resolve(PathStateCurrentStore.METADATA_FILE))); + assertNull(PathStateNodeStoreSet.loadProgress(manifest.getBaseDirectory(), manifest)); + + TestSnapshotSource resumed = exactSource(identity()); + resumed.add("proposal", new byte[]{1}, new byte[]{2}); + RebuildResult result = new PathStateRebuildCoordinator().rebuild(manifest, resumed); + + assertEquals(27, result.getStores().size()); + assertEquals(0, resumed.getScanCount("abi")); + assertEquals(0, resumed.getScanCount("accountid-index")); + assertEquals(0, resumed.getScanCount("account-index")); + assertEquals(0, resumed.getScanCount("account")); + assertEquals(1, resumed.getScanCount("account-asset")); + assertEquals(1, resumed.getScanCount("proposal")); + assertTrue(new PathStateCurrentStore(manifest).isInitialized()); + + PathStateStoreManifest freshManifest = manifest("resume-fresh", Engine.ROCKSDB); + TestSnapshotSource fresh = exactSource(identity()); + fresh.add("proposal", new byte[]{1}, new byte[]{2}); + RebuildResult freshResult = new PathStateRebuildCoordinator().rebuild(freshManifest, fresh); + assertArrayEquals(freshResult.getMetadata().getStateRoot(), + result.getMetadata().getStateRoot()); + assertArrayEquals(freshResult.getSourceDigest(), result.getSourceDigest()); + try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openCurrent(manifest)) { + assertNull(reopened.getRebuildCheckpoint()); + assertArrayEquals(result.getMetadata().getStateRoot(), reopened.createRoot().rootHash()); + } + } + + @Test + public void rejectsResumeAgainstAnotherSnapshotIdentity() throws Exception { + PathStateStoreManifest manifest = manifest("resume-identity", Engine.ROCKSDB); + TestSnapshotSource first = exactSource(identity()); + PathStateRebuildCoordinator interrupted = new PathStateRebuildCoordinator(store -> { + throw new IOException("stop after first Store"); + }); + assertThrows(IOException.class, () -> interrupted.rebuild(manifest, first)); + + SnapshotIdentity other = new SnapshotIdentity(101, bytes(3), bytes(4), 301, + P66Phase.P66_ON); + IOException failure = assertThrows(IOException.class, + () -> new PathStateRebuildCoordinator().rebuild(manifest, exactSource(other))); + assertTrue(failure.getMessage().contains("checkpoint snapshot identity mismatch")); + assertFalse(new PathStateCurrentStore(manifest).isInitialized()); + } + + @Test + public void rejectsResumeAgainstReplacedPhysicalSources() throws Exception { + PathStateStoreManifest manifest = manifest("resume-source", Engine.ROCKSDB); + TestSnapshotSource first = exactSource(identity()); + PathStateRebuildCoordinator interrupted = new PathStateRebuildCoordinator(store -> { + throw new IOException("stop after first Store"); + }); + assertThrows(IOException.class, () -> interrupted.rebuild(manifest, first)); + + TestSnapshotSource replacement = exactSource(identity()); + replacement.setSourceIdentityDigest(bytes(43)); + IOException failure = assertThrows(IOException.class, + () -> new PathStateRebuildCoordinator().rebuild(manifest, replacement)); + assertTrue(failure.getMessage().contains("checkpoint source identity mismatch")); + assertFalse(new PathStateCurrentStore(manifest).isInitialized()); + } + + @Test + public void rebuildCheckpointCodecRejectsCorruptionAndNonPrefixStores() throws Exception { + StoreResult abi = StoreResult.restore(1, "abi", 2, bytes(5), bytes(6)); + PathStateRebuildCheckpoint checkpoint = new PathStateRebuildCheckpoint(bytes(7), bytes(11), + identity(), Collections.singletonList(abi), bytes(8)); + PathStateRebuildCheckpoint decoded = PathStateRebuildCheckpoint.decode(checkpoint.encode()); + assertArrayEquals(checkpoint.getManifestDigest(), decoded.getManifestDigest()); + assertArrayEquals(checkpoint.getSourceIdentityDigest(), decoded.getSourceIdentityDigest()); + assertTrue(checkpoint.getIdentity().sameAs(decoded.getIdentity())); + assertEquals(1, decoded.getCompletedStores().size()); + assertArrayEquals(checkpoint.getPartialRoot(), decoded.getPartialRoot()); + + byte[] corrupt = checkpoint.encode(); + corrupt[corrupt.length - 1] ^= 1; + assertThrows(IOException.class, () -> PathStateRebuildCheckpoint.decode(corrupt)); + StoreResult wrongFirst = StoreResult.restore(2, "accountid-index", 0, bytes(9), bytes(10)); + assertThrows(IllegalArgumentException.class, () -> new PathStateRebuildCheckpoint(bytes(7), + bytes(11), identity(), Collections.singletonList(wrongFirst), bytes(8))); + } + private PathStateStoreManifest manifest(String name, Engine engine) throws IOException { Path directory = temporaryFolder.newFolder(name).toPath(); return PathStateStoreManifest.createOrOpen(directory, engine); @@ -237,6 +338,8 @@ private static final class TestSnapshotSource implements SnapshotSource { private final SnapshotIdentity identity; private final Map> stores; + private final Map scanCounts = new LinkedHashMap<>(); + private byte[] sourceIdentityDigest = bytes(42); private int verificationCount; private boolean drift; @@ -261,6 +364,15 @@ private int getVerificationCount() { return verificationCount; } + private int getScanCount(String dbName) { + Integer count = scanCounts.get(dbName); + return count == null ? 0 : count; + } + + private void setSourceIdentityDigest(byte[] digest) { + sourceIdentityDigest = java.util.Arrays.copyOf(digest, digest.length); + } + @Override public SnapshotIdentity identity() { return identity; @@ -273,6 +385,11 @@ public Collection databases() { return names; } + @Override + public byte[] sourceIdentityDigest() { + return java.util.Arrays.copyOf(sourceIdentityDigest, sourceIdentityDigest.length); + } + @Override public byte[] get(String dbName, byte[] physicalKey) { for (Row row : stores.get(dbName)) { @@ -285,6 +402,7 @@ public byte[] get(String dbName, byte[] physicalKey) { @Override public void scan(String dbName, EntryConsumer consumer) throws IOException { + scanCounts.put(dbName, getScanCount(dbName) + 1); for (Row row : stores.get(dbName)) { consumer.accept(row.key, row.value); } From 001579ac92057baecc4479bd4b92504852ff9ad5 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 10:49:34 +0800 Subject: [PATCH 070/161] feat(chainbase): add path state catch-up queue --- .../db2/stateroot/PathStateCatchUpQueue.java | 270 ++++++++++++++++++ .../PathStateRebuildCoordinator.java | 30 +- .../PathStateCurrentOnlyContractTest.java | 1 + .../PathStateRebuildCoordinatorTest.java | 100 +++++++ 4 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCatchUpQueue.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCatchUpQueue.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCatchUpQueue.java new file mode 100644 index 00000000000..02cfa78d178 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCatchUpQueue.java @@ -0,0 +1,270 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; +import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; + +/** + * Bounded handoff from a fixed rebuild snapshot to normal block-final path-state processing. + * + *

Transitions captured while BASE(P0) is rebuilding remain in memory and must form one exact + * block/hash chain above P0. BASE publication and the switch to draining share one synchronized + * gate, so capture cannot pass through that boundary unnoticed. Once the queue becomes READY, + * callers must send later transitions through the normal direct-apply path. + */ +public final class PathStateCatchUpQueue { + + private final SnapshotIdentity snapshot; + private final int maxTransitions; + private final long maxMutations; + private final long maxBytes; + private final DrainHook drainHook; + private final Deque transitions = new ArrayDeque<>(); + private State state = State.CAPTURING; + private long queuedMutations; + private long queuedBytes; + private PathStateRootMetadata readyHead; + + public PathStateCatchUpQueue(SnapshotIdentity snapshot, int maxTransitions, + long maxMutations, long maxBytes) { + this(snapshot, maxTransitions, maxMutations, maxBytes, transition -> { }); + } + + PathStateCatchUpQueue(SnapshotIdentity snapshot, int maxTransitions, + long maxMutations, long maxBytes, DrainHook drainHook) { + this.snapshot = Objects.requireNonNull(snapshot, "snapshot"); + if (maxTransitions <= 0 || maxMutations <= 0 || maxBytes <= 0) { + throw new IllegalArgumentException("path-state catch-up limits must be positive"); + } + this.maxTransitions = maxTransitions; + this.maxMutations = maxMutations; + this.maxBytes = maxBytes; + this.drainHook = Objects.requireNonNull(drainHook, "drainHook"); + } + + /** Queues a rebuilding transition, or explicitly returns it to the post-READY direct path. */ + public synchronized CaptureDisposition capture(PathStateBlockTransition transition) + throws IOException { + PathStateBlockTransition admitted = Objects.requireNonNull(transition, "transition"); + if (state == State.FAILED) { + throw new IOException("path-state catch-up queue has failed"); + } + if (state == State.READY) { + return CaptureDisposition.DIRECT_APPLY; + } + requireNext(admitted); + long nextMutations; + long nextBytes; + try { + nextMutations = Math.addExact(queuedMutations, admitted.getMutations().size()); + nextBytes = Math.addExact(queuedBytes, logicalBytes(admitted)); + } catch (ArithmeticException overflow) { + return failOverflow(overflow); + } + if (transitions.size() >= maxTransitions || nextMutations > maxMutations + || nextBytes > maxBytes) { + return failOverflow(null); + } + transitions.addLast(admitted); + queuedMutations = nextMutations; + queuedBytes = nextBytes; + return CaptureDisposition.QUEUED; + } + + public synchronized State getState() { + return state; + } + + public synchronized int getQueuedTransitions() { + return transitions.size(); + } + + public synchronized long getQueuedMutations() { + return queuedMutations; + } + + public synchronized long getQueuedBytes() { + return queuedBytes; + } + + public synchronized PathStateRootMetadata getReadyHead() { + return readyHead; + } + + synchronized void admitSnapshot(SnapshotIdentity identity) throws IOException { + requireCapturing(); + if (!snapshot.sameAs(Objects.requireNonNull(identity, "identity"))) { + state = State.FAILED; + throw new IOException("path-state catch-up snapshot identity mismatch"); + } + } + + synchronized PathStateRootMetadata publishBase(SnapshotIdentity identity, + PathStateRootMetadata base, BasePublisher publisher) throws IOException { + requireCapturing(); + requireBase(identity, base); + try { + PathStateRootMetadata published = Objects.requireNonNull( + publisher.publish(), "published BASE"); + if (!Arrays.equals(base.encode(), published.encode())) { + throw new IOException("path-state catch-up published BASE identity mismatch"); + } + state = State.DRAINING; + readyHead = published; + return published; + } catch (IOException | RuntimeException failure) { + state = State.FAILED; + throw failure; + } + } + + PathStateRootMetadata drain(PathStateStoreManifest manifest, PathStateLayerLimits limits) + throws IOException { + PathStateStoreManifest admittedManifest = Objects.requireNonNull(manifest, "manifest"); + PathStateLayerLimits admittedLimits = Objects.requireNonNull(limits, "limits"); + while (true) { + PathStateBlockTransition transition; + PathStateRootMetadata parent; + synchronized (this) { + if (state == State.FAILED) { + throw new IOException("path-state catch-up queue has failed"); + } + if (state == State.READY) { + return readyHead; + } + if (state != State.DRAINING) { + throw new IOException("path-state catch-up BASE is not published"); + } + transition = transitions.peekFirst(); + if (transition == null) { + state = State.READY; + return readyHead; + } + parent = readyHead; + } + try { + PathStateRootMetadata committed; + try (PathStateLayer layer = PathStateLayer.begin(admittedManifest, parent, + transition.getBlockNumber(), transition.getBlockHash(), transition.getParentHash(), + transition.getTimestamp(), transition.getPhase(), transition.getPayloadDigest(), + admittedLimits)) { + layer.apply(transition.getMutations()); + committed = layer.commit(); + } + synchronized (this) { + if (transitions.peekFirst() != transition || state != State.DRAINING) { + throw new IOException("path-state catch-up queue changed during drain"); + } + transitions.removeFirst(); + queuedMutations -= transition.getMutations().size(); + queuedBytes -= logicalBytes(transition); + readyHead = committed; + } + drainHook.afterCommit(transition); + } catch (IOException | RuntimeException failure) { + synchronized (this) { + state = State.FAILED; + } + throw failure; + } + } + } + + private void requireNext(PathStateBlockTransition transition) throws IOException { + PathStateBlockTransition tail = transitions.peekLast(); + long parentNumber; + byte[] parentHash; + if (tail != null) { + parentNumber = tail.getBlockNumber(); + parentHash = tail.getBlockHash(); + } else if (state == State.DRAINING) { + parentNumber = readyHead.getBlockNumber(); + parentHash = readyHead.getBlockHash(); + } else { + parentNumber = snapshot.getBlockNumber(); + parentHash = snapshot.getBlockHash(); + } + if (transition.getBlockNumber() != parentNumber + 1 + || !Arrays.equals(transition.getParentHash(), parentHash)) { + state = State.FAILED; + throw new IOException("path-state catch-up transition is not block/hash continuous"); + } + } + + private void requireCapturing() throws IOException { + if (state != State.CAPTURING) { + throw new IOException("path-state catch-up BASE publication is not admissible"); + } + } + + private void requireBase(SnapshotIdentity identity, PathStateRootMetadata base) + throws IOException { + SnapshotIdentity admittedIdentity = Objects.requireNonNull(identity, "identity"); + PathStateRootMetadata admittedBase = Objects.requireNonNull(base, "base"); + if (!snapshot.sameAs(admittedIdentity) + || admittedBase.getKind() != Kind.BASE + || admittedBase.getBlockNumber() != snapshot.getBlockNumber() + || admittedBase.getTimestamp() != snapshot.getTimestamp() + || admittedBase.getPhase() != snapshot.getPhase() + || !Arrays.equals(admittedBase.getBlockHash(), snapshot.getBlockHash()) + || !Arrays.equals(admittedBase.getParentHash(), snapshot.getParentHash())) { + state = State.FAILED; + throw new IOException("path-state catch-up snapshot and BASE identity mismatch"); + } + } + + private CaptureDisposition failOverflow(ArithmeticException cause) throws IOException { + state = State.FAILED; + IOException failure = new IOException("path-state catch-up queue limit exceeded"); + if (cause != null) { + failure.initCause(cause); + } + throw failure; + } + + private static long logicalBytes(PathStateBlockTransition transition) { + long bytes = Long.BYTES * 2L + PathStateBlockTransition.HASH_LENGTH * 2L + + Integer.BYTES; + for (PathStateMutation mutation : transition.getMutations()) { + bytes = Math.addExact(bytes, + mutation.getDbName().getBytes(StandardCharsets.UTF_8).length); + bytes = Math.addExact(bytes, mutation.getCanonicalKey().length); + byte[] value = mutation.getCanonicalValue(); + if (value != null) { + bytes = Math.addExact(bytes, value.length); + } + bytes = Math.addExact(bytes, Integer.BYTES * 3L + 1L); + } + return bytes; + } + + public enum CaptureDisposition { + QUEUED, + DIRECT_APPLY + } + + public enum State { + CAPTURING, + DRAINING, + READY, + FAILED + } + + @FunctionalInterface + interface BasePublisher { + + PathStateRootMetadata publish() throws IOException; + } + + @FunctionalInterface + interface DrainHook { + + void afterCommit(PathStateBlockTransition transition) throws IOException; + } + +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index 40cd5300139..a13b1a0ea83 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -46,8 +46,21 @@ public PathStateRebuildCoordinator() { */ public RebuildResult rebuild(PathStateStoreManifest manifest, SnapshotSource source) throws IOException { + return rebuildInternal(manifest, source, null, PathStateLayerLimits.defaults()); + } + + /** Publishes BASE(P0) through the catch-up handoff and drains every queued transition. */ + public RebuildResult rebuild(PathStateStoreManifest manifest, SnapshotSource source, + PathStateCatchUpQueue catchUpQueue, PathStateLayerLimits layerLimits) throws IOException { + return rebuildInternal(manifest, source, + Objects.requireNonNull(catchUpQueue, "catchUpQueue"), layerLimits); + } + + private RebuildResult rebuildInternal(PathStateStoreManifest manifest, SnapshotSource source, + PathStateCatchUpQueue catchUpQueue, PathStateLayerLimits layerLimits) throws IOException { PathStateStoreManifest admittedManifest = Objects.requireNonNull(manifest, "manifest"); SnapshotSource admittedSource = Objects.requireNonNull(source, "source"); + PathStateLayerLimits admittedLimits = Objects.requireNonNull(layerLimits, "layerLimits"); SnapshotIdentity identity = Objects.requireNonNull(admittedSource.identity(), "identity"); byte[] sourceIdentityDigest = SnapshotIdentity.copy32( admittedSource.sourceIdentityDigest(), "sourceIdentityDigest"); @@ -58,7 +71,11 @@ public RebuildResult rebuild(PathStateStoreManifest manifest, SnapshotSource sou if (currentStore.isInitialized()) { throw new IOException("path-state rebuild requires an uninitialized current store"); } + if (catchUpQueue != null) { + catchUpQueue.admitSnapshot(identity); + } + RebuildResult rebuildResult; try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(admittedManifest)) { PathStateRoot root = stores.createRoot(); PathStateRebuildCheckpoint checkpoint = stores.getRebuildCheckpoint(); @@ -99,12 +116,19 @@ public RebuildResult rebuild(PathStateStoreManifest manifest, SnapshotSource sou PathStateRootMetadata metadata = PathStateRootMetadata.base(identity.getBlockNumber(), identity.getBlockHash(), identity.getParentHash(), identity.getTimestamp(), identity.getPhase(), admittedManifest.getIdentityDigest(), stateRoot, sourceDigest); - PathStateRootMetadata published = - new PathStateBasePublication(admittedManifest).publish(stores, metadata); - return new RebuildResult(published, storeResults, totalEntries, sourceDigest); + PathStateBasePublication publication = new PathStateBasePublication(admittedManifest); + PathStateRootMetadata published = catchUpQueue == null + ? publication.publish(stores, metadata) + : catchUpQueue.publishBase(identity, metadata, + () -> publication.publish(stores, metadata)); + rebuildResult = new RebuildResult(published, storeResults, totalEntries, sourceDigest); } catch (ArithmeticException overflow) { throw new IOException("path-state rebuild entry count overflow", overflow); } + if (catchUpQueue != null) { + catchUpQueue.drain(admittedManifest, admittedLimits); + } + return rebuildResult; } private byte[] sourceDigest(SnapshotIdentity identity, byte[] sourceIdentityDigest, diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentOnlyContractTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentOnlyContractTest.java index 015ed0016bc..d1237dec953 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentOnlyContractTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCurrentOnlyContractTest.java @@ -26,6 +26,7 @@ public class PathStateCurrentOnlyContractTest { private static final Class[] DURABLE_API = new Class[]{ PathStateBasePublication.class, + PathStateCatchUpQueue.class, PathStateCurrentStore.class, PathStateLayer.class, PathStateLayerLimits.class, diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index e7666274899..e463181e163 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -279,6 +280,99 @@ public void rebuildCheckpointCodecRejectsCorruptionAndNonPrefixStores() throws E bytes(11), identity(), Collections.singletonList(wrongFirst), bytes(8))); } + @Test + public void publishesBaseThenDrainsBlockHashContinuousCatchUp() throws Exception { + PathStateStoreManifest manifest = manifest("catch-up", Engine.ROCKSDB); + PathStateCatchUpQueue queue = new PathStateCatchUpQueue(identity(), 4, 8, 1L << 20); + PathStateBlockTransition first = transition(101, bytes(20), bytes(1), + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2})); + PathStateBlockTransition second = transition(102, bytes(21), bytes(20), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4})); + assertEquals(PathStateCatchUpQueue.CaptureDisposition.QUEUED, queue.capture(first)); + assertEquals(PathStateCatchUpQueue.CaptureDisposition.QUEUED, queue.capture(second)); + + RebuildResult result = new PathStateRebuildCoordinator().rebuild(manifest, + exactSource(identity()), queue, new PathStateLayerLimits(4, 1L << 30)); + + assertEquals(100, result.getMetadata().getBlockNumber()); + PathStateRootMetadata current = new PathStateCurrentStore(manifest).current(); + assertEquals(102, current.getBlockNumber()); + assertArrayEquals(second.getBlockHash(), current.getBlockHash()); + assertArrayEquals(second.getPayloadDigest(), current.getPayloadDigest()); + assertEquals(PathStateCatchUpQueue.State.READY, queue.getState()); + assertEquals(0, queue.getQueuedTransitions()); + assertEquals(0, queue.getQueuedMutations()); + assertEquals(0, queue.getQueuedBytes()); + assertEquals(PathStateCatchUpQueue.CaptureDisposition.DIRECT_APPLY, + queue.capture(transition(103, bytes(22), bytes(21), + PathStateMutation.delete("proposal", new byte[]{1})))); + } + + @Test + public void acceptsNewContinuousCaptureWhileCatchUpIsDraining() throws Exception { + PathStateStoreManifest manifest = manifest("catch-up-race", Engine.ROCKSDB); + AtomicReference queueRef = new AtomicReference<>(); + PathStateBlockTransition second = transition(102, bytes(31), bytes(30), + PathStateMutation.put("account", new byte[]{5}, new byte[]{6})); + PathStateCatchUpQueue queue = new PathStateCatchUpQueue(identity(), 4, 8, 1L << 20, + committed -> { + if (committed.getBlockNumber() == 101) { + queueRef.get().capture(second); + } + }); + queueRef.set(queue); + queue.capture(transition(101, bytes(30), bytes(1), + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}))); + + new PathStateRebuildCoordinator().rebuild(manifest, exactSource(identity()), queue, + new PathStateLayerLimits(4, 1L << 30)); + + assertEquals(PathStateCatchUpQueue.State.READY, queue.getState()); + assertEquals(102, queue.getReadyHead().getBlockNumber()); + assertEquals(102, new PathStateCurrentStore(manifest).current().getBlockNumber()); + } + + @Test + public void catchUpGapAndOverflowFailBeforeBasePublication() throws Exception { + PathStateCatchUpQueue gap = new PathStateCatchUpQueue(identity(), 2, 2, 1L << 20); + IOException gapFailure = assertThrows(IOException.class, + () -> gap.capture(transition(102, bytes(41), bytes(40), + PathStateMutation.delete("proposal", new byte[]{1})))); + assertTrue(gapFailure.getMessage().contains("block/hash continuous")); + assertEquals(PathStateCatchUpQueue.State.FAILED, gap.getState()); + + PathStateStoreManifest manifest = manifest("catch-up-overflow", Engine.ROCKSDB); + PathStateCatchUpQueue overflow = new PathStateCatchUpQueue(identity(), 1, 2, 1L << 20); + overflow.capture(transition(101, bytes(40), bytes(1), + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}))); + IOException overflowFailure = assertThrows(IOException.class, + () -> overflow.capture(transition(102, bytes(41), bytes(40), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4})))); + assertTrue(overflowFailure.getMessage().contains("limit exceeded")); + assertEquals(PathStateCatchUpQueue.State.FAILED, overflow.getState()); + assertThrows(IOException.class, () -> new PathStateRebuildCoordinator().rebuild(manifest, + exactSource(identity()), overflow, new PathStateLayerLimits(4, 1L << 30))); + assertFalse(new PathStateCurrentStore(manifest).isInitialized()); + assertFalse(Files.exists(manifest.getBaseDirectory() + .resolve(PathStateNodeStoreSet.NODES_DIRECTORY))); + + PathStateStoreManifest mismatchManifest = manifest("catch-up-identity", Engine.ROCKSDB); + SnapshotIdentity other = new SnapshotIdentity(99, bytes(50), bytes(51), 299, + P66Phase.P66_ON); + PathStateCatchUpQueue mismatch = new PathStateCatchUpQueue(other, 2, 2, 1L << 20); + IOException mismatchFailure = assertThrows(IOException.class, + () -> new PathStateRebuildCoordinator().rebuild(mismatchManifest, + exactSource(identity()), mismatch, new PathStateLayerLimits(4, 1L << 30))); + assertTrue(mismatchFailure.getMessage().contains("snapshot identity mismatch")); + assertFalse(new PathStateCurrentStore(mismatchManifest).isInitialized()); + + PathStateCatchUpQueue byteOverflow = new PathStateCatchUpQueue(identity(), 2, 2, 1); + assertThrows(IOException.class, () -> byteOverflow.capture( + transition(101, bytes(60), bytes(1), + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2})))); + assertEquals(PathStateCatchUpQueue.State.FAILED, byteOverflow.getState()); + } + private PathStateStoreManifest manifest(String name, Engine engine) throws IOException { Path directory = temporaryFolder.newFolder(name).toPath(); return PathStateStoreManifest.createOrOpen(directory, engine); @@ -301,6 +395,12 @@ private static SnapshotIdentity identity(P66Phase phase) { return new SnapshotIdentity(100, bytes(1), bytes(2), 300, phase); } + private static PathStateBlockTransition transition(long blockNumber, byte[] blockHash, + byte[] parentHash, PathStateMutation mutation) { + return new PathStateBlockTransition(blockNumber, blockHash, parentHash, + 300 + blockNumber, P66Phase.P66_ON, Collections.singletonList(mutation)); + } + private static Engine[] availableEngines() { return Arch.isArm64() ? new Engine[]{Engine.ROCKSDB} : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; From c6e587600c5561726658e415c30aab393ac2c682 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 12:29:36 +0800 Subject: [PATCH 071/161] perf(trie): update path state nodes locally --- .../core/db2/stateroot/PathMerkleTrie.java | 459 ++++++++++++++---- .../db2/stateroot/PathMerkleTrieTest.java | 25 + 2 files changed, 392 insertions(+), 92 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index 57d2ec9bc85..c296e9f0411 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -5,23 +5,15 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.TreeMap; import org.tron.common.crypto.Hash; -/** - * Backend-neutral secure-key Merkle Patricia trie with path-addressed node persistence. - * - *

This TASK-016 P1 core deliberately owns no database, block, history, or recovery lifecycle. - * It rebuilds the canonical node set from current leaves when committed, then reconciles that set - * through {@link PathNodeStore}. The later durable backend can replace the rebuild strategy without - * changing the node byte contract. - */ +/** Backend-neutral secure-key MPT with path-local immutable node updates. */ public final class PathMerkleTrie { public static final int SECURE_KEY_LENGTH = 32; @@ -29,23 +21,25 @@ public final class PathMerkleTrie { private static final byte[] EMPTY_PATH = new byte[0]; private static final byte[] EMPTY_RLP_ITEM = new byte[]{(byte) 0x80}; private static final Comparator UNSIGNED_KEY_COMPARATOR = (left, right) -> { - byte[] leftBytes = left.bytes; - byte[] rightBytes = right.bytes; - int length = Math.min(leftBytes.length, rightBytes.length); + int length = Math.min(left.bytes.length, right.bytes.length); for (int i = 0; i < length; i++) { - int compared = Integer.compare(leftBytes[i] & 0xff, rightBytes[i] & 0xff); + int compared = Integer.compare(left.bytes[i] & 0xff, right.bytes[i] & 0xff); if (compared != 0) { return compared; } } - return Integer.compare(leftBytes.length, rightBytes.length); + return Integer.compare(left.bytes.length, right.bytes.length); }; private final PathNodeStore nodeStore; private final Map leaves = new TreeMap<>(UNSIGNED_KEY_COMPARATOR); - private final Set committedPaths = new LinkedHashSet<>(); + private final IdentityHashMap materializedNodes = new IdentityHashMap<>(); + private Node rootNode; + private Node materializedRoot; private byte[] rootHash = Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length); private boolean dirty; + private int lastNodePuts; + private int lastNodeDeletes; public PathMerkleTrie(PathNodeStore nodeStore) { this.nodeStore = Objects.requireNonNull(nodeStore, "nodeStore"); @@ -55,11 +49,18 @@ public synchronized void put(byte[] secureKey, byte[] encodedValue) { BytesKey key = secureKey(secureKey); byte[] value = nonEmpty(encodedValue, "encodedValue"); byte[] previous = leaves.put(key, value); - dirty |= !Arrays.equals(previous, value); + if (!Arrays.equals(previous, value)) { + rootNode = update(rootNode, toNibbles(key.bytes), 0, value); + dirty = true; + } } public synchronized void delete(byte[] secureKey) { - dirty |= leaves.remove(secureKey(secureKey)) != null; + BytesKey key = secureKey(secureKey); + if (leaves.remove(key) != null) { + rootNode = update(rootNode, toNibbles(key.bytes), 0, null); + dirty = true; + } } public synchronized byte[] get(byte[] secureKey) { @@ -67,7 +68,7 @@ public synchronized byte[] get(byte[] secureKey) { return value == null ? null : Arrays.copyOf(value, value.length); } - /** Reconciles path-addressed nodes and returns the canonical root hash. */ + /** Reconciles only structurally changed paths and returns the canonical root hash. */ public synchronized byte[] rootHash() { if (dirty) { commit(); @@ -79,6 +80,14 @@ public synchronized int size() { return leaves.size(); } + synchronized int getLastNodePuts() { + return lastNodePuts; + } + + synchronized int getLastNodeDeletes() { + return lastNodeDeletes; + } + synchronized List leafEntries() { List entries = new ArrayList<>(leaves.size()); for (Map.Entry entry : leaves.entrySet()) { @@ -90,6 +99,7 @@ synchronized List leafEntries() { /** Initializes an empty trie from canonical leaves and writes its complete path-node set. */ synchronized void initializeLeaves(Collection entries) { importLeaves(entries, "initialized"); + rootNode = buildTree(); dirty = true; rootHash(); } @@ -97,18 +107,20 @@ synchronized void initializeLeaves(Collection entries) { /** Restores current leaves and verifies their complete path-node set without repairing it. */ synchronized void restoreLeaves(Collection entries) { importLeaves(entries, "restored"); - Map expectedNodes = buildCurrentNodes(); + rootNode = buildTree(); + Map expectedNodes = collectNodes(rootNode); for (Map.Entry entry : expectedNodes.entrySet()) { if (!Arrays.equals(nodeStore.get(entry.getKey().bytes), entry.getValue())) { throw new IllegalStateException("restored leaves do not match persisted path nodes"); } } - committedPaths.addAll(expectedNodes.keySet()); - rootHash = rootHash(expectedNodes); + materializedRoot = rootNode; + indexNodes(rootNode, EMPTY_PATH, materializedNodes); + rootHash = hash(rootNode); } private void importLeaves(Collection entries, String operation) { - if (!leaves.isEmpty() || !committedPaths.isEmpty() || dirty) { + if (!leaves.isEmpty() || rootNode != null || materializedRoot != null || dirty) { throw new IllegalStateException("path trie is not empty before leaf " + operation); } for (LeafEntry entry : Objects.requireNonNull(entries, "entries")) { @@ -120,90 +132,102 @@ private void importLeaves(Collection entries, String operation) { } } - /** Verifies every path owned by the current committed node set without repairing corruption. */ + /** Verifies every path owned by the current materialized node set without repairing it. */ public synchronized void verifyNodeStore() { if (dirty) { throw new IllegalStateException("cannot verify a dirty path trie"); } - Map expectedNodes = buildCurrentNodes(); - if (!committedPaths.equals(expectedNodes.keySet())) { - throw new IllegalStateException("committed path set does not match current leaves"); - } - byte[] expectedRoot = rootHash(expectedNodes); - if (!Arrays.equals(rootHash, expectedRoot)) { - throw new IllegalStateException("committed path root does not match current leaves"); + Map expectedNodes = collectNodes(rootNode); + if (materializedNodes.size() != expectedNodes.size()) { + throw new IllegalStateException("materialized path set does not match current leaves"); } for (Map.Entry entry : expectedNodes.entrySet()) { if (!Arrays.equals(nodeStore.get(entry.getKey().bytes), entry.getValue())) { - throw new IllegalStateException("missing or corrupt committed path node"); + throw new IllegalStateException("missing or corrupt materialized path node"); } } + if (!Arrays.equals(rootHash, hash(rootNode))) { + throw new IllegalStateException("materialized path root does not match current leaves"); + } } private void commit() { - Map nextNodes = buildCurrentNodes(); - rootHash = rootHash(nextNodes); - - Set stalePaths = new LinkedHashSet<>(committedPaths); - stalePaths.removeAll(nextNodes.keySet()); - for (BytesKey stalePath : stalePaths) { - nodeStore.delete(stalePath.copy()); - } - for (Map.Entry entry : nextNodes.entrySet()) { - byte[] existing = nodeStore.get(entry.getKey().bytes); - if (!Arrays.equals(existing, entry.getValue())) { - nodeStore.put(entry.getKey().copy(), Arrays.copyOf(entry.getValue(), entry.getValue().length)); - } - } - committedPaths.clear(); - committedPaths.addAll(nextNodes.keySet()); + IdentityHashMap retained = new IdentityHashMap<>(); + List additions = new ArrayList<>(); + collectAdditions(rootNode, EMPTY_PATH, retained, additions); + List removals = new ArrayList<>(); + collectRemovals(materializedRoot, retained, removals); + + for (NodePath removal : removals) { + nodeStore.delete(removal.path); + materializedNodes.remove(removal.node); + } + for (NodePath addition : additions) { + nodeStore.put(addition.path, addition.node.encoded); + materializedNodes.put(addition.node, new BytesKey(addition.path)); + } + lastNodeDeletes = removals.size(); + lastNodePuts = additions.size(); + materializedRoot = rootNode; + rootHash = hash(rootNode); dirty = false; } - private Map buildCurrentNodes() { - Map nodes = new LinkedHashMap<>(); - if (!leaves.isEmpty()) { - List entries = new ArrayList<>(leaves.size()); - for (Map.Entry entry : leaves.entrySet()) { - entries.add(new Leaf(toNibbles(entry.getKey().bytes), entry.getValue())); + private void collectAdditions(Node node, byte[] path, + IdentityHashMap retained, List additions) { + if (node == null) { + return; + } + BytesKey oldPath = materializedNodes.get(node); + if (oldPath != null) { + if (!Arrays.equals(oldPath.bytes, path)) { + throw new IllegalStateException("path-local update moved an unchanged subtree"); } - build(entries, 0, EMPTY_PATH, nodes); + retained.put(node, Boolean.TRUE); + return; } - return nodes; + additions.add(new NodePath(node, path)); + visitChildren(node, path, + (child, childPath) -> collectAdditions(child, childPath, retained, additions)); } - private static byte[] rootHash(Map nodes) { - if (nodes.isEmpty()) { - return Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length); + private void collectRemovals(Node node, IdentityHashMap retained, + List removals) { + if (node == null || retained.containsKey(node)) { + return; } - byte[] root = nodes.get(new BytesKey(EMPTY_PATH)); - if (root == null) { - throw new IllegalStateException("path node set has no root"); + BytesKey path = materializedNodes.get(node); + if (path == null) { + throw new IllegalStateException("path-local update lost a materialized node identity"); } - return Hash.sha3(root); + removals.add(new NodePath(node, path.copy())); + visitChildren(node, path.bytes, + (child, ignored) -> collectRemovals(child, retained, removals)); } - private static byte[] build(List entries, int depth, byte[] nodePath, - Map nodes) { + private Node buildTree() { + if (leaves.isEmpty()) { + return null; + } + List entries = new ArrayList<>(leaves.size()); + for (Map.Entry entry : leaves.entrySet()) { + entries.add(new Leaf(toNibbles(entry.getKey().bytes), entry.getValue())); + } + return build(entries, 0); + } + + private static Node build(List entries, int depth) { if (entries.size() == 1) { Leaf leaf = entries.get(0); - byte[] encoded = rlpList(rlpItem(compactPath(leaf.nibbles, depth, true)), - rlpItem(leaf.value)); - nodes.put(new BytesKey(nodePath), encoded); - return encoded; + return new LeafNode(Arrays.copyOfRange(leaf.nibbles, depth, leaf.nibbles.length), + leaf.value); } - int shared = sharedPrefix(entries, depth); if (shared > 0) { - byte[] childPath = append(nodePath, entries.get(0).nibbles, depth, shared); - byte[] child = build(entries, depth + shared, childPath, nodes); - byte[] prefix = Arrays.copyOfRange(entries.get(0).nibbles, depth, depth + shared); - byte[] encoded = rlpList(rlpItem(compactPath(prefix, 0, false)), nodeReference(child)); - nodes.put(new BytesKey(nodePath), encoded); - return encoded; + return new ExtensionNode(Arrays.copyOfRange(entries.get(0).nibbles, depth, + depth + shared), build(entries, depth + shared)); } - - List encodedChildren = new ArrayList<>(Collections.nCopies(17, EMPTY_RLP_ITEM)); + Node[] children = new Node[16]; int start = 0; while (start < entries.size()) { int nibble = entries.get(start).nibbles[depth]; @@ -211,14 +235,176 @@ private static byte[] build(List entries, int depth, byte[] nodePath, while (end < entries.size() && entries.get(end).nibbles[depth] == nibble) { end++; } - byte[] childPath = append(nodePath, new byte[]{(byte) nibble}, 0, 1); - byte[] child = build(entries.subList(start, end), depth + 1, childPath, nodes); - encodedChildren.set(nibble, nodeReference(child)); + children[nibble] = build(entries.subList(start, end), depth + 1); start = end; } - byte[] encoded = rlpList(encodedChildren.toArray(new byte[encodedChildren.size()][])); - nodes.put(new BytesKey(nodePath), encoded); - return encoded; + return new BranchNode(children); + } + + private static Node update(Node node, byte[] key, int offset, byte[] value) { + if (node == null) { + return value == null ? null + : new LeafNode(Arrays.copyOfRange(key, offset, key.length), value); + } + if (node instanceof LeafNode) { + return updateLeaf((LeafNode) node, key, offset, value); + } + if (node instanceof ExtensionNode) { + return updateExtension((ExtensionNode) node, key, offset, value); + } + BranchNode branch = (BranchNode) node; + if (offset >= key.length) { + throw new IllegalStateException("secure path ended inside a branch"); + } + int nibble = key[offset]; + Node previous = branch.children[nibble]; + Node changed = update(previous, key, offset + 1, value); + if (previous == changed) { + return branch; + } + Node[] children = Arrays.copyOf(branch.children, branch.children.length); + children[nibble] = changed; + return normalizeBranch(children); + } + + private static Node updateLeaf(LeafNode leaf, byte[] key, int offset, byte[] value) { + int remaining = key.length - offset; + int shared = commonPrefix(leaf.path, 0, key, offset); + if (shared == leaf.path.length && shared == remaining) { + if (value == null) { + return null; + } + return Arrays.equals(leaf.value, value) ? leaf : new LeafNode(leaf.path, value); + } + if (value == null) { + return leaf; + } + if (shared >= leaf.path.length || shared >= remaining) { + throw new IllegalStateException("fixed secure keys cannot prefix one another"); + } + Node[] children = new Node[16]; + int oldNibble = leaf.path[shared]; + children[oldNibble] = new LeafNode( + Arrays.copyOfRange(leaf.path, shared + 1, leaf.path.length), leaf.value); + int newNibble = key[offset + shared]; + children[newNibble] = new LeafNode( + Arrays.copyOfRange(key, offset + shared + 1, key.length), value); + Node branch = new BranchNode(children); + return shared == 0 ? branch + : new ExtensionNode(Arrays.copyOf(leaf.path, shared), branch); + } + + private static Node updateExtension(ExtensionNode extension, byte[] key, int offset, + byte[] value) { + int shared = commonPrefix(extension.path, 0, key, offset); + if (shared == extension.path.length) { + Node changed = update(extension.child, key, offset + shared, value); + if (changed == extension.child) { + return extension; + } + return normalizeExtension(extension.path, changed); + } + if (value == null) { + return extension; + } + Node[] children = new Node[16]; + int oldNibble = extension.path[shared]; + byte[] oldSuffix = Arrays.copyOfRange(extension.path, shared + 1, extension.path.length); + children[oldNibble] = oldSuffix.length == 0 ? extension.child + : new ExtensionNode(oldSuffix, extension.child); + int newNibble = key[offset + shared]; + children[newNibble] = new LeafNode( + Arrays.copyOfRange(key, offset + shared + 1, key.length), value); + Node branch = new BranchNode(children); + return shared == 0 ? branch + : new ExtensionNode(Arrays.copyOf(extension.path, shared), branch); + } + + private static Node normalizeBranch(Node[] children) { + int count = 0; + int only = -1; + for (int i = 0; i < children.length; i++) { + if (children[i] != null) { + count++; + only = i; + } + } + if (count == 0) { + return null; + } + if (count > 1) { + return new BranchNode(children); + } + Node child = children[only]; + byte[] prefix = new byte[]{(byte) only}; + if (child instanceof LeafNode) { + LeafNode leaf = (LeafNode) child; + return new LeafNode(append(prefix, leaf.path), leaf.value); + } + if (child instanceof ExtensionNode) { + ExtensionNode extension = (ExtensionNode) child; + return new ExtensionNode(append(prefix, extension.path), extension.child); + } + return new ExtensionNode(prefix, child); + } + + private static Node normalizeExtension(byte[] path, Node child) { + if (child == null) { + return null; + } + if (child instanceof LeafNode) { + LeafNode leaf = (LeafNode) child; + return new LeafNode(append(path, leaf.path), leaf.value); + } + if (child instanceof ExtensionNode) { + ExtensionNode extension = (ExtensionNode) child; + return new ExtensionNode(append(path, extension.path), extension.child); + } + return new ExtensionNode(path, child); + } + + private static Map collectNodes(Node root) { + Map nodes = new LinkedHashMap<>(); + collectNodes(root, EMPTY_PATH, nodes); + return nodes; + } + + private static void collectNodes(Node node, byte[] path, Map nodes) { + if (node == null) { + return; + } + if (nodes.put(new BytesKey(path), node.encoded) != null) { + throw new IllegalStateException("duplicate path-state node path"); + } + visitChildren(node, path, (child, childPath) -> collectNodes(child, childPath, nodes)); + } + + private static void indexNodes(Node node, byte[] path, + IdentityHashMap indexed) { + if (node == null) { + return; + } + indexed.put(node, new BytesKey(path)); + visitChildren(node, path, (child, childPath) -> indexNodes(child, childPath, indexed)); + } + + private static void visitChildren(Node node, byte[] path, NodeVisitor visitor) { + if (node instanceof ExtensionNode) { + ExtensionNode extension = (ExtensionNode) node; + visitor.visit(extension.child, append(path, extension.path)); + } else if (node instanceof BranchNode) { + BranchNode branch = (BranchNode) node; + for (int i = 0; i < branch.children.length; i++) { + if (branch.children[i] != null) { + visitor.visit(branch.children[i], append(path, new byte[]{(byte) i})); + } + } + } + } + + private static byte[] hash(Node node) { + return node == null ? Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length) + : Hash.sha3(node.encoded); } private static int sharedPrefix(List entries, int depth) { @@ -236,16 +422,25 @@ private static int sharedPrefix(List entries, int depth) { return shared; } + private static int commonPrefix(byte[] left, int leftOffset, byte[] right, int rightOffset) { + int length = Math.min(left.length - leftOffset, right.length - rightOffset); + int shared = 0; + while (shared < length && left[leftOffset + shared] == right[rightOffset + shared]) { + shared++; + } + return shared; + } + private static byte[] nodeReference(byte[] encodedNode) { return encodedNode.length < SECURE_KEY_LENGTH ? encodedNode : rlpItem(Hash.sha3(encodedNode)); } - private static byte[] compactPath(byte[] nibbles, int offset, boolean leaf) { - int length = nibbles.length - offset; + private static byte[] compactPath(byte[] nibbles, boolean leaf) { + int length = nibbles.length; boolean odd = (length & 1) != 0; byte[] compact = new byte[1 + length / 2]; int flag = leaf ? 2 : 0; - int source = offset; + int source = 0; if (odd) { compact[0] = (byte) ((flag + 1) << 4 | nibbles[source++]); } else { @@ -267,9 +462,9 @@ private static byte[] toNibbles(byte[] key) { return nibbles; } - private static byte[] append(byte[] prefix, byte[] suffix, int offset, int length) { - byte[] result = Arrays.copyOf(prefix, prefix.length + length); - System.arraycopy(suffix, offset, result, prefix.length, length); + private static byte[] append(byte[] first, byte[] second) { + byte[] result = Arrays.copyOf(first, first.length + second.length); + System.arraycopy(second, 0, result, first.length, second.length); return result; } @@ -293,8 +488,7 @@ private static byte[] rlpItem(byte[] raw) { if (raw.length == 1 && (raw[0] & 0xff) < 0x80) { return Arrays.copyOf(raw, raw.length); } - byte[] prefix = rlpLength(raw.length, 0x80, 0xb7); - return concatenate(prefix, raw); + return concatenate(rlpLength(raw.length, 0x80, 0xb7), raw); } private static byte[] rlpList(byte[]... encodedItems) { @@ -333,6 +527,70 @@ private static byte[] concatenate(byte[] first, byte[] second) { return result; } + private abstract static class Node { + + private final byte[] encoded; + + private Node(byte[] encoded) { + this.encoded = encoded; + } + } + + private static final class LeafNode extends Node { + + private final byte[] path; + private final byte[] value; + + private LeafNode(byte[] path, byte[] value) { + super(rlpList(rlpItem(compactPath(path, true)), rlpItem(value))); + this.path = Arrays.copyOf(path, path.length); + this.value = Arrays.copyOf(value, value.length); + } + } + + private static final class ExtensionNode extends Node { + + private final byte[] path; + private final Node child; + + private ExtensionNode(byte[] path, Node child) { + super(encode(path, child)); + if (path.length == 0) { + throw new IllegalArgumentException("extension path must not be empty"); + } + this.path = Arrays.copyOf(path, path.length); + this.child = Objects.requireNonNull(child, "child"); + } + + private static byte[] encode(byte[] path, Node child) { + Node present = Objects.requireNonNull(child, "child"); + return rlpList(rlpItem(compactPath(path, false)), nodeReference(present.encoded)); + } + } + + private static final class BranchNode extends Node { + + private final Node[] children; + + private BranchNode(Node[] children) { + super(encode(children)); + this.children = Arrays.copyOf(children, children.length); + } + + private static byte[] encode(Node[] children) { + if (children.length != 16) { + throw new IllegalArgumentException("branch must contain 16 child slots"); + } + List encodedChildren = new ArrayList<>(Collections.nCopies(17, EMPTY_RLP_ITEM)); + for (int i = 0; i < children.length; i++) { + if (children[i] != null) { + encodedChildren.set(i, nodeReference(children[i].encoded)); + } + } + return rlpList(encodedChildren.toArray(new byte[encodedChildren.size()][])); + } + } + private static final class Leaf { private final byte[] nibbles; @@ -344,6 +602,23 @@ private Leaf(byte[] nibbles, byte[] value) { } } + private static final class NodePath { + + private final Node node; + private final byte[] path; + + private NodePath(Node node, byte[] path) { + this.node = node; + this.path = Arrays.copyOf(path, path.length); + } + } + + @FunctionalInterface + private interface NodeVisitor { + + void visit(Node node, byte[] path); + } + static final class LeafEntry { private final byte[] secureKey; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java index 0ab3df06299..95ba5701fdb 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java @@ -120,6 +120,31 @@ public void detectsMissingCorruptAndDirtyCommittedNodes() { assertThrows(IllegalStateException.class, dirtyTrie::verifyNodeStore); } + @Test + public void singleLeafUpdateRewritesOnlyItsMaterializedPath() { + int leafCount = 32; + byte[][] keys = new byte[leafCount][]; + byte[][] values = new byte[leafCount][]; + InMemoryPathNodeStore store = new InMemoryPathNodeStore(); + PathMerkleTrie trie = new PathMerkleTrie(store); + for (int i = 0; i < leafCount; i++) { + keys[i] = filledKey(i); + values[i] = value("value-" + i); + trie.put(keys[i], values[i]); + } + trie.rootHash(); + int fullNodeCount = store.nodes.size(); + + values[17] = value("updated"); + trie.put(keys[17], values[17]); + + assertArrayEquals(referenceRoot(keys, values), trie.rootHash()); + assertTrue(fullNodeCount > 3); + assertEquals(3, trie.getLastNodePuts()); + assertEquals(3, trie.getLastNodeDeletes()); + trie.verifyNodeStore(); + } + private static byte[] referenceRoot(byte[][] keys, byte[][] values) { TrieImpl reference = new TrieImpl(); reference.setAsync(false); From d3bcf086503273cb4f57d672fabc758edcc5345d Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 12:31:42 +0800 Subject: [PATCH 072/161] feat(db): partition state archive history Write account, storage-row, account-asset, and delegation history to dedicated state-archive file libraries while keeping the remaining stores in the default library. Authenticate every lane through the default history envelope and preserve bounded restart recovery.\n\nRemove the legacy account-only index in favor of the shared exact-27 serving generation, and keep the startup diagnostic exact-only by excluding the deferred account-asset prefix check.\n\nTests: Archive and diagnostic regression 203/203; framework main/test Checkstyle. --- .../core/db2/archive/AccountChangeIndex.java | 199 ---------- .../db2/archive/ArchiveHistoryScanAnchor.java | 8 + .../db2/archive/ArchiveHistoryTruncator.java | 6 +- .../db2/archive/ArchiveHistoryWriter.java | 91 ++--- .../db2/archive/ArchiveTruncationIntent.java | 4 +- .../archive/ArchiveTruncationRecovery.java | 2 +- .../core/db2/archive/HistoryBodyStore.java | 22 ++ .../core/db2/archive/HistorySegmentStore.java | 31 +- .../archive/PartitionedHistoryBodyStore.java | 359 ++++++++++++++++++ .../PersistentCommittedHistoryReader.java | 10 +- .../tron/program/ArchiveStateDiagnostic.java | 82 +--- .../db2/archive/ArchiveHistoryWriterTest.java | 85 ++++- .../ArchiveTruncationRecoveryTest.java | 10 +- .../program/ArchiveStateDiagnosticTest.java | 22 +- 14 files changed, 536 insertions(+), 395 deletions(-) delete mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoryBodyStore.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/PartitionedHistoryBodyStore.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java deleted file mode 100644 index 6659bc2a3e3..00000000000 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountChangeIndex.java +++ /dev/null @@ -1,199 +0,0 @@ -package org.tron.core.db2.archive; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; -import java.util.OptionalLong; -import org.rocksdb.Options; -import org.rocksdb.RocksDB; -import org.rocksdb.RocksDBException; -import org.rocksdb.RocksIterator; -import org.rocksdb.WriteBatch; -import org.rocksdb.WriteOptions; -import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; -import org.tron.core.db2.archive.BlockReverseDiff.Entry; - -/** Persistent derived exact-key change index for the narrow historical account query. */ -final class AccountChangeIndex implements Closeable { - - static { - RocksDB.loadLibrary(); - } - - private static final byte DATA_PREFIX = 1; - private static final byte[] HEAD_KEY = new byte[]{0, 'h', 'e', 'a', 'd'}; - private static final int ADDRESS_LENGTH = HistoricalAccountBalanceReader.ADDRESS_LENGTH; - private static final int DATA_KEY_LENGTH = 1 + ADDRESS_LENGTH + Long.BYTES; - - private final Options options = new Options().setCreateIfMissing(true); - private final RocksDB database; - private final WriteOptions syncWrites = new WriteOptions().setSync(true); - - AccountChangeIndex(Path directory) throws IOException { - try { - database = RocksDB.open(options, directory.toString()); - } catch (RocksDBException failure) { - throw new IOException("Failed to open account change index", failure); - } - } - - synchronized void apply(List diffs) throws IOException { - if (diffs.isEmpty()) { - return; - } - long current = getIndexedThrough(); - BlockSnapshotMeta previous = null; - try (WriteBatch batch = new WriteBatch()) { - for (BlockReverseDiff diff : diffs) { - BlockSnapshotMeta meta = diff.getMeta(); - if (current >= 0 && previous == null && meta.getEpoch() != current + 1) { - throw new ArchivePersistenceException("Account index catch-up is not contiguous"); - } - if (previous != null && meta.getEpoch() != previous.getEpoch() + 1) { - throw new ArchivePersistenceException("Account index batch is not contiguous"); - } - for (DbGroup group : diff.getGroups()) { - if (!HistoricalAccountBalanceReader.ACCOUNT_DATABASE.equals(group.getDbName())) { - continue; - } - for (Entry entry : group.getEntries()) { - byte[] address = entry.getKey(); - if (address.length != ADDRESS_LENGTH) { - continue; - } - batch.put(dataKey(address, meta.getEpoch()), new byte[]{1}); - } - } - previous = meta; - } - batch.put(HEAD_KEY, encodeHead(previous)); - database.write(syncWrites, batch); - } catch (RocksDBException failure) { - throw new IOException("Failed to update account change index", failure); - } - } - - synchronized void revert(BlockReverseDiff diff, BlockSnapshotMeta newHead) throws IOException { - if (getIndexedThrough() != diff.getMeta().getEpoch()) { - throw new ArchivePersistenceException("Account index revert does not target its head"); - } - try (WriteBatch batch = new WriteBatch()) { - for (DbGroup group : diff.getGroups()) { - if (HistoricalAccountBalanceReader.ACCOUNT_DATABASE.equals(group.getDbName())) { - for (Entry entry : group.getEntries()) { - if (entry.getKey().length == ADDRESS_LENGTH) { - batch.delete(dataKey(entry.getKey(), diff.getMeta().getEpoch())); - } - } - } - } - if (newHead == null) { - batch.delete(HEAD_KEY); - } else { - batch.put(HEAD_KEY, encodeHead(newHead)); - } - database.write(syncWrites, batch); - } catch (RocksDBException failure) { - throw new IOException("Failed to revert account change index", failure); - } - } - - /** Truncates this derived index to the authoritative committed-history head. */ - synchronized void truncateAfter(BlockSnapshotMeta newHead) throws IOException { - long target = newHead == null ? -1 : newHead.getEpoch(); - try (WriteBatch batch = new WriteBatch(); RocksIterator iterator = database.newIterator()) { - iterator.seek(new byte[]{DATA_PREFIX}); - while (iterator.isValid()) { - byte[] key = iterator.key(); - if (key.length != DATA_KEY_LENGTH || key[0] != DATA_PREFIX) { - break; - } - long epoch = ByteBuffer.wrap(key, 1 + ADDRESS_LENGTH, Long.BYTES).getLong(); - if (epoch > target) { - batch.delete(key); - } - iterator.next(); - } - if (newHead == null) { - batch.delete(HEAD_KEY); - } else { - batch.put(HEAD_KEY, encodeHead(newHead)); - } - database.write(syncWrites, batch); - } catch (RocksDBException failure) { - throw new IOException("Failed to truncate account change index", failure); - } - } - - synchronized OptionalLong firstChangeAfter(byte[] address, long target, long upperBound) - throws IOException { - if (address == null || address.length != ADDRESS_LENGTH) { - throw new IllegalArgumentException("TRON account address must be exactly 21 bytes"); - } - if (target > upperBound || upperBound > getIndexedThrough()) { - throw new IllegalArgumentException("Account query is outside index coverage"); - } - if (target == Long.MAX_VALUE) { - return OptionalLong.empty(); - } - byte[] seek = dataKey(address, target + 1); - try (RocksIterator iterator = database.newIterator()) { - iterator.seek(seek); - if (!iterator.isValid()) { - return OptionalLong.empty(); - } - byte[] key = iterator.key(); - if (key.length != DATA_KEY_LENGTH || key[0] != DATA_PREFIX - || !Arrays.equals(address, Arrays.copyOfRange(key, 1, 1 + ADDRESS_LENGTH))) { - return OptionalLong.empty(); - } - long epoch = ByteBuffer.wrap(key, 1 + ADDRESS_LENGTH, Long.BYTES).getLong(); - return epoch <= upperBound ? OptionalLong.of(epoch) : OptionalLong.empty(); - } - } - - synchronized long getIndexedThrough() { - try { - byte[] encoded = database.get(HEAD_KEY); - return encoded == null ? -1 : ByteBuffer.wrap(encoded).getLong(); - } catch (RocksDBException failure) { - throw new ArchivePersistenceException("Failed to read account index head", failure); - } - } - - synchronized boolean headMatches(BlockSnapshotMeta meta) { - try { - byte[] encoded = database.get(HEAD_KEY); - return encoded != null - && encoded.length == Long.BYTES + 32 - && ByteBuffer.wrap(encoded).getLong() == meta.getEpoch() - && Arrays.equals(Arrays.copyOfRange(encoded, Long.BYTES, encoded.length), - meta.getBlockHash()); - } catch (RocksDBException failure) { - throw new ArchivePersistenceException("Failed to validate account index head", failure); - } - } - - private static byte[] dataKey(byte[] address, long epoch) { - if (address.length != ADDRESS_LENGTH || epoch < 0) { - throw new IllegalArgumentException("Invalid account change-index key"); - } - return ByteBuffer.allocate(DATA_KEY_LENGTH).put(DATA_PREFIX).put(address).putLong(epoch) - .array(); - } - - private static byte[] encodeHead(BlockSnapshotMeta meta) { - return ByteBuffer.allocate(Long.BYTES + 32).putLong(meta.getEpoch()).put(meta.getBlockHash()) - .array(); - } - - @Override - public synchronized void close() throws IOException { - syncWrites.close(); - database.close(); - options.close(); - } -} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryScanAnchor.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryScanAnchor.java index 18ddf6aa7e2..bcf8e2827db 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryScanAnchor.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryScanAnchor.java @@ -111,6 +111,14 @@ byte[] getEncodedMarker() { return Arrays.copyOf(encodedMarker, encodedMarker.length); } + ArchiveHistoryScanAnchor forHistoryLocation(HistoryLocation historyLocation) { + HistoryCommitMarker laneMarker = new HistoryCommitMarker(marker.getMeta(), + marker.getPreviousEpoch(), historyLocation, marker.getIndexLocation(), + marker.getBatchId(), marker.getDatabases()); + return new ArchiveHistoryScanAnchor(firstEpoch, recordCount, commitRecordLength, + laneMarker, encodedMarker); + } + private static byte[] encode(long firstEpoch, long recordCount, int commitRecordLength, byte[] markerBytes) { try { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryTruncator.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryTruncator.java index de0e2438a4d..2cbd17a11e8 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryTruncator.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryTruncator.java @@ -9,16 +9,16 @@ public final class ArchiveHistoryTruncator { private final HistoryCommitStore commits; private final HistoryIndexStore index; - private final HistorySegmentStore bodies; + private final HistoryBodyStore bodies; private final FaultHook faultHook; public ArchiveHistoryTruncator(HistoryCommitStore commits, HistoryIndexStore index, - HistorySegmentStore bodies) { + HistoryBodyStore bodies) { this(commits, index, bodies, stage -> { }); } ArchiveHistoryTruncator(HistoryCommitStore commits, HistoryIndexStore index, - HistorySegmentStore bodies, FaultHook faultHook) { + HistoryBodyStore bodies, FaultHook faultHook) { this.commits = Objects.requireNonNull(commits, "commits"); this.index = Objects.requireNonNull(index, "index"); this.bodies = Objects.requireNonNull(bodies, "bodies"); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java index e0058bfa11a..2facabe2f7e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveHistoryWriter.java @@ -20,10 +20,9 @@ public final class ArchiveHistoryWriter static final int MAX_RESTART_TAIL_RECORDS = 1024; - private final HistorySegmentStore bodies; + private final HistoryBodyStore bodies; private final HistoryIndexStore index; private final HistoryCommitStore commits; - private final AccountChangeIndex accountIndex; private final ArchiveBaseManifest manifest; private final Path archiveDirectory; private final HistoryCommitMarkerCodec commitCodec; @@ -46,7 +45,7 @@ public ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, new ArchiveTruncationRecovery(archiveDirectory, maxSegmentSize).recover(); ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archiveDirectory, commitCodec); - this.bodies = new HistorySegmentStore(archiveDirectory, new BlockHistoryCodec(), + this.bodies = new PartitionedHistoryBodyStore(archiveDirectory, new BlockHistoryCodec(), maxSegmentSize, checkpoint); this.index = new HistoryIndexStore(archiveDirectory, new HistoryIndexCodec(), checkpoint); this.commits = new HistoryCommitStore(archiveDirectory, commitCodec, checkpoint); @@ -56,10 +55,8 @@ public ArchiveHistoryWriter(Path archiveDirectory, long maxSegmentSize, if (commits.head() != null) { manifest.ensureBase(commits.get(commits.firstEpoch()).getMeta()); } - this.accountIndex = new AccountChangeIndex(archiveDirectory.resolve("account-change-index")); HistoryCommitMarker bootstrap; try { - catchUpAccountIndex(); bootstrap = ArchiveBootstrapAnchor.loadAndValidateIfPresent( archiveDirectory, this, this.participatingDatabases); } catch (IOException | RuntimeException failure) { @@ -96,7 +93,6 @@ public synchronized void acceptAll(List diffs) { int end = Math.min(diffs.size(), start + MAX_RESTART_TAIL_RECORDS); persistChunk(diffs.subList(start, end)); } - accountIndex.apply(diffs); } catch (IOException | RuntimeException e) { handleWriteFailure(diffs.get(diffs.size() - 1).getMeta(), e); } @@ -107,9 +103,7 @@ public synchronized void revert(BlockSnapshotMeta meta) { try { HistoryCommitMarker head = commits.head(); if (head != null && head.getMeta().equals(meta)) { - BlockReverseDiff reverted = readCommitted(meta.getEpoch()); HistoryCommitMarker previous = commits.get(meta.getEpoch() - 1); - accountIndex.revert(reverted, previous == null ? null : previous.getMeta()); commits.removeHead(meta); persistHistoryScanAnchor(); previous = commits.head(); @@ -219,13 +213,19 @@ public synchronized OldValue readAccountAt(long targetBlock, byte[] address, throw new IllegalArgumentException("Account query is outside archive coverage"); } try { - java.util.OptionalLong changed = accountIndex.firstChangeAfter(address, targetBlock, - head.getMeta().getEpoch()); - if (!changed.isPresent()) { - return OldValue.fromNullable(accountAtCommittedHead); + for (long epoch = targetBlock + 1; epoch <= head.getMeta().getEpoch(); epoch++) { + HistoryCommitMarker marker = commits.get(epoch); + if (marker == null) { + throw new ArchivePersistenceException("Committed account history contains a gap"); + } + HistoryIndexRecord indexRecord = index.read(marker.getIndexLocation()); + validateMarkerReferences(marker, indexRecord); + if (contains(indexRecord, HistoricalAccountBalanceReader.ACCOUNT_DATABASE, address)) { + return findOldValue(readCommitted(epoch), + HistoricalAccountBalanceReader.ACCOUNT_DATABASE, address); + } } - BlockReverseDiff diff = readCommitted(changed.getAsLong()); - return findOldValue(diff, HistoricalAccountBalanceReader.ACCOUNT_DATABASE, address); + return OldValue.fromNullable(accountAtCommittedHead); } catch (IOException failure) { throw new ArchivePersistenceException("Failed to query historical account", failure); } @@ -443,47 +443,7 @@ private void recoverPreparedSuffix() throws IOException { bodies.truncateAfter(head == null ? null : head.getHistoryLocation(), commits.size()); } - private void catchUpAccountIndex() throws IOException { - HistoryCommitMarker head = commits.head(); - if (head == null) { - if (accountIndex.getIndexedThrough() >= 0) { - accountIndex.truncateAfter(null); - } - return; - } - long indexed = accountIndex.getIndexedThrough(); - long first = commits.firstEpoch(); - if (indexed > head.getMeta().getEpoch()) { - accountIndex.truncateAfter(head.getMeta()); - indexed = head.getMeta().getEpoch(); - } - if (indexed >= 0) { - HistoryCommitMarker indexedMarker = commits.get(indexed); - if (indexedMarker == null || !accountIndex.headMatches(indexedMarker.getMeta())) { - throw new ArchivePersistenceException( - "Account index head differs from committed history"); - } - } - if (indexed >= head.getMeta().getEpoch()) { - return; - } - long next = indexed < 0 ? first : indexed + 1; - List batch = new ArrayList<>(1024); - for (long epoch = next; epoch <= head.getMeta().getEpoch(); epoch++) { - batch.add(readCommitted(epoch)); - if (batch.size() == 1024 || epoch == head.getMeta().getEpoch()) { - accountIndex.apply(batch); - batch.clear(); - } - } - } - private void closeAfterFailedConstruction(Exception failure) { - try { - accountIndex.close(); - } catch (IOException closeFailure) { - failure.addSuppressed(closeFailure); - } try { index.close(); } catch (IOException closeFailure) { @@ -515,6 +475,20 @@ private static OldValue findOldValue(BlockReverseDiff diff, String dbName, byte[ throw new ArchivePersistenceException("Account index references a missing history key"); } + private static boolean contains(HistoryIndexRecord record, String dbName, byte[] rawKey) { + for (HistoryIndexRecord.KeyGroup group : record.getGroups()) { + if (!dbName.equals(group.getDbName())) { + continue; + } + for (byte[] key : group.getKeys()) { + if (Arrays.equals(rawKey, key)) { + return true; + } + } + } + return false; + } + private void validateMarkerReferences(HistoryCommitMarker marker, HistoryIndexRecord indexRecord) { if (!marker.getMeta().equals(indexRecord.getMeta()) @@ -551,15 +525,6 @@ public synchronized void close() throws IOException { } catch (IOException e) { failure = e; } - try { - accountIndex.close(); - } catch (IOException e) { - if (failure == null) { - failure = e; - } else { - failure.addSuppressed(e); - } - } try { bodies.close(); } catch (IOException e) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java index 4e246251693..a5b467d66d8 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationIntent.java @@ -41,14 +41,14 @@ private ArchiveTruncationIntent(long firstEpoch, long recordCount, int recordLen } static ArchiveTruncationIntent prepare(Path archiveDirectory, HistoryCommitStore commits, - HistoryIndexStore index, HistorySegmentStore bodies, long targetEpoch, + HistoryIndexStore index, HistoryBodyStore bodies, long targetEpoch, HistoryCommitMarkerCodec markerCodec) throws IOException { return prepare(archiveDirectory, commits, index, bodies, targetEpoch, markerCodec, temporary -> { }); } static ArchiveTruncationIntent prepare(Path archiveDirectory, HistoryCommitStore commits, - HistoryIndexStore index, HistorySegmentStore bodies, long targetEpoch, + HistoryIndexStore index, HistoryBodyStore bodies, long targetEpoch, HistoryCommitMarkerCodec markerCodec, FaultHook faultHook) throws IOException { HistoryCommitMarker marker = commits.get(targetEpoch); if (marker == null) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java index 3ee6ced72be..4a3841dd294 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveTruncationRecovery.java @@ -45,7 +45,7 @@ archiveDirectory, new HistoryIndexCodec(), checkpoint)) { index.truncateAfter(intent.getMarker().getIndexLocation(), intent.getRecordCount()); } faultHook.afterDurableStage(Stage.INDEX_TRUNCATED); - try (HistorySegmentStore bodies = new HistorySegmentStore(archiveDirectory, + try (HistoryBodyStore bodies = new PartitionedHistoryBodyStore(archiveDirectory, new BlockHistoryCodec(), maxSegmentSize, checkpoint)) { bodies.truncateAfter(intent.getMarker().getHistoryLocation(), intent.getRecordCount()); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoryBodyStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryBodyStore.java new file mode 100644 index 00000000000..f5429a45540 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoryBodyStore.java @@ -0,0 +1,22 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; + +/** Physical history-body placement behind the authoritative history writer. */ +interface HistoryBodyStore extends Closeable { + + HistoryLocation append(BlockReverseDiff diff) throws IOException; + + void sync() throws IOException; + + BlockReverseDiff read(HistoryLocation location) throws IOException; + + HistorySegmentStore.ScanResult getScanResult(); + + void truncateAfter(HistoryLocation last) throws IOException; + + void truncateAfter(HistoryLocation last, long knownRecordCount) throws IOException; + + long getStartupScannedRecords(); +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java index 4374041e005..7c66905d561 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistorySegmentStore.java @@ -1,6 +1,5 @@ package org.tron.core.db2.archive; -import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; @@ -16,7 +15,7 @@ import java.util.List; /** Append-only, rotating history body segments with strict tail scanning. */ -public final class HistorySegmentStore implements Closeable { +public final class HistorySegmentStore implements HistoryBodyStore { private static final String PREFIX = "history."; private static final String SUFFIX = ".dat"; @@ -37,10 +36,25 @@ public HistorySegmentStore(Path archiveDirectory, BlockHistoryCodec codec, long HistorySegmentStore(Path archiveDirectory, BlockHistoryCodec codec, long maxSegmentSize, ArchiveHistoryScanAnchor checkpoint) throws IOException { + this(archiveDirectory.resolve("history"), codec, maxSegmentSize, checkpoint, true); + } + + static HistorySegmentStore openLibrary(Path archiveDirectory, String libraryName, + BlockHistoryCodec codec, long maxSegmentSize, ArchiveHistoryScanAnchor checkpoint) + throws IOException { + if (libraryName == null || !libraryName.matches("state-archive(?:-[a-z0-9-]+)?")) { + throw new IllegalArgumentException("Invalid state-archive history library name"); + } + return new HistorySegmentStore(archiveDirectory.resolve("history").resolve(libraryName), + codec, maxSegmentSize, checkpoint, true); + } + + private HistorySegmentStore(Path directory, BlockHistoryCodec codec, long maxSegmentSize, + ArchiveHistoryScanAnchor checkpoint, boolean ignored) throws IOException { if (maxSegmentSize <= 0) { throw new IllegalArgumentException("maxSegmentSize must be positive"); } - this.directory = archiveDirectory.resolve("history"); + this.directory = directory; this.codec = codec; this.maxSegmentSize = maxSegmentSize; Files.createDirectories(directory); @@ -48,6 +62,7 @@ public HistorySegmentStore(Path archiveDirectory, BlockHistoryCodec codec, long openAppendChannel(); } + @Override public synchronized HistoryLocation append(BlockReverseDiff diff) throws IOException { if (scanResult.getInvalidTail() != null) { throw new IllegalStateException("History has an invalid tail which must be truncated first"); @@ -69,11 +84,13 @@ public synchronized HistoryLocation append(BlockReverseDiff diff) throws IOExcep return location; } + @Override public synchronized void sync() throws IOException { appendChannel.force(true); syncDirectory(directory); } + @Override public synchronized BlockReverseDiff read(HistoryLocation location) throws IOException { return codec.decode(readRecord(location)); } @@ -95,6 +112,7 @@ public synchronized byte[] readRecord(HistoryLocation location) throws IOExcepti } } + @Override public synchronized ScanResult getScanResult() { return scanResult; } @@ -117,11 +135,13 @@ public synchronized void truncateInvalidTail() throws IOException { } /** Truncates all records after {@code last}; null means remove every body record. */ + @Override public synchronized void truncateAfter(HistoryLocation last) throws IOException { truncateAfter(last, -1); } - synchronized void truncateAfter(HistoryLocation last, long knownRecordCount) + @Override + public synchronized void truncateAfter(HistoryLocation last, long knownRecordCount) throws IOException { closeAppendChannel(); if (last == null) { @@ -145,7 +165,8 @@ synchronized void truncateAfter(HistoryLocation last, long knownRecordCount) openAppendChannel(); } - long getStartupScannedRecords() { + @Override + public long getStartupScannedRecords() { return startupScannedRecords; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PartitionedHistoryBodyStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/PartitionedHistoryBodyStore.java new file mode 100644 index 00000000000..e8542386fc2 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PartitionedHistoryBodyStore.java @@ -0,0 +1,359 @@ +package org.tron.core.db2.archive; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +/** + * State Archive history placement with one default file library and dedicated hot-Store + * libraries. The default record contains authenticated locations for every dedicated record, so + * the existing authoritative marker commits the complete multi-library body. + */ +final class PartitionedHistoryBodyStore implements HistoryBodyStore { + + static final String DEFAULT_LIBRARY = "state-archive"; + static final List DEDICATED_STORES = Collections.unmodifiableList(Arrays.asList( + "account", "storage-row", "account-asset", "delegation")); + + private static final String REFERENCE_PREFIX = "\u0000state-archive-library/"; + private static final byte[] REFERENCE_KEY = new byte[]{1}; + private static final int ENCODED_LOCATION_LENGTH = Integer.BYTES + Long.BYTES + + Integer.BYTES + Integer.BYTES + 32; + + private final HistorySegmentStore defaultLibrary; + private final Map dedicatedLibraries; + + PartitionedHistoryBodyStore(Path archiveDirectory, BlockHistoryCodec codec, + long maxSegmentSize, ArchiveHistoryScanAnchor checkpoint) throws IOException { + rejectLegacySharedLibrary(archiveDirectory); + rejectLegacyAccountIndex(archiveDirectory); + validateExistingLibraryEntries(archiveDirectory); + this.defaultLibrary = HistorySegmentStore.openLibrary(archiveDirectory, DEFAULT_LIBRARY, + codec, maxSegmentSize, checkpoint); + Map opened = new LinkedHashMap<>(); + try { + Map checkpointLocations = checkpointLocations(checkpoint); + for (String store : DEDICATED_STORES) { + ArchiveHistoryScanAnchor laneCheckpoint = checkpoint == null ? null + : checkpoint.forHistoryLocation(checkpointLocations.get(store)); + opened.put(store, HistorySegmentStore.openLibrary(archiveDirectory, + libraryName(store), codec, maxSegmentSize, laneCheckpoint)); + } + this.dedicatedLibraries = Collections.unmodifiableMap(opened); + validateScannedHeads(); + } catch (IOException | RuntimeException failure) { + closeOpened(opened, failure); + try { + defaultLibrary.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } + } + + static String libraryName(String store) { + return DEFAULT_LIBRARY + "-" + store; + } + + @Override + public synchronized HistoryLocation append(BlockReverseDiff diff) throws IOException { + Map locations = new LinkedHashMap<>(); + for (Map.Entry library : dedicatedLibraries.entrySet()) { + BlockReverseDiff lane = select(diff, library.getKey()); + locations.put(library.getKey(), library.getValue().append(lane)); + } + List defaultGroups = new ArrayList<>(); + for (DbGroup group : diff.getGroups()) { + if (!dedicatedLibraries.containsKey(group.getDbName())) { + defaultGroups.add(group); + } + } + for (Map.Entry location : locations.entrySet()) { + defaultGroups.add(referenceGroup(location.getKey(), location.getValue())); + } + return defaultLibrary.append(new BlockReverseDiff(diff.getMeta(), defaultGroups)); + } + + @Override + public synchronized void sync() throws IOException { + for (HistorySegmentStore library : dedicatedLibraries.values()) { + library.sync(); + } + defaultLibrary.sync(); + } + + @Override + public synchronized BlockReverseDiff read(HistoryLocation location) throws IOException { + BlockReverseDiff envelope = defaultLibrary.read(location); + DecodedEnvelope decoded = decodeEnvelope(envelope); + List groups = new ArrayList<>(decoded.defaultGroups); + for (String store : DEDICATED_STORES) { + HistoryLocation dedicatedLocation = decoded.locations.get(store); + if (dedicatedLocation == null) { + throw new ArchivePersistenceException( + "State Archive history envelope is missing dedicated library: " + store); + } + BlockReverseDiff lane = dedicatedLibraries.get(store).read(dedicatedLocation); + if (!envelope.getMeta().equals(lane.getMeta())) { + throw new ArchivePersistenceException( + "State Archive dedicated library metadata mismatch: " + store); + } + if (lane.getGroups().size() > 1 + || (!lane.getGroups().isEmpty() + && !store.equals(lane.getGroups().get(0).getDbName()))) { + throw new ArchivePersistenceException( + "State Archive dedicated library contains a foreign Store: " + store); + } + groups.addAll(lane.getGroups()); + } + return new BlockReverseDiff(envelope.getMeta(), groups); + } + + @Override + public synchronized HistorySegmentStore.ScanResult getScanResult() { + return defaultLibrary.getScanResult(); + } + + @Override + public synchronized void truncateAfter(HistoryLocation last) throws IOException { + truncateAfter(last, -1); + } + + @Override + public synchronized void truncateAfter(HistoryLocation last, long knownRecordCount) + throws IOException { + Map dedicated = Collections.emptyMap(); + if (last != null) { + dedicated = decodeEnvelope(defaultLibrary.read(last)).locations; + } + IOException failure = null; + for (Map.Entry library : dedicatedLibraries.entrySet()) { + try { + HistoryLocation laneLast = last == null ? null : dedicated.get(library.getKey()); + if (last != null && laneLast == null) { + throw new ArchivePersistenceException( + "State Archive history envelope is missing dedicated library: " + + library.getKey()); + } + library.getValue().truncateAfter(laneLast, knownRecordCount); + } catch (IOException | RuntimeException problem) { + if (problem instanceof IOException) { + failure = merge(failure, (IOException) problem); + } else { + throw problem; + } + } + } + try { + defaultLibrary.truncateAfter(last, knownRecordCount); + } catch (IOException problem) { + failure = merge(failure, problem); + } + if (failure != null) { + throw failure; + } + } + + @Override + public synchronized long getStartupScannedRecords() { + // The public counter is logical body records scanned, not the number of physical lanes read. + return defaultLibrary.getStartupScannedRecords(); + } + + @Override + public synchronized void close() throws IOException { + IOException failure = null; + for (HistorySegmentStore library : dedicatedLibraries.values()) { + try { + library.close(); + } catch (IOException problem) { + failure = merge(failure, problem); + } + } + try { + defaultLibrary.close(); + } catch (IOException problem) { + failure = merge(failure, problem); + } + if (failure != null) { + throw failure; + } + } + + private void validateScannedHeads() throws IOException { + HistorySegmentStore.ScannedRecord defaultHead = defaultLibrary.getScanResult().getHead(); + if (defaultHead == null) { + return; + } + read(defaultHead.getLocation()); + } + + private Map checkpointLocations( + ArchiveHistoryScanAnchor checkpoint) throws IOException { + if (checkpoint == null) { + return Collections.emptyMap(); + } + BlockReverseDiff envelope = defaultLibrary.read( + checkpoint.getMarker().getHistoryLocation()); + Map locations = decodeEnvelope(envelope).locations; + for (String store : DEDICATED_STORES) { + if (!locations.containsKey(store)) { + throw new ArchivePersistenceException( + "State Archive scan anchor is missing dedicated library: " + store); + } + } + return locations; + } + + private static BlockReverseDiff select(BlockReverseDiff diff, String store) { + for (DbGroup group : diff.getGroups()) { + if (store.equals(group.getDbName())) { + return new BlockReverseDiff(diff.getMeta(), Collections.singletonList(group)); + } + } + return new BlockReverseDiff(diff.getMeta(), Collections.emptyList()); + } + + private static DbGroup referenceGroup(String store, HistoryLocation location) { + return new DbGroup(REFERENCE_PREFIX + store, Collections.singletonList( + new Entry(REFERENCE_KEY, OldValue.present(encodeLocation(location))))); + } + + private static DecodedEnvelope decodeEnvelope(BlockReverseDiff envelope) { + Map locations = new LinkedHashMap<>(); + List groups = new ArrayList<>(); + for (DbGroup group : envelope.getGroups()) { + if (!group.getDbName().startsWith(REFERENCE_PREFIX)) { + groups.add(group); + continue; + } + String store = group.getDbName().substring(REFERENCE_PREFIX.length()); + if (!DEDICATED_STORES.contains(store) || locations.containsKey(store) + || group.getEntries().size() != 1 + || !Arrays.equals(REFERENCE_KEY, group.getEntries().get(0).getKey()) + || !group.getEntries().get(0).getOldValue().isPresent()) { + throw new ArchivePersistenceException("Invalid State Archive history library reference"); + } + locations.put(store, decodeLocation(group.getEntries().get(0).getOldValue().getValue())); + } + return new DecodedEnvelope(groups, locations); + } + + private static byte[] encodeLocation(HistoryLocation location) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(ENCODED_LOCATION_LENGTH); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(location.getSegmentId()); + output.writeLong(location.getOffset()); + output.writeInt(location.getRecordLength()); + output.writeInt(location.getBodyChecksum()); + output.write(location.getBodyDigest()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected location encoding failure", impossible); + } + } + + private static HistoryLocation decodeLocation(byte[] encoded) { + if (encoded.length != ENCODED_LOCATION_LENGTH) { + throw new ArchivePersistenceException("Invalid State Archive library location length"); + } + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + int segment = input.readInt(); + long offset = input.readLong(); + int length = input.readInt(); + int checksum = input.readInt(); + byte[] digest = new byte[32]; + input.readFully(digest); + return new HistoryLocation(segment, offset, length, checksum, digest); + } catch (IOException impossible) { + throw new ArchivePersistenceException("Invalid State Archive library location", impossible); + } + } + + private static void rejectLegacySharedLibrary(Path archiveDirectory) throws IOException { + Path history = archiveDirectory.resolve("history"); + if (!Files.isDirectory(history)) { + return; + } + try (DirectoryStream segments = Files.newDirectoryStream(history, + "history.*.dat")) { + if (segments.iterator().hasNext()) { + throw new ArchivePersistenceException( + "Legacy shared history layout requires explicit offline migration"); + } + } + } + + private static void rejectLegacyAccountIndex(Path archiveDirectory) { + if (Files.exists(archiveDirectory.resolve("account-change-index"))) { + throw new ArchivePersistenceException( + "Legacy account-change-index requires explicit offline removal"); + } + } + + private static void validateExistingLibraryEntries(Path archiveDirectory) throws IOException { + Path history = archiveDirectory.resolve("history"); + if (!Files.isDirectory(history)) { + return; + } + List expected = new ArrayList<>(); + expected.add(DEFAULT_LIBRARY); + for (String store : DEDICATED_STORES) { + expected.add(libraryName(store)); + } + try (DirectoryStream entries = Files.newDirectoryStream(history)) { + for (Path entry : entries) { + if (!Files.isDirectory(entry) || !expected.contains(entry.getFileName().toString())) { + throw new ArchivePersistenceException( + "Unknown State Archive history library entry: " + entry.getFileName()); + } + } + } + } + + private static void closeOpened(Map stores, Exception failure) { + for (HistorySegmentStore store : stores.values()) { + try { + store.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + } + + private static IOException merge(IOException current, IOException next) { + if (current == null) { + return next; + } + current.addSuppressed(next); + return current; + } + + private static final class DecodedEnvelope { + private final List defaultGroups; + private final Map locations; + + private DecodedEnvelope(List defaultGroups, + Map locations) { + this.defaultGroups = defaultGroups; + this.locations = locations; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java index d5ceacd2f2b..cb35d75198f 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentCommittedHistoryReader.java @@ -17,7 +17,7 @@ public final class PersistentCommittedHistoryReader private final long indexedThrough; private final byte[] headHash; private final byte[] sourceDigest; - private final HistorySegmentStore bodies; + private final HistoryBodyStore bodies; private final HistoryIndexStore index; private final HistoryCommitStore commits; private boolean closed; @@ -30,11 +30,11 @@ private PersistentCommittedHistoryReader(Path archiveDirectory, long maxSegmentS if (checkpoint == null) { throw new ArchivePersistenceException("Archive history scan anchor is missing"); } - HistorySegmentStore openedBodies = null; + HistoryBodyStore openedBodies = null; HistoryIndexStore openedIndex = null; HistoryCommitStore openedCommits = null; try { - openedBodies = new HistorySegmentStore(archiveDirectory, new BlockHistoryCodec(), + openedBodies = new PartitionedHistoryBodyStore(archiveDirectory, new BlockHistoryCodec(), maxSegmentSize, checkpoint); openedIndex = new HistoryIndexStore(archiveDirectory, new HistoryIndexCodec(), checkpoint); openedCommits = new HistoryCommitStore(archiveDirectory, new HistoryCommitMarkerCodec(), @@ -142,7 +142,7 @@ public synchronized void close() throws IOException { } private static void validatePinnedAuthority(PersistentServingKeyIndexGeneration serving, - HistorySegmentStore bodies, HistoryIndexStore index, HistoryCommitStore commits) + HistoryBodyStore bodies, HistoryIndexStore index, HistoryCommitStore commits) throws IOException { HistoryCommitMarker marker = commits.get(serving.getIndexedThrough()); if (marker == null || !Arrays.equals(marker.getMeta().getBlockHash(), serving.getHeadHash()) @@ -211,7 +211,7 @@ private static boolean same(HistoryLocation left, HistoryLocation right) { && Arrays.equals(left.getBodyDigest(), right.getBodyDigest()); } - private static void closeAfterFailedConstruction(HistorySegmentStore bodies, + private static void closeAfterFailedConstruction(HistoryBodyStore bodies, HistoryIndexStore index, HistoryCommitStore commits, Exception failure) { close(index, failure); close(bodies, failure); diff --git a/framework/src/main/java/org/tron/program/ArchiveStateDiagnostic.java b/framework/src/main/java/org/tron/program/ArchiveStateDiagnostic.java index 2cfba7066bf..9843acbba34 100644 --- a/framework/src/main/java/org/tron/program/ArchiveStateDiagnostic.java +++ b/framework/src/main/java/org/tron/program/ArchiveStateDiagnostic.java @@ -19,13 +19,10 @@ import org.tron.core.db2.archive.ArchivePersistenceException; import org.tron.core.db2.archive.ArchiveStoreScope; import org.tron.core.db2.archive.HistoricalAccountAssetBalanceResolver; -import org.tron.core.db2.archive.HistoricalAccountAssetPrefixResolver; -import org.tron.core.db2.archive.HistoricalAccountAssetPrefixResolver.Balance; import org.tron.core.db2.archive.OldValue; import org.tron.core.db2.archive.P66AccountAssetCodec; import org.tron.core.db2.archive.P66AccountAssetCodec.DecodedAssetRow; import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; -import org.tron.core.db2.common.WrappedByteArray; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.store.AccountAssetStore; @@ -52,10 +49,9 @@ public static void runIfEnabled(TronApplicationContext context) { } Report report = run(context.getBean(Manager.class), (SnapshotManager) revokingDatabase); logger.info("State archive startup diagnostic complete: block={}, stores={}, present={}, " - + "absent={}, p66Phase={}, p66Balance={}, p66PrefixEntries={}", + + "absent={}, p66Phase={}, p66Balance={}", report.getBlockNumber(), report.getStoreCount(), report.getPresentCount(), - report.getAbsentCount(), report.getP66Phase(), report.getP66Balance(), - report.getP66PrefixCount()); + report.getAbsentCount(), report.getP66Phase(), report.getP66Balance()); } static Report run(Manager manager, SnapshotManager snapshotManager) { @@ -109,72 +105,8 @@ static Report run(Manager manager, SnapshotManager snapshotManager) { blockNumber, Hex.toHexString(decoded.getAccountAddress()), decoded.getTokenId(), logical.getPhase(), logical.getBalance()); - Map currentBalances = currentAccountAssetBalances( - accountAssetStore, decoded.getAccountAddress()); - HistoricalAccountAssetPrefixResolver.Limits limits = prefixLimits( - accountAssetStore, decoded.getAccountAddress()); - HistoricalAccountAssetPrefixResolver.Result prefix = manager.getArchiveAccountAssets( - blockNumber, decoded.getAccountAddress(), limits); - if (!prefix.isAccountPresent() || prefix.getPhase() != Phase.P66_ON - || prefix.getBalances().size() != currentBalances.size()) { - throw new ArchivePersistenceException( - "State Archive diagnostic P66 AccountAsset prefix mismatch at block " + blockNumber); - } - for (Balance balance : prefix.getBalances()) { - Long currentBalance = currentBalances.get(balance.getTokenId()); - if (currentBalance == null || currentBalance != balance.getBalance()) { - throw new ArchivePersistenceException( - "State Archive diagnostic P66 AccountAsset prefix value mismatch at block " - + blockNumber); - } - } - logger.info("State archive diagnostic P66 prefix: block={}, address={}, phase={}, entries={}", - blockNumber, Hex.toHexString(decoded.getAccountAddress()), prefix.getPhase(), - prefix.getBalances().size()); return new Report(blockNumber, expectedStores.size(), presentCount, absentCount, - logical.getPhase(), logical.getBalance(), prefix.getBalances().size()); - } - - private static Map currentAccountAssetBalances(AccountAssetStore store, - byte[] address) { - Map balances = new java.util.TreeMap<>(); - P66AccountAssetCodec codec = new P66AccountAssetCodec(); - for (Map.Entry entry : store.prefixQuery(address).entrySet()) { - DecodedAssetRow decoded = codec.decodePresentAssetRow( - entry.getKey().getBytes(), entry.getValue()); - if (!Arrays.equals(address, decoded.getAccountAddress()) - || balances.put(decoded.getTokenId(), decoded.getBalance()) != null) { - throw new ArchivePersistenceException( - "State Archive diagnostic current AccountAsset prefix is invalid"); - } - } - if (balances.isEmpty()) { - throw new ArchivePersistenceException( - "State Archive diagnostic requires a nonempty AccountAsset prefix"); - } - return balances; - } - - private static HistoricalAccountAssetPrefixResolver.Limits prefixLimits( - AccountAssetStore store, byte[] address) { - int entries = 0; - int maxKeyBytes = 1; - int maxValueBytes = 1; - long totalBytes = 0L; - for (Map.Entry entry : store.prefixQuery(address).entrySet()) { - byte[] key = entry.getKey().getBytes(); - byte[] value = Objects.requireNonNull(entry.getValue(), "AccountAsset prefix value"); - entries++; - maxKeyBytes = Math.max(maxKeyBytes, key.length); - maxValueBytes = Math.max(maxValueBytes, value.length); - totalBytes = Math.addExact(totalBytes, Math.addExact((long) key.length, value.length)); - } - if (entries == 0) { - throw new ArchivePersistenceException( - "State Archive diagnostic requires AccountAsset prefix limits"); - } - return new HistoricalAccountAssetPrefixResolver.Limits( - 1, entries, entries, maxKeyBytes, maxValueBytes, totalBytes); + logical.getPhase(), logical.getBalance()); } private static Map collectStores(Manager manager, @@ -301,17 +233,15 @@ static final class Report { private final int absentCount; private final Phase p66Phase; private final long p66Balance; - private final int p66PrefixCount; private Report(long blockNumber, int storeCount, int presentCount, int absentCount, - Phase p66Phase, long p66Balance, int p66PrefixCount) { + Phase p66Phase, long p66Balance) { this.blockNumber = blockNumber; this.storeCount = storeCount; this.presentCount = presentCount; this.absentCount = absentCount; this.p66Phase = p66Phase; this.p66Balance = p66Balance; - this.p66PrefixCount = p66PrefixCount; } long getBlockNumber() { @@ -337,9 +267,5 @@ Phase getP66Phase() { long getP66Balance() { return p66Balance; } - - int getP66PrefixCount() { - return p66PrefixCount; - } } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java index 0ed55189ad3..afe154d4205 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveHistoryWriterTest.java @@ -127,8 +127,8 @@ public void rollsBackPreparedSuffixAtEveryPreCommitFailure() throws Exception { public void truncatesCrashLeftPreparedBodyAndIndexOnOpen() throws Exception { Path archive = temporaryFolder.newFolder("prepared").toPath(); BlockReverseDiff diff = diff(1); - try (HistorySegmentStore bodies = new HistorySegmentStore( - archive, new BlockHistoryCodec(), 4096); + try (HistoryBodyStore bodies = new PartitionedHistoryBodyStore( + archive, new BlockHistoryCodec(), 4096, null); HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec())) { HistoryLocation body = bodies.append(diff); index.append(HistoryIndexRecord.from(diff, body)); @@ -156,7 +156,7 @@ public void rejectsNonContiguousCanonicalInput() throws Exception { } @Test - public void persistsAccountSeekIndexAndRecoversItAcrossRestart() throws Exception { + public void readsAccountHistoryWithoutLegacyAccountIndexAcrossRestart() throws Exception { Path archive = temporaryFolder.newFolder("account-index").toPath(); byte[] address = new byte[21]; address[0] = 0x41; @@ -184,6 +184,7 @@ public void persistsAccountSeekIndexAndRecoversItAcrossRestart() throws Exceptio assertEquals(1, files.count()); } } + assertFalse(Files.exists(archive.resolve("account-change-index"))); } @Test @@ -215,7 +216,8 @@ public void persistsBatchedPrefixWithoutPerBlockFilesAndResumes() throws Excepti } try (java.util.stream.Stream commits = Files.list(archive.resolve("commits")); - java.util.stream.Stream segments = Files.list(archive.resolve("history"))) { + java.util.stream.Stream segments = Files.list( + archive.resolve("history/state-archive"))) { assertEquals(1, commits.count()); assertTrue(segments.count() < 1_000); } @@ -279,7 +281,8 @@ public void truncatesInvalidBodyAndIndexTailWithoutRescanningPrefix() throws Exc writer.acceptAll(batch); } Path lastSegment; - try (java.util.stream.Stream segments = Files.list(archive.resolve("history"))) { + try (java.util.stream.Stream segments = Files.list( + archive.resolve("history/state-archive"))) { lastSegment = segments.sorted().reduce((left, right) -> right) .orElseThrow(AssertionError::new); } @@ -351,7 +354,7 @@ public void completesPreparedTruncationBeforeLoadingHistoryScanAnchor() throws E } @Test - public void truncatesDerivedAccountIndexToRecoveredHistoryAuthority() throws Exception { + public void recoveryDoesNotCreateLegacyAccountIndex() throws Exception { Path archive = temporaryFolder.newFolder("writer-index-ahead").toPath(); try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter(archive, 4096, databases())) { writer.acceptAll(Arrays.asList(diff(1), diff(2), diff(3))); @@ -362,17 +365,67 @@ public void truncatesDerivedAccountIndexToRecoveredHistoryAuthority() throws Exc archive, 4096, databases())) { assertEquals(2, reopened.committedHead().getMeta().getEpoch()); } - ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archive, - new HistoryCommitMarkerCodec()); + ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load( + archive, new HistoryCommitMarkerCodec()); assertEquals(2, checkpoint.getMarker().getMeta().getEpoch()); - try (AccountChangeIndex index = new AccountChangeIndex( - archive.resolve("account-change-index"))) { - assertEquals(2, index.getIndexedThrough()); - assertTrue(index.headMatches(checkpoint.getMarker().getMeta())); - } + assertFalse(Files.exists(archive.resolve("account-change-index"))); assertFalse(Files.exists(archive.resolve("truncation.intent"))); } + @Test + public void placesFourHotStoresInDedicatedStateArchiveLibraries() throws Exception { + Path archive = temporaryFolder.newFolder("dedicated-history-libraries").toPath(); + BlockSnapshotMeta meta = new BlockSnapshotMeta(1, 1, hash(1), hash(0), 3_000L); + List groups = new ArrayList<>(); + for (String store : PartitionedHistoryBodyStore.DEDICATED_STORES) { + groups.add(new DbGroup(store, Collections.singletonList( + new Entry(bytes(store), OldValue.present(bytes("old-" + store)))))); + } + groups.add(new DbGroup("properties", Collections.singletonList( + new Entry(bytes("property"), OldValue.present(bytes("old-property")))))); + BlockReverseDiff expected = new BlockReverseDiff(meta, groups); + + try (ArchiveHistoryWriter writer = new ArchiveHistoryWriter( + archive, 4096, exactDatabases())) { + writer.accept(expected); + assertEquals(expected.getGroups().size(), writer.readCommitted(1).getGroups().size()); + } + + Path history = archive.resolve("history"); + assertTrue(Files.isDirectory(history.resolve("state-archive"))); + for (String store : PartitionedHistoryBodyStore.DEDICATED_STORES) { + Path library = history.resolve(PartitionedHistoryBodyStore.libraryName(store)); + assertTrue(Files.isDirectory(library)); + assertTrue(Files.size(library.resolve("history.000000.dat")) > 0); + } + assertFalse(Files.exists(archive.resolve("account-change-index"))); + } + + @Test + public void rejectsLegacyAccountIndexWithoutCreatingNewHistoryLibraries() throws Exception { + Path archive = temporaryFolder.newFolder("legacy-account-index").toPath(); + Files.createDirectory(archive.resolve("account-change-index")); + + ArchivePersistenceException failure = assertThrows(ArchivePersistenceException.class, + () -> new ArchiveHistoryWriter(archive, 4096, databases())); + + assertTrue(failure.getMessage().contains("explicit offline removal")); + assertFalse(Files.exists(archive.resolve("history"))); + } + + @Test + public void rejectsLegacySharedHistoryWithoutCreatingNewLibraries() throws Exception { + Path archive = temporaryFolder.newFolder("legacy-shared-history").toPath(); + Path history = Files.createDirectory(archive.resolve("history")); + Files.createFile(history.resolve("history.000000.dat")); + + ArchivePersistenceException failure = assertThrows(ArchivePersistenceException.class, + () -> new ArchiveHistoryWriter(archive, 4096, databases())); + + assertTrue(failure.getMessage().contains("explicit offline migration")); + assertFalse(Files.exists(history.resolve("state-archive"))); + } + @Test public void buildsPersistentServingGenerationFromCommittedWriterPrefix() throws Exception { Path archive = temporaryFolder.newFolder("writer-serving-generation").toPath(); @@ -477,8 +530,8 @@ private static void assertCoverage(HistoryCoverage coverage, long firstEpoch, } private static void initializeHistory(Path archive, int lastEpoch) throws Exception { - try (HistorySegmentStore bodies = new HistorySegmentStore( - archive, new BlockHistoryCodec(), 4096); + try (HistoryBodyStore bodies = new PartitionedHistoryBodyStore( + archive, new BlockHistoryCodec(), 4096, null); HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); HistoryCommitStore commits = new HistoryCommitStore( archive, new HistoryCommitMarkerCodec())) { @@ -501,7 +554,7 @@ archive, new HistoryCommitMarkerCodec())) { private static void prepareTruncation(Path archive, long targetEpoch) throws Exception { ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archive, new HistoryCommitMarkerCodec()); - try (HistorySegmentStore bodies = new HistorySegmentStore( + try (HistoryBodyStore bodies = new PartitionedHistoryBodyStore( archive, new BlockHistoryCodec(), 4096, checkpoint); HistoryIndexStore index = new HistoryIndexStore( archive, new HistoryIndexCodec(), checkpoint); diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java index 0ee797e487f..f2ea3d292cc 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveTruncationRecoveryTest.java @@ -69,7 +69,7 @@ public void intentPreReplaceCrashNeverShrinksCommittedAuthority() throws Excepti initialize(archive); ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archive, new HistoryCommitMarkerCodec()); - try (HistorySegmentStore bodies = new HistorySegmentStore( + try (HistoryBodyStore bodies = new PartitionedHistoryBodyStore( archive, new BlockHistoryCodec(), 4096, checkpoint); HistoryIndexStore index = new HistoryIndexStore( archive, new HistoryIndexCodec(), checkpoint); @@ -102,8 +102,8 @@ public void corruptIntentFailsBeforeCommitShrink() throws Exception { private static void initialize(Path archive) throws Exception { HistoryCommitMarker head; - try (HistorySegmentStore bodies = new HistorySegmentStore( - archive, new BlockHistoryCodec(), 4096); + try (HistoryBodyStore bodies = new PartitionedHistoryBodyStore( + archive, new BlockHistoryCodec(), 4096, null); HistoryIndexStore index = new HistoryIndexStore(archive, new HistoryIndexCodec()); HistoryCommitStore commits = new HistoryCommitStore( archive, new HistoryCommitMarkerCodec())) { @@ -127,7 +127,7 @@ archive, new HistoryCommitMarkerCodec())) { private static void prepare(Path archive) throws Exception { ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archive, new HistoryCommitMarkerCodec()); - try (HistorySegmentStore bodies = new HistorySegmentStore( + try (HistoryBodyStore bodies = new PartitionedHistoryBodyStore( archive, new BlockHistoryCodec(), 4096, checkpoint); HistoryIndexStore index = new HistoryIndexStore( archive, new HistoryIndexCodec(), checkpoint); @@ -148,7 +148,7 @@ private static void assertHeads(Path archive, long checkpointEpoch, long commitE ArchiveHistoryScanAnchor checkpoint = ArchiveHistoryScanAnchor.load(archive, new HistoryCommitMarkerCodec()); assertEquals(checkpointEpoch, checkpoint.getMarker().getMeta().getEpoch()); - try (HistorySegmentStore bodies = new HistorySegmentStore( + try (HistoryBodyStore bodies = new PartitionedHistoryBodyStore( archive, new BlockHistoryCodec(), 4096, checkpoint); HistoryIndexStore index = new HistoryIndexStore( archive, new HistoryIndexCodec(), checkpoint); diff --git a/framework/src/test/java/org/tron/program/ArchiveStateDiagnosticTest.java b/framework/src/test/java/org/tron/program/ArchiveStateDiagnosticTest.java index b1ea4322724..19aaeae0474 100644 --- a/framework/src/test/java/org/tron/program/ArchiveStateDiagnosticTest.java +++ b/framework/src/test/java/org/tron/program/ArchiveStateDiagnosticTest.java @@ -7,6 +7,8 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @@ -25,10 +27,8 @@ import org.tron.core.db2.archive.ArchivePersistenceException; import org.tron.core.db2.archive.ArchiveStoreScope; import org.tron.core.db2.archive.HistoricalAccountAssetBalanceResolver; -import org.tron.core.db2.archive.HistoricalAccountAssetPrefixResolver; import org.tron.core.db2.archive.OldValue; import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; -import org.tron.core.db2.common.WrappedByteArray; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.exception.TronError; @@ -72,7 +72,7 @@ public void shouldRemainDisabledWithoutExplicitSystemProperty() { } @Test - public void shouldCompareExact27AndP66ThroughManagerRequests() { + public void shouldCompareExact27AndP66PointWithoutDeferredPrefix() { Manager manager = mock(Manager.class); SnapshotManager snapshotManager = mock(SnapshotManager.class); DynamicPropertiesStore properties = mock(DynamicPropertiesStore.class); @@ -94,8 +94,6 @@ public void shouldCompareExact27AndP66ThroughManagerRequests() { entry(assetKey, assetValue)).iterator()); when(accountAssetSource.getData(any(byte[].class))).thenAnswer(invocation -> Arrays.equals(assetKey, invocation.getArgument(0)) ? assetValue : null); - when(accountAssetStore.prefixQuery(any(byte[].class))).thenReturn(Collections.singletonMap( - WrappedByteArray.of(assetKey), assetValue)); List databases = new ArrayList<>(); for (String dbName : ArchiveStoreScope.getStateDatabases()) { @@ -137,18 +135,6 @@ public void shouldCompareExact27AndP66ThroughManagerRequests() { when(logical.getBalance()).thenReturn(9L); when(manager.getArchiveAccountAssetBalance(eq(BLOCK_NUMBER), any(byte[].class), eq(tokenId))) .thenReturn(logical); - HistoricalAccountAssetPrefixResolver.Balance prefixBalance = - mock(HistoricalAccountAssetPrefixResolver.Balance.class); - when(prefixBalance.getTokenId()).thenReturn(tokenId); - when(prefixBalance.getBalance()).thenReturn(9L); - HistoricalAccountAssetPrefixResolver.Result prefix = - mock(HistoricalAccountAssetPrefixResolver.Result.class); - when(prefix.isAccountPresent()).thenReturn(true); - when(prefix.getPhase()).thenReturn(Phase.P66_ON); - when(prefix.getBalances()).thenReturn(Collections.singletonList(prefixBalance)); - when(manager.getArchiveAccountAssets(eq(BLOCK_NUMBER), any(byte[].class), - any(HistoricalAccountAssetPrefixResolver.Limits.class))).thenReturn(prefix); - ArchiveStateDiagnostic.Report report = ArchiveStateDiagnostic.run(manager, snapshotManager); assertEquals(BLOCK_NUMBER, report.getBlockNumber()); @@ -157,7 +143,7 @@ public void shouldCompareExact27AndP66ThroughManagerRequests() { assertEquals(1, report.getAbsentCount()); assertEquals(Phase.P66_ON, report.getP66Phase()); assertEquals(9L, report.getP66Balance()); - assertEquals(1, report.getP66PrefixCount()); + verify(manager, never()).getArchiveAccountAssets(anyLong(), any(byte[].class), any()); } @Test From 8dc6bc9f126d8dbc730cade310453995b4af79d5 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 12:45:17 +0800 Subject: [PATCH 073/161] perf(trie): store layer node deltas --- .../core/db2/stateroot/PathStateLayer.java | 35 ++-- .../stateroot/PathStateLayerPublication.java | 1 + .../db2/stateroot/PathStateNodeStoreSet.java | 197 +++++++++++++++--- .../db2/stateroot/PathStateLayerTest.java | 23 ++ 4 files changed, 214 insertions(+), 42 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java index 390978b2a42..a1d948bbdd4 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java @@ -86,26 +86,27 @@ static PathStateLayer begin(PathStateStoreManifest manifest, admittedParent.getStateRoot(), admittedParent.getStateRoot(), transitionDigest); Path layerDirectory = admitted.getLayerDirectory(blockNumber, blockHash); admittedLimits.verifyCanBegin(admitted, layerDirectory); - try (PathStateNodeStoreSet parentStores = - PathStateNodeStoreSet.openPublished(admitted, admittedParent)) { + PathStateNodeStoreSet parentStores = + PathStateNodeStoreSet.openPublished(admitted, admittedParent); + PathStateNodeStoreSet childStores = null; + try { PathStateRoot parentRoot = parentStores.createRoot(); - PathStateNodeStoreSet childStores = PathStateNodeStoreSet.beginLayer(admitted, identity); + childStores = PathStateNodeStoreSet.beginLayer(admitted, identity, parentStores); + PathStateRoot childRoot = childStores.createRootFrom(parentStores.leafRecords(), + parentRoot.rootHash()); + return new PathStateLayer(admitted, + new PathStateLayerPublication(admitted, admittedLimits, faultHook), + childStores, childRoot, + admittedParent, blockNumber, blockHash, parentHash, timestamp, phase, + transitionDigest); + } catch (RuntimeException | IOException failure) { + PathStateNodeStoreSet owned = childStores == null ? parentStores : childStores; try { - PathStateRoot childRoot = childStores.createRootFrom(parentStores.leafRecords(), - parentRoot.rootHash()); - return new PathStateLayer(admitted, - new PathStateLayerPublication(admitted, admittedLimits, faultHook), - childStores, childRoot, - admittedParent, blockNumber, blockHash, parentHash, timestamp, phase, - transitionDigest); - } catch (RuntimeException failure) { - try { - childStores.close(); - } catch (IOException closeFailure) { - failure.addSuppressed(closeFailure); - } - throw failure; + owned.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); } + throw failure; } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java index d20fd714ab0..1b50163956c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java @@ -55,6 +55,7 @@ public synchronized PathStateRootMetadata publish(PathStateNodeStoreSet stores, throw new IllegalArgumentException("path-state LAYER node database directory mismatch"); } requireCurrentParentOrChild(layer); + nodeStores.releaseParentReadHandles(); limits.verifyAdmission(manifest, directory, layer, nodeStores.projectedLogicalBytes(layer)); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index ba5b03f6dd6..b155a4c5a1b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -28,6 +28,10 @@ public final class PathStateNodeStoreSet implements Closeable { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 'r', 'e', 'b', 'u', 'i', 'l', 'd'}; private static final int LEAF_DOMAIN = -2; + private static final int NODE_TOMBSTONE_DOMAIN = -3; + private static final byte[] NODE_TOMBSTONE_PREFIX = ByteBuffer.allocate(Integer.BYTES) + .putInt(NODE_TOMBSTONE_DOMAIN).array(); + private static final byte[] TOMBSTONE_VALUE = new byte[]{1}; private static final byte[] LEAF_PREFIX = ByteBuffer.allocate(Integer.BYTES) .putInt(LEAF_DOMAIN).array(); private static final int LEAF_KEY_LENGTH = Integer.BYTES * 2 + PathMerkleTrie.SECURE_KEY_LENGTH; @@ -38,6 +42,7 @@ public final class PathStateNodeStoreSet implements Closeable { private final Map pending = new LinkedHashMap<>(); private final Map persistedLeaves = new LinkedHashMap<>(); private final PathStateNativeNodeStore nativeStore; + private final PathStateNodeStoreSet parentStores; private final PathNodeStore superStore; private final byte[] manifestDigest; private final Kind kind; @@ -51,13 +56,14 @@ public final class PathStateNodeStoreSet implements Closeable { private boolean closed; private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, Kind kind, - PathStateRootMetadata expectedMetadata) + PathStateRootMetadata expectedMetadata, PathStateNodeStoreSet parentStores) throws IOException { this.directory = directory.resolve(NODES_DIRECTORY); this.scope = new PathStateCanonicalizer().participantScope(); this.manifestDigest = manifest.getIdentityDigest(); this.kind = kind; this.expectedMetadata = expectedMetadata; + this.parentStores = parentStores; this.sealed = Files.exists(directory.resolve(PathStateCurrentStore.METADATA_FILE), LinkOption.NOFOLLOW_LINKS); nativeStore = PathStateNativeNodeStore.open(this.directory, manifest.getEngine()); @@ -80,6 +86,7 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K requireRebuildCheckpointIdentity(rebuildCheckpoint); } loadPersistedLeaves(); + validateNodeTombstones(); if (progress == null && rebuildCheckpoint == null && !persistedLeaves.isEmpty()) { throw new IOException("path-state leaf inventory exists without native progress"); } @@ -100,7 +107,8 @@ public static PathStateNodeStoreSet openBase(PathStateStoreManifest manifest) Path metadataPath = admitted.getBaseDirectory().resolve(PathStateCurrentStore.METADATA_FILE); PathStateRootMetadata metadata = Files.exists(metadataPath, LinkOption.NOFOLLOW_LINKS) ? PathStateMetadataFile.load(metadataPath) : null; - return new PathStateNodeStoreSet(admitted.getBaseDirectory(), admitted, Kind.BASE, metadata); + return new PathStateNodeStoreSet(admitted.getBaseDirectory(), admitted, Kind.BASE, metadata, + null); } public static PathStateNodeStoreSet openLayer(PathStateStoreManifest manifest, @@ -115,7 +123,7 @@ public static PathStateNodeStoreSet openLayer(PathStateStoreManifest manifest, } Path layerDirectory = admitted.getLayerDirectory(layer.getBlockNumber(), layer.getBlockHash()); requireUnsealed(layerDirectory); - return new PathStateNodeStoreSet(layerDirectory, admitted, Kind.LAYER, layer); + return new PathStateNodeStoreSet(layerDirectory, admitted, Kind.LAYER, layer, null); } /** Opens the node database referenced by the verified current authority. */ @@ -127,7 +135,7 @@ public static PathStateNodeStoreSet openCurrent(PathStateStoreManifest manifest) } static PathStateNodeStoreSet beginLayer(PathStateStoreManifest manifest, - PathStateRootMetadata identity) throws IOException { + PathStateRootMetadata identity, PathStateNodeStoreSet parentStores) throws IOException { PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); PathStateRootMetadata layer = Objects.requireNonNull(identity, "identity"); if (layer.getKind() != Kind.LAYER @@ -136,7 +144,8 @@ static PathStateNodeStoreSet beginLayer(PathStateStoreManifest manifest, } Path directory = admitted.getLayerDirectory(layer.getBlockNumber(), layer.getBlockHash()); requireUnsealed(directory); - return new PathStateNodeStoreSet(directory, admitted, Kind.LAYER, null); + return new PathStateNodeStoreSet(directory, admitted, Kind.LAYER, null, + Objects.requireNonNull(parentStores, "parentStores")); } static PathStateNodeStoreSet openPublished(PathStateStoreManifest manifest, @@ -150,7 +159,14 @@ static PathStateNodeStoreSet openPublished(PathStateStoreManifest manifest, if (!Arrays.equals(stored.encode(), published.encode())) { throw new IOException("path-state published metadata differs from authority"); } - return new PathStateNodeStoreSet(owner, admitted, published.getKind(), published); + PathStateNodeStoreSet parent = published.getKind() == Kind.BASE ? null + : openPublished(admitted, loadParent(admitted, published)); + try { + return new PathStateNodeStoreSet(owner, admitted, published.getKind(), published, parent); + } catch (IOException | RuntimeException failure) { + closeAfterFailure(parent, failure); + throw failure; + } } /** Claims this database and restores its durable leaves when progress already exists. */ @@ -186,7 +202,13 @@ synchronized PathStateRoot createRootFrom(List parentL } PathStateRoot candidate = new PathStateRoot(scope, participant -> participantStores.get(participant.getDbName()), superStore); - candidate.initializeLeaves(parentLeaves, parentRoot); + if (parentStores == null) { + throw new IllegalStateException("path-state layer has no parent node overlay"); + } + candidate.restoreLeaves(parentLeaves, parentRoot); + if (!pending.isEmpty()) { + throw new IllegalStateException("path-state parent restore attempted to copy nodes"); + } root = candidate; rootClaimed = true; return root; @@ -213,12 +235,7 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti long nextLogicalBytes = projectedLogicalBytes(next); List mutations = new ArrayList<>(pending.size() + persistedLeaves.size() + 1); - for (Map.Entry entry : pending.entrySet()) { - byte[] value = entry.getValue(); - mutations.add(value == null - ? PathStateNativeNodeStore.BatchMutation.delete(entry.getKey().copy()) - : PathStateNativeNodeStore.BatchMutation.put(entry.getKey().copy(), value)); - } + appendPendingMutations(mutations); Map nextLeaves = leafMap(root.leafRecords()); for (BytesKey persisted : persistedLeaves.keySet()) { if (!nextLeaves.containsKey(persisted)) { @@ -298,10 +315,7 @@ synchronized long projectedLogicalBytes(PathStateRootMetadata metadata) throws I } long total = rebuildCheckpoint == null ? (logicalBytes == null ? 0 : logicalBytes) : rebuildLogicalBytes(); - for (Map.Entry entry : pending.entrySet()) { - byte[] key = entry.getKey().copy(); - total = replaceLogicalEntry(total, key, nativeStore.get(key), entry.getValue()); - } + total = projectedPendingBytes(total); Map nextLeaves = leafMap(root.leafRecords()); for (Map.Entry entry : persistedLeaves.entrySet()) { if (!nextLeaves.containsKey(entry.getKey())) { @@ -378,13 +392,40 @@ public Path getDirectory() { return directory; } + /** Releases inherited read handles after the child root has been frozen for publication. */ + synchronized void releaseParentReadHandles() throws IOException { + requireOpen(); + if (parentStores != null) { + parentStores.close(); + } + } + @Override public synchronized void close() throws IOException { if (closed) { return; } closed = true; - nativeStore.close(); + IOException failure = null; + try { + nativeStore.close(); + } catch (IOException closeFailure) { + failure = closeFailure; + } + if (parentStores != null) { + try { + parentStores.close(); + } catch (IOException closeFailure) { + if (failure == null) { + failure = closeFailure; + } else { + failure.addSuppressed(closeFailure); + } + } + } + if (failure != null) { + throw failure; + } } private void requireOpen() { @@ -399,7 +440,15 @@ private synchronized byte[] get(byte[] key) { byte[] value = pending.get(ownedKey); return value == null ? null : Arrays.copyOf(value, value.length); } - return nativeStore.get(ownedKey.copy()); + byte[] owned = ownedKey.copy(); + byte[] local = nativeStore.get(owned); + if (local != null) { + return local; + } + if (kind == Kind.LAYER && nativeStore.get(tombstoneKey(owned)) != null) { + return null; + } + return parentStores == null ? null : parentStores.get(owned); } private synchronized void put(byte[] key, byte[] value) { @@ -433,16 +482,40 @@ private void loadPersistedLeaves() throws IOException { } } + private void validateNodeTombstones() throws IOException { + List tombstones = + nativeStore.scanPrefix(NODE_TOMBSTONE_PREFIX); + if (!tombstones.isEmpty() && (kind != Kind.LAYER || progress == null)) { + throw new IOException("path-state node tombstones require durable LAYER progress"); + } + for (PathStateNativeNodeStore.KeyValue entry : tombstones) { + byte[] key = entry.getKey(); + if (key.length < Integer.BYTES * 2 + || ByteBuffer.wrap(key).getInt() != NODE_TOMBSTONE_DOMAIN + || !Arrays.equals(entry.getValue(), TOMBSTONE_VALUE)) { + throw new IOException("path-state node tombstone is malformed"); + } + byte[] nodeKey = Arrays.copyOfRange(key, Integer.BYTES, key.length); + int storeId = ByteBuffer.wrap(nodeKey).getInt(); + if (storeId < 0 || storeId > scope.getParticipants().size()) { + throw new IOException("path-state node tombstone has an unknown Store ID"); + } + for (int index = Integer.BYTES; index < nodeKey.length; index++) { + if (nodeKey[index] < 0 || nodeKey[index] > 15) { + throw new IOException("path-state node tombstone contains a non-nibble path"); + } + } + if (nativeStore.get(nodeKey) != null) { + throw new IOException("path-state node and tombstone coexist"); + } + } + } + private List durableStateMutations( byte[] rebuildValue) { List mutations = new ArrayList<>(pending.size() + persistedLeaves.size() + 1); - for (Map.Entry entry : pending.entrySet()) { - byte[] value = entry.getValue(); - mutations.add(value == null - ? PathStateNativeNodeStore.BatchMutation.delete(entry.getKey().copy()) - : PathStateNativeNodeStore.BatchMutation.put(entry.getKey().copy(), value)); - } + appendPendingMutations(mutations); Map nextLeaves = leafMap(root.leafRecords()); for (BytesKey persisted : persistedLeaves.keySet()) { if (!nextLeaves.containsKey(persisted)) { @@ -570,6 +643,80 @@ private static long replaceLogicalEntry(long total, byte[] key, byte[] previous, } } + private void appendPendingMutations( + List mutations) { + for (Map.Entry entry : pending.entrySet()) { + byte[] key = entry.getKey().copy(); + byte[] value = entry.getValue(); + if (kind == Kind.LAYER) { + mutations.add(value == null + ? PathStateNativeNodeStore.BatchMutation.delete(key) + : PathStateNativeNodeStore.BatchMutation.put(key, value)); + byte[] tombstone = tombstoneKey(key); + mutations.add(value == null + ? PathStateNativeNodeStore.BatchMutation.put(tombstone, TOMBSTONE_VALUE) + : PathStateNativeNodeStore.BatchMutation.delete(tombstone)); + } else { + mutations.add(value == null + ? PathStateNativeNodeStore.BatchMutation.delete(key) + : PathStateNativeNodeStore.BatchMutation.put(key, value)); + } + } + } + + private long projectedPendingBytes(long total) throws IOException { + long projected = total; + for (Map.Entry entry : pending.entrySet()) { + byte[] key = entry.getKey().copy(); + byte[] value = entry.getValue(); + projected = replaceLogicalEntry(projected, key, nativeStore.get(key), value); + if (kind == Kind.LAYER) { + byte[] tombstone = tombstoneKey(key); + projected = replaceLogicalEntry(projected, tombstone, nativeStore.get(tombstone), + value == null ? TOMBSTONE_VALUE : null); + } + } + return projected; + } + + private static byte[] tombstoneKey(byte[] nodeKey) { + return ByteBuffer.allocate(Integer.BYTES + nodeKey.length) + .putInt(NODE_TOMBSTONE_DOMAIN) + .put(nodeKey) + .array(); + } + + private static PathStateRootMetadata loadParent(PathStateStoreManifest manifest, + PathStateRootMetadata child) throws IOException { + if (child.getKind() != Kind.LAYER || child.getBlockNumber() == 0) { + throw new IOException("path-state node overlay has an invalid child identity"); + } + PathStateRootMetadata base = PathStateMetadataFile.load( + manifest.getBaseDirectory().resolve(PathStateCurrentStore.METADATA_FILE)); + PathStateRootMetadata parent = base.getBlockNumber() == child.getBlockNumber() - 1 + ? base : PathStateMetadataFile.load(manifest.getLayerDirectory( + child.getBlockNumber() - 1, child.getParentHash()) + .resolve(PathStateCurrentStore.METADATA_FILE)); + if (child.getBlockNumber() != parent.getBlockNumber() + 1 + || !Arrays.equals(child.getParentHash(), parent.getBlockHash()) + || !Arrays.equals(child.getParentStateRoot(), parent.getStateRoot()) + || !Arrays.equals(parent.getFormatDigest(), manifest.getIdentityDigest())) { + throw new IOException("path-state node overlay parent identity mismatch"); + } + return parent; + } + + private static void closeAfterFailure(PathStateNodeStoreSet stores, Throwable failure) { + if (stores == null) { + return; + } + try { + stores.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + private static void requireUnsealed(Path directory) throws IOException { Path metadata = directory.resolve(PathStateCurrentStore.METADATA_FILE); if (Files.exists(metadata, LinkOption.NOFOLLOW_LINKS)) { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java index 0594ba9dca4..9d6894d367b 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import java.io.File; import java.io.IOException; @@ -43,6 +44,10 @@ public void layersInheritPublishedParentAndRestoreCurrentAcrossReopen() throws E } assertCurrentRoot(fixture.manifest, first, firstRoot); + assertTrue(nodeEntryCount(fixture.manifest.getLayerDirectory(101, first.getBlockHash()), + engine) < nodeEntryCount(fixture.manifest.getBaseDirectory(), engine)); + assertTrue(nodeTombstoneCount( + fixture.manifest.getLayerDirectory(101, first.getBlockHash()), engine) > 0); PathStateRootMetadata second; byte[] secondRoot; @@ -140,6 +145,24 @@ private static byte[] durableLeafKey(int storeId, byte[] secureKey) { .array(); } + private static long nodeEntryCount(Path owner, Engine engine) throws Exception { + try (PathStateNativeNodeStore store = PathStateNativeNodeStore.open( + owner.resolve(PathStateNodeStoreSet.NODES_DIRECTORY), engine)) { + return store.scanAll().stream() + .filter(entry -> java.nio.ByteBuffer.wrap(entry.getKey()).getInt() >= 0) + .count(); + } + } + + private static long nodeTombstoneCount(Path owner, Engine engine) throws Exception { + try (PathStateNativeNodeStore store = PathStateNativeNodeStore.open( + owner.resolve(PathStateNodeStoreSet.NODES_DIRECTORY), engine)) { + return store.scanAll().stream() + .filter(entry -> java.nio.ByteBuffer.wrap(entry.getKey()).getInt() == -3) + .count(); + } + } + private static byte[] bytes(int seed) { byte[] value = new byte[32]; for (int index = 0; index < value.length; index++) { From 85a7c6de152b8b62e0ec3dfcf7891c587a7cd766 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 12:58:35 +0800 Subject: [PATCH 074/161] perf(trie): store layer leaf deltas --- .../db2/stateroot/PathStateNodeStoreSet.java | 201 ++++++++++++++---- .../db2/stateroot/PathStateLayerTest.java | 21 ++ 2 files changed, 183 insertions(+), 39 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index b155a4c5a1b..014588e628c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -9,9 +9,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; /** Exact-27 participant and super-trie namespace views over one BASE or LAYER native database. */ @@ -27,20 +29,29 @@ public final class PathStateNodeStoreSet implements Closeable { private static final byte[] REBUILD_CHECKPOINT_KEY = new byte[]{ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 'r', 'e', 'b', 'u', 'i', 'l', 'd'}; + private static final byte[] LEAF_OVERLAY_KEY = new byte[]{ + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + 'l', 'e', 'a', 'f', '-', 'o', 'v', 'e', 'r', 'l', 'a', 'y'}; + private static final byte[] LEAF_OVERLAY_VALUE = new byte[]{1}; private static final int LEAF_DOMAIN = -2; private static final int NODE_TOMBSTONE_DOMAIN = -3; + private static final int LEAF_TOMBSTONE_DOMAIN = -4; private static final byte[] NODE_TOMBSTONE_PREFIX = ByteBuffer.allocate(Integer.BYTES) .putInt(NODE_TOMBSTONE_DOMAIN).array(); private static final byte[] TOMBSTONE_VALUE = new byte[]{1}; private static final byte[] LEAF_PREFIX = ByteBuffer.allocate(Integer.BYTES) .putInt(LEAF_DOMAIN).array(); private static final int LEAF_KEY_LENGTH = Integer.BYTES * 2 + PathMerkleTrie.SECURE_KEY_LENGTH; + private static final byte[] LEAF_TOMBSTONE_PREFIX = ByteBuffer.allocate(Integer.BYTES) + .putInt(LEAF_TOMBSTONE_DOMAIN).array(); private final Path directory; private final PathStateParticipantScope scope; private final Map participantStores = new LinkedHashMap<>(); private final Map pending = new LinkedHashMap<>(); + private final Map localLeaves = new LinkedHashMap<>(); private final Map persistedLeaves = new LinkedHashMap<>(); + private final Set leafTombstones = new LinkedHashSet<>(); private final PathStateNativeNodeStore nativeStore; private final PathStateNodeStoreSet parentStores; private final PathNodeStore superStore; @@ -51,6 +62,7 @@ public final class PathStateNodeStoreSet implements Closeable { private PathStateRootMetadata progress; private PathStateRebuildCheckpoint rebuildCheckpoint; private Long logicalBytes; + private boolean leafOverlay; private PathStateRoot root; private boolean rootClaimed; private boolean closed; @@ -71,6 +83,7 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K progress = decodeProgress(nativeStore.get(PROGRESS_KEY)); logicalBytes = decodeLogicalBytes(nativeStore.get(LOGICAL_BYTES_KEY)); rebuildCheckpoint = decodeRebuildCheckpoint(nativeStore.get(REBUILD_CHECKPOINT_KEY)); + byte[] leafOverlayValue = nativeStore.get(LEAF_OVERLAY_KEY); if ((progress == null) != (logicalBytes == null)) { throw new IOException("path-state native progress and logical bytes marker differ"); } @@ -85,9 +98,19 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K if (rebuildCheckpoint != null) { requireRebuildCheckpointIdentity(rebuildCheckpoint); } + if (leafOverlayValue != null + && (kind != Kind.LAYER || progress == null + || !Arrays.equals(leafOverlayValue, LEAF_OVERLAY_VALUE))) { + throw new IOException("path-state leaf overlay marker is invalid"); + } + leafOverlay = leafOverlayValue != null + || kind == Kind.LAYER && progress == null && parentStores != null; + inheritParentLeaves(); loadPersistedLeaves(); + loadLeafTombstones(); validateNodeTombstones(); - if (progress == null && rebuildCheckpoint == null && !persistedLeaves.isEmpty()) { + if (progress == null && rebuildCheckpoint == null + && (!localLeaves.isEmpty() || !leafTombstones.isEmpty())) { throw new IOException("path-state leaf inventory exists without native progress"); } for (PathStateParticipant participant : scope.getParticipants()) { @@ -197,7 +220,7 @@ synchronized PathStateRoot createRootFrom(List parentL if (rootClaimed) { throw new IllegalStateException("path-state node database set already has a trie owner"); } - if (progress != null || !persistedLeaves.isEmpty()) { + if (progress != null || !localLeaves.isEmpty() || !leafTombstones.isEmpty()) { throw new IllegalStateException("path-state layer already contains durable state"); } PathStateRoot candidate = new PathStateRoot(scope, @@ -237,18 +260,12 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti new ArrayList<>(pending.size() + persistedLeaves.size() + 1); appendPendingMutations(mutations); Map nextLeaves = leafMap(root.leafRecords()); - for (BytesKey persisted : persistedLeaves.keySet()) { - if (!nextLeaves.containsKey(persisted)) { - mutations.add(PathStateNativeNodeStore.BatchMutation.delete(persisted.copy())); - } - } - for (Map.Entry entry : nextLeaves.entrySet()) { - if (!Arrays.equals(persistedLeaves.get(entry.getKey()), entry.getValue())) { - mutations.add(PathStateNativeNodeStore.BatchMutation.put( - entry.getKey().copy(), entry.getValue())); - } - } + appendLeafMutations(mutations, nextLeaves); mutations.add(PathStateNativeNodeStore.BatchMutation.put(PROGRESS_KEY, next.encode())); + if (leafOverlay) { + mutations.add(PathStateNativeNodeStore.BatchMutation.put( + LEAF_OVERLAY_KEY, LEAF_OVERLAY_VALUE)); + } mutations.add(PathStateNativeNodeStore.BatchMutation.put(LOGICAL_BYTES_KEY, ByteBuffer.allocate(Long.BYTES).putLong(nextLogicalBytes).array())); if (rebuildCheckpoint != null) { @@ -256,8 +273,7 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti } nativeStore.writeBatch(mutations); pending.clear(); - persistedLeaves.clear(); - persistedLeaves.putAll(nextLeaves); + recordCommittedLeaves(nextLeaves); progress = next; rebuildCheckpoint = null; logicalBytes = nextLogicalBytes; @@ -293,8 +309,7 @@ synchronized void checkpointRebuild(PathStateRebuildCheckpoint checkpoint) throw durableStateMutations(next.encode()); nativeStore.writeBatch(mutations); pending.clear(); - persistedLeaves.clear(); - persistedLeaves.putAll(leafMap(root.leafRecords())); + recordCommittedLeaves(leafMap(root.leafRecords())); rebuildCheckpoint = next; } @@ -317,16 +332,10 @@ synchronized long projectedLogicalBytes(PathStateRootMetadata metadata) throws I : rebuildLogicalBytes(); total = projectedPendingBytes(total); Map nextLeaves = leafMap(root.leafRecords()); - for (Map.Entry entry : persistedLeaves.entrySet()) { - if (!nextLeaves.containsKey(entry.getKey())) { - total = replaceLogicalEntry(total, entry.getKey().copy(), entry.getValue(), null); - } - } - for (Map.Entry entry : nextLeaves.entrySet()) { - byte[] previous = persistedLeaves.get(entry.getKey()); - if (!Arrays.equals(previous, entry.getValue())) { - total = replaceLogicalEntry(total, entry.getKey().copy(), previous, entry.getValue()); - } + total = projectedLeafBytes(total, nextLeaves); + if (leafOverlay) { + total = replaceLogicalEntry(total, LEAF_OVERLAY_KEY, + nativeStore.get(LEAF_OVERLAY_KEY), LEAF_OVERLAY_VALUE); } total = replaceLogicalEntry(total, PROGRESS_KEY, progress == null ? null : progress.encode(), next.encode()); @@ -478,7 +487,47 @@ private void loadPersistedLeaves() throws IOException { } int storeId = ByteBuffer.wrap(key, Integer.BYTES, Integer.BYTES).getInt(); requireParticipant(storeId); - persistedLeaves.put(new BytesKey(key), entry.getValue()); + BytesKey leafKey = new BytesKey(key); + if (localLeaves.put(leafKey, entry.getValue()) != null) { + throw new IOException("duplicate path-state durable leaf key"); + } + persistedLeaves.put(leafKey, entry.getValue()); + } + } + + private void inheritParentLeaves() { + if (!leafOverlay || parentStores == null) { + return; + } + for (Map.Entry entry : parentStores.persistedLeaves.entrySet()) { + persistedLeaves.put(entry.getKey(), Arrays.copyOf(entry.getValue(), entry.getValue().length)); + } + } + + private void loadLeafTombstones() throws IOException { + List tombstones = + nativeStore.scanPrefix(LEAF_TOMBSTONE_PREFIX); + if (!tombstones.isEmpty() && (!leafOverlay || kind != Kind.LAYER || progress == null)) { + throw new IOException("path-state leaf tombstones require durable LAYER progress"); + } + for (PathStateNativeNodeStore.KeyValue entry : tombstones) { + byte[] key = entry.getKey(); + if (key.length != LEAF_KEY_LENGTH + || ByteBuffer.wrap(key).getInt() != LEAF_TOMBSTONE_DOMAIN + || !Arrays.equals(entry.getValue(), TOMBSTONE_VALUE)) { + throw new IOException("path-state leaf tombstone is malformed"); + } + int storeId = ByteBuffer.wrap(key, Integer.BYTES, Integer.BYTES).getInt(); + requireParticipant(storeId); + BytesKey leafKey = new BytesKey(leafKeyFromTombstone(key)); + if (localLeaves.containsKey(leafKey)) { + throw new IOException("path-state leaf and tombstone coexist"); + } + if (!persistedLeaves.containsKey(leafKey)) { + throw new IOException("path-state leaf tombstone does not mask a parent leaf"); + } + leafTombstones.add(leafKey); + persistedLeaves.remove(leafKey); } } @@ -517,17 +566,7 @@ private List durableStateMutations( new ArrayList<>(pending.size() + persistedLeaves.size() + 1); appendPendingMutations(mutations); Map nextLeaves = leafMap(root.leafRecords()); - for (BytesKey persisted : persistedLeaves.keySet()) { - if (!nextLeaves.containsKey(persisted)) { - mutations.add(PathStateNativeNodeStore.BatchMutation.delete(persisted.copy())); - } - } - for (Map.Entry entry : nextLeaves.entrySet()) { - if (!Arrays.equals(persistedLeaves.get(entry.getKey()), entry.getValue())) { - mutations.add(PathStateNativeNodeStore.BatchMutation.put( - entry.getKey().copy(), entry.getValue())); - } - } + appendLeafMutations(mutations, nextLeaves); mutations.add(PathStateNativeNodeStore.BatchMutation.put(REBUILD_CHECKPOINT_KEY, rebuildValue)); return mutations; @@ -664,6 +703,78 @@ private void appendPendingMutations( } } + private void appendLeafMutations(List mutations, + Map nextLeaves) { + for (BytesKey persisted : persistedLeaves.keySet()) { + if (!nextLeaves.containsKey(persisted)) { + appendLeafMutation(mutations, persisted.copy(), null); + } + } + for (Map.Entry entry : nextLeaves.entrySet()) { + if (!Arrays.equals(persistedLeaves.get(entry.getKey()), entry.getValue())) { + appendLeafMutation(mutations, entry.getKey().copy(), entry.getValue()); + } + } + } + + private void appendLeafMutation(List mutations, + byte[] key, byte[] value) { + mutations.add(value == null + ? PathStateNativeNodeStore.BatchMutation.delete(key) + : PathStateNativeNodeStore.BatchMutation.put(key, value)); + if (leafOverlay) { + byte[] tombstone = leafTombstoneKey(key); + mutations.add(value == null + ? PathStateNativeNodeStore.BatchMutation.put(tombstone, TOMBSTONE_VALUE) + : PathStateNativeNodeStore.BatchMutation.delete(tombstone)); + } + } + + private long projectedLeafBytes(long total, Map nextLeaves) + throws IOException { + long projected = total; + for (BytesKey persisted : persistedLeaves.keySet()) { + if (!nextLeaves.containsKey(persisted)) { + projected = projectedLeafMutation(projected, persisted.copy(), null); + } + } + for (Map.Entry entry : nextLeaves.entrySet()) { + if (!Arrays.equals(persistedLeaves.get(entry.getKey()), entry.getValue())) { + projected = projectedLeafMutation(projected, entry.getKey().copy(), entry.getValue()); + } + } + return projected; + } + + private long projectedLeafMutation(long total, byte[] key, byte[] value) throws IOException { + long projected = replaceLogicalEntry(total, key, nativeStore.get(key), value); + if (leafOverlay) { + byte[] tombstone = leafTombstoneKey(key); + projected = replaceLogicalEntry(projected, tombstone, nativeStore.get(tombstone), + value == null ? TOMBSTONE_VALUE : null); + } + return projected; + } + + private void recordCommittedLeaves(Map nextLeaves) { + for (BytesKey persisted : persistedLeaves.keySet()) { + if (!nextLeaves.containsKey(persisted)) { + localLeaves.remove(persisted); + if (leafOverlay) { + leafTombstones.add(persisted); + } + } + } + for (Map.Entry entry : nextLeaves.entrySet()) { + if (!Arrays.equals(persistedLeaves.get(entry.getKey()), entry.getValue())) { + localLeaves.put(entry.getKey(), Arrays.copyOf(entry.getValue(), entry.getValue().length)); + leafTombstones.remove(entry.getKey()); + } + } + persistedLeaves.clear(); + persistedLeaves.putAll(nextLeaves); + } + private long projectedPendingBytes(long total) throws IOException { long projected = total; for (Map.Entry entry : pending.entrySet()) { @@ -686,6 +797,18 @@ private static byte[] tombstoneKey(byte[] nodeKey) { .array(); } + private static byte[] leafTombstoneKey(byte[] leafKey) { + byte[] tombstone = Arrays.copyOf(leafKey, leafKey.length); + ByteBuffer.wrap(tombstone).putInt(LEAF_TOMBSTONE_DOMAIN); + return tombstone; + } + + private static byte[] leafKeyFromTombstone(byte[] tombstoneKey) { + byte[] leafKey = Arrays.copyOf(tombstoneKey, tombstoneKey.length); + ByteBuffer.wrap(leafKey).putInt(LEAF_DOMAIN); + return leafKey; + } + private static PathStateRootMetadata loadParent(PathStateStoreManifest manifest, PathStateRootMetadata child) throws IOException { if (child.getKind() != Kind.LAYER || child.getBlockNumber() == 0) { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java index 9d6894d367b..f9057c8c638 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java @@ -48,6 +48,10 @@ public void layersInheritPublishedParentAndRestoreCurrentAcrossReopen() throws E engine) < nodeEntryCount(fixture.manifest.getBaseDirectory(), engine)); assertTrue(nodeTombstoneCount( fixture.manifest.getLayerDirectory(101, first.getBlockHash()), engine) > 0); + assertTrue(leafEntryCount(fixture.manifest.getLayerDirectory(101, first.getBlockHash()), + engine) < leafEntryCount(fixture.manifest.getBaseDirectory(), engine)); + assertTrue(leafTombstoneCount( + fixture.manifest.getLayerDirectory(101, first.getBlockHash()), engine) > 0); PathStateRootMetadata second; byte[] secondRoot; @@ -163,6 +167,23 @@ private static long nodeTombstoneCount(Path owner, Engine engine) throws Excepti } } + private static long leafEntryCount(Path owner, Engine engine) throws Exception { + return domainEntryCount(owner, engine, -2); + } + + private static long leafTombstoneCount(Path owner, Engine engine) throws Exception { + return domainEntryCount(owner, engine, -4); + } + + private static long domainEntryCount(Path owner, Engine engine, int domain) throws Exception { + try (PathStateNativeNodeStore store = PathStateNativeNodeStore.open( + owner.resolve(PathStateNodeStoreSet.NODES_DIRECTORY), engine)) { + return store.scanAll().stream() + .filter(entry -> java.nio.ByteBuffer.wrap(entry.getKey()).getInt() == domain) + .count(); + } + } + private static byte[] bytes(int seed) { byte[] value = new byte[32]; for (int index = 0; index < value.length; index++) { From 4caa178ba413f3c700a18b9b6d7f31e30a6d400b Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 13:08:56 +0800 Subject: [PATCH 075/161] perf(trie): reuse immutable parent state --- .../core/db2/stateroot/PathMerkleTrie.java | 146 +++++++++++++++--- .../core/db2/stateroot/PathStateLayer.java | 36 ++++- .../db2/stateroot/PathStateNodeStoreSet.java | 24 +++ .../core/db2/stateroot/PathStateRoot.java | 57 ++++++- .../db2/stateroot/PathStateLayerTest.java | 8 +- 5 files changed, 245 insertions(+), 26 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index c296e9f0411..1c7754946f6 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -34,10 +34,13 @@ public final class PathMerkleTrie { private final PathNodeStore nodeStore; private final Map leaves = new TreeMap<>(UNSIGNED_KEY_COMPARATOR); private final IdentityHashMap materializedNodes = new IdentityHashMap<>(); + private Snapshot inheritedSnapshot; private Node rootNode; private Node materializedRoot; private byte[] rootHash = Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length); + private int leafCount; private boolean dirty; + private boolean frozen; private int lastNodePuts; private int lastNodeDeletes; @@ -46,25 +49,34 @@ public PathMerkleTrie(PathNodeStore nodeStore) { } public synchronized void put(byte[] secureKey, byte[] encodedValue) { + requireMutable(); BytesKey key = secureKey(secureKey); byte[] value = nonEmpty(encodedValue, "encodedValue"); - byte[] previous = leaves.put(key, value); - if (!Arrays.equals(previous, value)) { - rootNode = update(rootNode, toNibbles(key.bytes), 0, value); - dirty = true; + byte[] previous = leafValue(key); + if (Arrays.equals(previous, value)) { + return; + } + leaves.put(key, value); + if (previous == null) { + leafCount++; } + rootNode = update(rootNode, toNibbles(key.bytes), 0, value); + dirty = true; } public synchronized void delete(byte[] secureKey) { + requireMutable(); BytesKey key = secureKey(secureKey); - if (leaves.remove(key) != null) { + if (leafValue(key) != null) { + leaves.put(key, null); + leafCount--; rootNode = update(rootNode, toNibbles(key.bytes), 0, null); dirty = true; } } public synchronized byte[] get(byte[] secureKey) { - byte[] value = leaves.get(secureKey(secureKey)); + byte[] value = leafValue(secureKey(secureKey)); return value == null ? null : Arrays.copyOf(value, value.length); } @@ -77,7 +89,7 @@ public synchronized byte[] rootHash() { } public synchronized int size() { - return leaves.size(); + return leafCount; } synchronized int getLastNodePuts() { @@ -89,13 +101,32 @@ synchronized int getLastNodeDeletes() { } synchronized List leafEntries() { - List entries = new ArrayList<>(leaves.size()); - for (Map.Entry entry : leaves.entrySet()) { + Map effective = effectiveLeaves(); + List entries = new ArrayList<>(effective.size()); + for (Map.Entry entry : effective.entrySet()) { entries.add(new LeafEntry(entry.getKey().copy(), entry.getValue())); } return entries; } + synchronized Snapshot snapshot() { + rootHash(); + frozen = true; + return new Snapshot(inheritedSnapshot, leaves, materializedNodes, rootNode, rootHash, + leafCount); + } + + static PathMerkleTrie fromSnapshot(PathNodeStore nodeStore, Snapshot snapshot) { + Snapshot parent = Objects.requireNonNull(snapshot, "snapshot"); + PathMerkleTrie trie = new PathMerkleTrie(nodeStore); + trie.inheritedSnapshot = parent; + trie.rootNode = parent.rootNode; + trie.materializedRoot = parent.rootNode; + trie.rootHash = Arrays.copyOf(parent.rootHash, parent.rootHash.length); + trie.leafCount = parent.leafCount; + return trie; + } + /** Initializes an empty trie from canonical leaves and writes its complete path-node set. */ synchronized void initializeLeaves(Collection entries) { importLeaves(entries, "initialized"); @@ -120,7 +151,8 @@ synchronized void restoreLeaves(Collection entries) { } private void importLeaves(Collection entries, String operation) { - if (!leaves.isEmpty() || rootNode != null || materializedRoot != null || dirty) { + if (!leaves.isEmpty() || inheritedSnapshot != null || rootNode != null + || materializedRoot != null || dirty) { throw new IllegalStateException("path trie is not empty before leaf " + operation); } for (LeafEntry entry : Objects.requireNonNull(entries, "entries")) { @@ -129,6 +161,7 @@ private void importLeaves(Collection entries, String operation) { nonEmpty(present.encodedValue, "encodedValue")) != null) { throw new IllegalArgumentException("duplicate " + operation + " path-state leaf"); } + leafCount++; } } @@ -138,9 +171,6 @@ public synchronized void verifyNodeStore() { throw new IllegalStateException("cannot verify a dirty path trie"); } Map expectedNodes = collectNodes(rootNode); - if (materializedNodes.size() != expectedNodes.size()) { - throw new IllegalStateException("materialized path set does not match current leaves"); - } for (Map.Entry entry : expectedNodes.entrySet()) { if (!Arrays.equals(nodeStore.get(entry.getKey().bytes), entry.getValue())) { throw new IllegalStateException("missing or corrupt materialized path node"); @@ -178,7 +208,7 @@ private void collectAdditions(Node node, byte[] path, if (node == null) { return; } - BytesKey oldPath = materializedNodes.get(node); + BytesKey oldPath = materializedPath(node); if (oldPath != null) { if (!Arrays.equals(oldPath.bytes, path)) { throw new IllegalStateException("path-local update moved an unchanged subtree"); @@ -196,7 +226,7 @@ private void collectRemovals(Node node, IdentityHashMap retained, if (node == null || retained.containsKey(node)) { return; } - BytesKey path = materializedNodes.get(node); + BytesKey path = materializedPath(node); if (path == null) { throw new IllegalStateException("path-local update lost a materialized node identity"); } @@ -206,16 +236,56 @@ private void collectRemovals(Node node, IdentityHashMap retained, } private Node buildTree() { - if (leaves.isEmpty()) { + Map effective = effectiveLeaves(); + if (effective.isEmpty()) { return null; } - List entries = new ArrayList<>(leaves.size()); - for (Map.Entry entry : leaves.entrySet()) { + List entries = new ArrayList<>(effective.size()); + for (Map.Entry entry : effective.entrySet()) { entries.add(new Leaf(toNibbles(entry.getKey().bytes), entry.getValue())); } return build(entries, 0); } + private byte[] leafValue(BytesKey key) { + if (leaves.containsKey(key)) { + return leaves.get(key); + } + return inheritedSnapshot == null ? null : inheritedSnapshot.leafValue(key); + } + + private Map effectiveLeaves() { + Map effective = new TreeMap<>(UNSIGNED_KEY_COMPARATOR); + if (inheritedSnapshot != null) { + inheritedSnapshot.populateLeaves(effective); + } + applyLeaves(effective, leaves); + return effective; + } + + private BytesKey materializedPath(Node node) { + BytesKey path = materializedNodes.get(node); + return path != null || inheritedSnapshot == null + ? path : inheritedSnapshot.materializedPath(node); + } + + private void requireMutable() { + if (frozen) { + throw new IllegalStateException("path trie is frozen as an immutable parent snapshot"); + } + } + + private static void applyLeaves(Map target, + Map changes) { + for (Map.Entry entry : changes.entrySet()) { + if (entry.getValue() == null) { + target.remove(entry.getKey()); + } else { + target.put(entry.getKey(), entry.getValue()); + } + } + } + private static Node build(List entries, int depth) { if (entries.size() == 1) { Leaf leaf = entries.get(0); @@ -619,6 +689,46 @@ private interface NodeVisitor { void visit(Node node, byte[] path); } + static final class Snapshot { + + private final Snapshot parent; + private final Map leaves; + private final IdentityHashMap materializedNodes; + private final Node rootNode; + private final byte[] rootHash; + private final int leafCount; + + private Snapshot(Snapshot parent, Map leaves, + IdentityHashMap materializedNodes, Node rootNode, byte[] rootHash, + int leafCount) { + this.parent = parent; + this.leaves = leaves; + this.materializedNodes = materializedNodes; + this.rootNode = rootNode; + this.rootHash = Arrays.copyOf(rootHash, rootHash.length); + this.leafCount = leafCount; + } + + private byte[] leafValue(BytesKey key) { + if (leaves.containsKey(key)) { + return leaves.get(key); + } + return parent == null ? null : parent.leafValue(key); + } + + private void populateLeaves(Map target) { + if (parent != null) { + parent.populateLeaves(target); + } + applyLeaves(target, leaves); + } + + private BytesKey materializedPath(Node node) { + BytesKey path = materializedNodes.get(node); + return path != null || parent == null ? path : parent.materializedPath(node); + } + } + static final class LeafEntry { private final byte[] secureKey; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java index a1d948bbdd4..162cdfe3b68 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java @@ -58,18 +58,37 @@ public static PathStateLayer begin(PathStateStoreManifest manifest, transitionDigest, limits, stage -> { }); } + /** Begins a child from an explicitly retained, immutable in-process parent trie snapshot. */ + public static PathStateLayer beginFromSnapshot(PathStateStoreManifest manifest, + PathStateRootMetadata parent, PathStateRoot.Snapshot parentSnapshot, long blockNumber, + byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, + byte[] transitionDigest) throws IOException { + return begin(manifest, parent, blockNumber, blockHash, parentHash, timestamp, phase, + transitionDigest, PathStateLayerLimits.defaults(), stage -> { }, + Objects.requireNonNull(parentSnapshot, "parentSnapshot")); + } + static PathStateLayer begin(PathStateStoreManifest manifest, PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, byte[] transitionDigest, PathStateLayerPublication.FaultHook faultHook) throws IOException { return begin(manifest, parent, blockNumber, blockHash, parentHash, timestamp, phase, - transitionDigest, PathStateLayerLimits.defaults(), faultHook); + transitionDigest, PathStateLayerLimits.defaults(), faultHook, null); } static PathStateLayer begin(PathStateStoreManifest manifest, PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, byte[] transitionDigest, PathStateLayerLimits limits, PathStateLayerPublication.FaultHook faultHook) throws IOException { + return begin(manifest, parent, blockNumber, blockHash, parentHash, timestamp, phase, + transitionDigest, limits, faultHook, null); + } + + private static PathStateLayer begin(PathStateStoreManifest manifest, + PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase, byte[] transitionDigest, PathStateLayerLimits limits, + PathStateLayerPublication.FaultHook faultHook, PathStateRoot.Snapshot parentSnapshot) + throws IOException { PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); PathStateLayerLimits admittedLimits = Objects.requireNonNull(limits, "limits"); PathStateRootMetadata admittedParent = Objects.requireNonNull(parent, "parent"); @@ -90,10 +109,11 @@ static PathStateLayer begin(PathStateStoreManifest manifest, PathStateNodeStoreSet.openPublished(admitted, admittedParent); PathStateNodeStoreSet childStores = null; try { - PathStateRoot parentRoot = parentStores.createRoot(); + PathStateRoot parentRoot = parentSnapshot == null ? parentStores.createRoot() : null; childStores = PathStateNodeStoreSet.beginLayer(admitted, identity, parentStores); - PathStateRoot childRoot = childStores.createRootFrom(parentStores.leafRecords(), - parentRoot.rootHash()); + PathStateRoot childRoot = parentSnapshot == null + ? childStores.createRootFrom(parentStores.leafRecords(), parentRoot.rootHash()) + : childStores.createRootFrom(parentSnapshot, admittedParent.getStateRoot()); return new PathStateLayer(admitted, new PathStateLayerPublication(admitted, admittedLimits, faultHook), childStores, childRoot, @@ -132,6 +152,14 @@ public synchronized byte[] rootHash() { return root.rootHash(); } + /** Returns a detached immutable trie snapshot only after this layer is durably CURRENT. */ + public synchronized PathStateRoot.Snapshot snapshot() { + if (committed == null) { + throw new IllegalStateException("path-state layer is not committed"); + } + return root.snapshot(); + } + @Override public synchronized void close() throws IOException { stores.close(); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index 014588e628c..d5bbb262c9a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -237,6 +237,30 @@ synchronized PathStateRoot createRootFrom(List parentL return root; } + synchronized PathStateRoot createRootFrom(PathStateRoot.Snapshot snapshot, + byte[] parentRoot) { + requireOpen(); + if (rootClaimed) { + throw new IllegalStateException("path-state node database set already has a trie owner"); + } + if (progress != null || !localLeaves.isEmpty() || !leafTombstones.isEmpty()) { + throw new IllegalStateException("path-state layer already contains durable state"); + } + if (parentStores == null + || !Arrays.equals(snapshot.getStateRoot(), Objects.requireNonNull(parentRoot, + "parentRoot"))) { + throw new IllegalArgumentException("path-state parent snapshot root mismatch"); + } + PathStateRoot candidate = PathStateRoot.fromSnapshot(scope, + participant -> participantStores.get(participant.getDbName()), superStore, snapshot); + if (!pending.isEmpty()) { + throw new IllegalStateException("path-state snapshot fork attempted to copy nodes"); + } + root = candidate; + rootClaimed = true; + return root; + } + synchronized List leafRecords() { requireOpen(); if (root == null) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index 7f5a0ef7b5e..8bf7e4ab6fc 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -36,6 +36,11 @@ public final class PathStateRoot { public PathStateRoot(PathStateParticipantScope scope, PathNodeStoreFactory storeFactory, PathNodeStore superNodeStore) { + this(scope, storeFactory, superNodeStore, null); + } + + private PathStateRoot(PathStateParticipantScope scope, PathNodeStoreFactory storeFactory, + PathNodeStore superNodeStore, Snapshot snapshot) { this.scope = Objects.requireNonNull(scope, "scope"); PathNodeStoreFactory factory = Objects.requireNonNull(storeFactory, "storeFactory"); Set uniqueStores = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -45,13 +50,27 @@ public PathStateRoot(PathStateParticipantScope scope, PathNodeStoreFactory store if (!uniqueStores.add(nodeStore)) { throw new IllegalArgumentException("participant node Stores must have distinct identities"); } - participantTries.put(participant.getDbName(), new PathMerkleTrie(nodeStore)); + PathMerkleTrie.Snapshot trieSnapshot = snapshot == null ? null + : snapshot.participants.get(participant.getDbName()); + if (snapshot != null && trieSnapshot == null) { + throw new IllegalArgumentException("path-state snapshot participant scope mismatch"); + } + participantTries.put(participant.getDbName(), snapshot == null + ? new PathMerkleTrie(nodeStore) : PathMerkleTrie.fromSnapshot(nodeStore, trieSnapshot)); } PathNodeStore rootStore = Objects.requireNonNull(superNodeStore, "superNodeStore"); if (!uniqueStores.add(rootStore)) { throw new IllegalArgumentException("super node Store must have a distinct identity"); } - superTrie = new PathMerkleTrie(rootStore); + superTrie = snapshot == null ? new PathMerkleTrie(rootStore) + : PathMerkleTrie.fromSnapshot(rootStore, snapshot.superTrie); + if (snapshot != null) { + if (snapshot.participants.size() != scope.getParticipants().size() + || !Arrays.equals(superTrie.rootHash(), snapshot.stateRoot)) { + throw new IllegalArgumentException("path-state snapshot root or scope mismatch"); + } + rootMaterialized = true; + } } public synchronized void put(String dbName, byte[] canonicalKey, byte[] canonicalValue) { @@ -117,6 +136,22 @@ synchronized List leafRecords() { return records; } + synchronized Snapshot snapshot() { + byte[] stateRoot = rootHash(); + Map snapshots = new LinkedHashMap<>(); + for (PathStateParticipant participant : scope.getParticipants()) { + snapshots.put(participant.getDbName(), + participantTries.get(participant.getDbName()).snapshot()); + } + return new Snapshot(snapshots, superTrie.snapshot(), stateRoot); + } + + static PathStateRoot fromSnapshot(PathStateParticipantScope scope, + PathNodeStoreFactory storeFactory, PathNodeStore superNodeStore, Snapshot snapshot) { + return new PathStateRoot(scope, storeFactory, superNodeStore, + Objects.requireNonNull(snapshot, "snapshot")); + } + synchronized void initializeLeaves(Collection records, byte[] expectedRoot) { restoreLeaves(records, expectedRoot, true); } @@ -209,6 +244,24 @@ public interface PathNodeStoreFactory { PathNodeStore open(PathStateParticipant participant); } + public static final class Snapshot { + + private final Map participants; + private final PathMerkleTrie.Snapshot superTrie; + private final byte[] stateRoot; + + private Snapshot(Map participants, + PathMerkleTrie.Snapshot superTrie, byte[] stateRoot) { + this.participants = Collections.unmodifiableMap(new LinkedHashMap<>(participants)); + this.superTrie = Objects.requireNonNull(superTrie, "superTrie"); + this.stateRoot = Arrays.copyOf(stateRoot, stateRoot.length); + } + + public byte[] getStateRoot() { + return Arrays.copyOf(stateRoot, stateRoot.length); + } + } + static final class LeafRecord { private final int storeId; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java index f9057c8c638..c897561e15a 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java @@ -30,6 +30,7 @@ public void layersInheritPublishedParentAndRestoreCurrentAcrossReopen() throws E for (Engine engine : availableEngines()) { Fixture fixture = publishedBase("inherit-" + engine, engine); PathStateRootMetadata first; + PathStateRoot.Snapshot firstSnapshot; byte[] firstRoot; try (PathStateLayer layer = PathStateLayer.begin(fixture.manifest, fixture.base, 101, bytes(11), fixture.base.getBlockHash(), 303, P66Phase.P66_ON, bytes(12))) { @@ -38,6 +39,8 @@ public void layersInheritPublishedParentAndRestoreCurrentAcrossReopen() throws E PathStateMutation.delete("account", new byte[]{3}))); firstRoot = layer.rootHash(); first = layer.commit(); + firstSnapshot = layer.snapshot(); + assertArrayEquals(firstRoot, firstSnapshot.getStateRoot()); assertArrayEquals(first.encode(), layer.commit().encode()); assertThrows(IllegalStateException.class, () -> layer.apply(Collections.singletonList( PathStateMutation.delete("proposal", new byte[]{1})))); @@ -55,8 +58,9 @@ public void layersInheritPublishedParentAndRestoreCurrentAcrossReopen() throws E PathStateRootMetadata second; byte[] secondRoot; - try (PathStateLayer layer = PathStateLayer.begin(fixture.manifest, first, 102, - bytes(13), first.getBlockHash(), 306, P66Phase.P66_ON, bytes(14))) { + try (PathStateLayer layer = PathStateLayer.beginFromSnapshot(fixture.manifest, first, + firstSnapshot, 102, bytes(13), first.getBlockHash(), 306, P66Phase.P66_ON, + bytes(14))) { layer.apply(Collections.singletonList( PathStateMutation.put("account", new byte[]{7}, new byte[]{8}))); secondRoot = layer.rootHash(); From 6337510ca3c92b7e61466a74b4ba6caa0d5544cb Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 14:02:08 +0800 Subject: [PATCH 076/161] feat(trie): own committed snapshot head --- .../core/db2/stateroot/PathStateLayer.java | 30 ++++- .../core/db2/stateroot/PathStateRoot.java | 3 + .../db2/stateroot/PathStateSnapshotHead.java | 118 ++++++++++++++++++ .../stateroot/PathStateSnapshotHeadTest.java | 108 ++++++++++++++++ 4 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java index 162cdfe3b68..8d4db651d54 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java @@ -24,6 +24,7 @@ public final class PathStateLayer implements Closeable { private final byte[] transitionDigest; private PathStateRootMetadata prepared; private PathStateRootMetadata committed; + private PathStateRoot.Snapshot preparedSnapshot; private PathStateLayer(PathStateStoreManifest manifest, PathStateLayerPublication publication, PathStateNodeStoreSet stores, PathStateRoot root, PathStateRootMetadata parent, @@ -63,8 +64,16 @@ public static PathStateLayer beginFromSnapshot(PathStateStoreManifest manifest, PathStateRootMetadata parent, PathStateRoot.Snapshot parentSnapshot, long blockNumber, byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, byte[] transitionDigest) throws IOException { + return beginFromSnapshot(manifest, parent, parentSnapshot, blockNumber, blockHash, parentHash, + timestamp, phase, transitionDigest, PathStateLayerLimits.defaults()); + } + + public static PathStateLayer beginFromSnapshot(PathStateStoreManifest manifest, + PathStateRootMetadata parent, PathStateRoot.Snapshot parentSnapshot, long blockNumber, + byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, + byte[] transitionDigest, PathStateLayerLimits limits) throws IOException { return begin(manifest, parent, blockNumber, blockHash, parentHash, timestamp, phase, - transitionDigest, PathStateLayerLimits.defaults(), stage -> { }, + transitionDigest, Objects.requireNonNull(limits, "limits"), stage -> { }, Objects.requireNonNull(parentSnapshot, "parentSnapshot")); } @@ -157,7 +166,24 @@ public synchronized PathStateRoot.Snapshot snapshot() { if (committed == null) { throw new IllegalStateException("path-state layer is not committed"); } - return root.snapshot(); + return preparedSnapshot == null ? root.snapshot() : preparedSnapshot; + } + + synchronized PathStateRoot.Snapshot prepareSnapshot() { + if (committed != null) { + return snapshot(); + } + if (prepared == null) { + prepared = PathStateRootMetadata.layer(blockNumber, blockHash, parentHash, timestamp, phase, + manifest.getIdentityDigest(), parent.getStateRoot(), root.rootHash(), transitionDigest); + } + if (preparedSnapshot == null) { + preparedSnapshot = root.snapshot(); + } + if (!Arrays.equals(prepared.getStateRoot(), preparedSnapshot.getStateRoot())) { + throw new IllegalStateException("path-state prepared snapshot root mismatch"); + } + return preparedSnapshot; } @Override diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index 8bf7e4ab6fc..338529a328b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -102,6 +102,9 @@ public synchronized byte[] participantRoot(String dbName) { /** Returns the super root after binding every participant identity, format, and current root. */ public synchronized byte[] rootHash() { + if (rootMaterialized) { + return superTrie.rootHash(); + } for (PathStateParticipant participant : scope.getParticipants()) { byte[] storeRoot = participantTries.get(participant.getDbName()).rootHash(); superTrie.put(PathStateCommitmentCodec.superLeafKey(participant.getStoreId()), diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java new file mode 100644 index 00000000000..7fb268b9a16 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java @@ -0,0 +1,118 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +/** In-process snapshot authority that advances only with the durable block-final CURRENT head. */ +public final class PathStateSnapshotHead { + + private final PathStateStoreManifest manifest; + private final PathStateLayerLimits limits; + private PathStateRootMetadata head; + private PathStateRoot.Snapshot snapshot; + private boolean failed; + + private PathStateSnapshotHead(PathStateStoreManifest manifest, PathStateLayerLimits limits, + PathStateRootMetadata head, PathStateRoot.Snapshot snapshot) { + this.manifest = manifest; + this.limits = limits; + this.head = head; + this.snapshot = snapshot; + } + + /** Restores and verifies the exact durable CURRENT root before owning its detached snapshot. */ + public static PathStateSnapshotHead open(PathStateStoreManifest manifest, + PathStateLayerLimits limits) throws IOException { + PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); + PathStateLayerLimits admittedLimits = Objects.requireNonNull(limits, "limits"); + PathStateRootMetadata current = new PathStateCurrentStore(admitted).current(); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openPublished(admitted, current)) { + PathStateRoot root = stores.createRoot(); + PathStateRoot.Snapshot restored = root.snapshot(); + if (!Arrays.equals(restored.getStateRoot(), current.getStateRoot())) { + throw new IOException("path-state snapshot head restore root mismatch"); + } + return new PathStateSnapshotHead(admitted, admittedLimits, current, restored); + } catch (IllegalArgumentException | IllegalStateException failure) { + throw new IOException("path-state snapshot head restore failed", failure); + } + } + + /** Applies one exact child and publishes its snapshot only after durable layer commit succeeds. */ + public synchronized PathStateRootMetadata advance(PathStateBlockTransition transition) + throws IOException { + requireHealthy(); + PathStateBlockTransition admitted = Objects.requireNonNull(transition, "transition"); + requireChild(admitted); + PathStateRootMetadata previous = head; + PathStateRoot.Snapshot candidateSnapshot; + PathStateRootMetadata committed; + try (PathStateLayer layer = PathStateLayer.beginFromSnapshot(manifest, previous, snapshot, + admitted.getBlockNumber(), admitted.getBlockHash(), admitted.getParentHash(), + admitted.getTimestamp(), admitted.getPhase(), admitted.getPayloadDigest(), limits)) { + if (!admitted.getMutations().isEmpty()) { + layer.apply(admitted.getMutations()); + } + candidateSnapshot = layer.prepareSnapshot(); + committed = layer.commit(); + } catch (IOException | RuntimeException failure) { + failIfAuthorityMoved(previous, failure); + throw failure; + } + if (!same(committed, new PathStateCurrentStore(manifest).current()) + || committed.getBlockNumber() != admitted.getBlockNumber() + || !Arrays.equals(committed.getBlockHash(), admitted.getBlockHash()) + || !Arrays.equals(committed.getParentHash(), admitted.getParentHash()) + || !Arrays.equals(committed.getPayloadDigest(), admitted.getPayloadDigest()) + || !Arrays.equals(committed.getStateRoot(), candidateSnapshot.getStateRoot())) { + failed = true; + throw new IOException("path-state committed snapshot identity mismatch"); + } + head = committed; + snapshot = candidateSnapshot; + return committed; + } + + public synchronized PathStateRootMetadata getHead() throws IOException { + requireHealthy(); + return head; + } + + public synchronized PathStateRoot.Snapshot getSnapshot() throws IOException { + requireHealthy(); + return snapshot; + } + + public synchronized boolean isFailed() { + return failed; + } + + private void requireChild(PathStateBlockTransition transition) throws IOException { + if (transition.getBlockNumber() != head.getBlockNumber() + 1 + || !Arrays.equals(transition.getParentHash(), head.getBlockHash())) { + throw new IOException("path-state snapshot transition does not extend owned head"); + } + } + + private void failIfAuthorityMoved(PathStateRootMetadata previous, Throwable failure) { + try { + if (!same(previous, new PathStateCurrentStore(manifest).current())) { + failed = true; + } + } catch (IOException currentFailure) { + failed = true; + failure.addSuppressed(currentFailure); + } + } + + private void requireHealthy() throws IOException { + if (failed) { + throw new IOException("path-state snapshot head is failed"); + } + } + + private static boolean same(PathStateRootMetadata left, PathStateRootMetadata right) { + return Arrays.equals(left.encode(), right.encode()); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java new file mode 100644 index 00000000000..0b15bb2d648 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java @@ -0,0 +1,108 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.arch.Arch; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateSnapshotHeadTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void publishesOnlyCommittedContinuousSnapshotsAcrossReopen() throws Exception { + for (Engine engine : availableEngines()) { + Fixture fixture = fixture("advance-" + engine, engine); + PathStateSnapshotHead owner = PathStateSnapshotHead.open( + fixture.manifest, PathStateLayerLimits.defaults()); + PathStateRootMetadata first = owner.advance(transition(101, 11, + fixture.base.getBlockHash(), Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5})))); + assertArrayEquals(first.getStateRoot(), owner.getSnapshot().getStateRoot()); + + assertThrows(IOException.class, () -> owner.advance(transition(103, 13, + first.getBlockHash(), Collections.emptyList()))); + assertArrayEquals(first.encode(), owner.getHead().encode()); + assertFalse(owner.isFailed()); + + PathStateRootMetadata second = owner.advance(transition(102, 12, + first.getBlockHash(), Collections.emptyList())); + assertArrayEquals(first.getStateRoot(), second.getStateRoot()); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openCurrent(fixture.manifest)) { + assertArrayEquals(second.getStateRoot(), stores.createRoot().rootHash()); + } + } + } + + @Test + public void commitAdmissionFailureKeepsOwnedSnapshotAndCurrent() throws Exception { + Fixture fixture = fixture("admission", Engine.ROCKSDB); + PathStateSnapshotHead owner = PathStateSnapshotHead.open( + fixture.manifest, new PathStateLayerLimits(10, 1)); + + assertThrows(IOException.class, () -> owner.advance(transition(101, 11, + fixture.base.getBlockHash(), Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5}))))); + + assertArrayEquals(fixture.base.encode(), owner.getHead().encode()); + assertArrayEquals(fixture.base.encode(), + new PathStateCurrentStore(fixture.manifest).current().encode()); + assertFalse(owner.isFailed()); + } + + private Fixture fixture(String name, Engine engine) throws Exception { + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen( + new File(temporaryFolder.getRoot(), name).toPath(), engine); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4}))); + PathStateRootMetadata base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), root.rootHash(), bytes(3)); + new PathStateBasePublication(manifest).publish(stores, base); + return new Fixture(manifest, base); + } + } + + private static PathStateBlockTransition transition(long blockNumber, int hashSeed, + byte[] parentHash, java.util.List mutations) { + return new PathStateBlockTransition(blockNumber, bytes(hashSeed), parentHash, + blockNumber * 3, P66Phase.P66_ON, mutations); + } + + private static Engine[] availableEngines() { + return Arch.isArm64() ? new Engine[]{Engine.ROCKSDB} + : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + private static final class Fixture { + + private final PathStateStoreManifest manifest; + private final PathStateRootMetadata base; + + private Fixture(PathStateStoreManifest manifest, PathStateRootMetadata base) { + this.manifest = manifest; + this.base = base; + } + } +} From 36f60a8ecd390ce0f56f0565c62f74c68a9d4e80 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 14:11:56 +0800 Subject: [PATCH 077/161] feat(trie): prepare state transition in memory --- .../core/db2/stateroot/PathStateLayer.java | 31 +++- .../db2/stateroot/PathStateNodeStoreSet.java | 42 +++++ .../db2/stateroot/PathStateSnapshotHead.java | 36 ++-- .../PreparedPathStateTransition.java | 166 ++++++++++++++++++ .../stateroot/PathStateSnapshotHeadTest.java | 18 +- 5 files changed, 278 insertions(+), 15 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PreparedPathStateTransition.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java index 8d4db651d54..1706588475b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java @@ -77,6 +77,21 @@ public static PathStateLayer beginFromSnapshot(PathStateStoreManifest manifest, Objects.requireNonNull(parentSnapshot, "parentSnapshot")); } + /** Begins durable publication from an immutable candidate computed without native I/O. */ + public static PathStateLayer beginPrepared(PathStateStoreManifest manifest, + PathStateRootMetadata parent, PreparedPathStateTransition prepared, + PathStateLayerLimits limits) throws IOException { + PreparedPathStateTransition candidate = Objects.requireNonNull(prepared, "prepared"); + if (!candidate.extendsParent(Objects.requireNonNull(parent, "parent"))) { + throw new IOException("path-state prepared transition parent is not CURRENT candidate"); + } + PathStateBlockTransition transition = candidate.getTransition(); + return begin(manifest, parent, transition.getBlockNumber(), transition.getBlockHash(), + transition.getParentHash(), transition.getTimestamp(), transition.getPhase(), + transition.getPayloadDigest(), Objects.requireNonNull(limits, "limits"), stage -> { }, + null, candidate); + } + static PathStateLayer begin(PathStateStoreManifest manifest, PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, byte[] transitionDigest, @@ -98,6 +113,15 @@ private static PathStateLayer begin(PathStateStoreManifest manifest, long timestamp, P66Phase phase, byte[] transitionDigest, PathStateLayerLimits limits, PathStateLayerPublication.FaultHook faultHook, PathStateRoot.Snapshot parentSnapshot) throws IOException { + return begin(manifest, parent, blockNumber, blockHash, parentHash, timestamp, phase, + transitionDigest, limits, faultHook, parentSnapshot, null); + } + + private static PathStateLayer begin(PathStateStoreManifest manifest, + PathStateRootMetadata parent, long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase, byte[] transitionDigest, PathStateLayerLimits limits, + PathStateLayerPublication.FaultHook faultHook, PathStateRoot.Snapshot parentSnapshot, + PreparedPathStateTransition preparedTransition) throws IOException { PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); PathStateLayerLimits admittedLimits = Objects.requireNonNull(limits, "limits"); PathStateRootMetadata admittedParent = Objects.requireNonNull(parent, "parent"); @@ -118,9 +142,12 @@ private static PathStateLayer begin(PathStateStoreManifest manifest, PathStateNodeStoreSet.openPublished(admitted, admittedParent); PathStateNodeStoreSet childStores = null; try { - PathStateRoot parentRoot = parentSnapshot == null ? parentStores.createRoot() : null; + PathStateRoot parentRoot = parentSnapshot == null && preparedTransition == null + ? parentStores.createRoot() : null; childStores = PathStateNodeStoreSet.beginLayer(admitted, identity, parentStores); - PathStateRoot childRoot = parentSnapshot == null + PathStateRoot childRoot = preparedTransition != null + ? childStores.createRootFrom(preparedTransition) + : parentSnapshot == null ? childStores.createRootFrom(parentStores.leafRecords(), parentRoot.rootHash()) : childStores.createRootFrom(parentSnapshot, admittedParent.getStateRoot()); return new PathStateLayer(admitted, diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index d5bbb262c9a..0e287e6ed4c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -261,6 +261,36 @@ synchronized PathStateRoot createRootFrom(PathStateRoot.Snapshot snapshot, return root; } + synchronized PathStateRoot createRootFrom(PreparedPathStateTransition prepared) { + requireOpen(); + if (rootClaimed) { + throw new IllegalStateException("path-state node database set already has a trie owner"); + } + if (progress != null || !localLeaves.isEmpty() || !leafTombstones.isEmpty()) { + throw new IllegalStateException("path-state layer already contains durable state"); + } + if (parentStores == null) { + throw new IllegalStateException("path-state layer has no parent node overlay"); + } + PreparedPathStateTransition candidate = Objects.requireNonNull(prepared, "prepared"); + PathStateRoot next = PathStateRoot.fromSnapshot(scope, + participant -> participantStores.get(participant.getDbName()), superStore, + candidate.getSnapshot()); + for (PreparedPathStateTransition.NodeMutation mutation : candidate.getNodeMutations()) { + PathNodeStore store = nodeStore(mutation.getStoreId()); + byte[] encoded = mutation.getEncodedNode(); + if (encoded == null) { + store.delete(mutation.getPath()); + } else { + store.put(mutation.getPath(), encoded); + } + } + next.verifyNodeStores(); + root = next; + rootClaimed = true; + return root; + } + synchronized List leafRecords() { requireOpen(); if (root == null) { @@ -492,6 +522,18 @@ private synchronized void delete(byte[] key) { pending.put(new BytesKey(key), null); } + private PathNodeStore nodeStore(int storeId) { + if (storeId == 0) { + return superStore; + } + for (PathStateParticipant participant : scope.getParticipants()) { + if (participant.getStoreId() == storeId) { + return participantStores.get(participant.getDbName()); + } + } + throw new IllegalArgumentException("unknown prepared path-state Store ID: " + storeId); + } + private void requireProgressIdentity(PathStateRootMetadata metadata) throws IOException { if (metadata.getKind() != kind || !Arrays.equals(metadata.getFormatDigest(), manifestDigest)) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java index 7fb268b9a16..ba4211765b1 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java @@ -42,29 +42,41 @@ public static PathStateSnapshotHead open(PathStateStoreManifest manifest, /** Applies one exact child and publishes its snapshot only after durable layer commit succeeds. */ public synchronized PathStateRootMetadata advance(PathStateBlockTransition transition) throws IOException { + return advancePrepared(prepare(transition)); + } + + /** Computes an immutable candidate without opening or changing durable path-state storage. */ + public synchronized PreparedPathStateTransition prepare(PathStateBlockTransition transition) + throws IOException { requireHealthy(); PathStateBlockTransition admitted = Objects.requireNonNull(transition, "transition"); requireChild(admitted); + return PreparedPathStateTransition.prepare(head, snapshot, admitted); + } + + /** Publishes one exact prepared child and adopts it only after CURRENT confirms durability. */ + public synchronized PathStateRootMetadata advancePrepared( + PreparedPathStateTransition prepared) throws IOException { + requireHealthy(); + PreparedPathStateTransition admitted = Objects.requireNonNull(prepared, "prepared"); + if (!admitted.extendsParent(head)) { + throw new IOException("path-state prepared transition does not extend owned head"); + } PathStateRootMetadata previous = head; - PathStateRoot.Snapshot candidateSnapshot; + PathStateRoot.Snapshot candidateSnapshot = admitted.getSnapshot(); PathStateRootMetadata committed; - try (PathStateLayer layer = PathStateLayer.beginFromSnapshot(manifest, previous, snapshot, - admitted.getBlockNumber(), admitted.getBlockHash(), admitted.getParentHash(), - admitted.getTimestamp(), admitted.getPhase(), admitted.getPayloadDigest(), limits)) { - if (!admitted.getMutations().isEmpty()) { - layer.apply(admitted.getMutations()); - } - candidateSnapshot = layer.prepareSnapshot(); + PathStateBlockTransition transition = admitted.getTransition(); + try (PathStateLayer layer = PathStateLayer.beginPrepared(manifest, previous, admitted, limits)) { committed = layer.commit(); } catch (IOException | RuntimeException failure) { failIfAuthorityMoved(previous, failure); throw failure; } if (!same(committed, new PathStateCurrentStore(manifest).current()) - || committed.getBlockNumber() != admitted.getBlockNumber() - || !Arrays.equals(committed.getBlockHash(), admitted.getBlockHash()) - || !Arrays.equals(committed.getParentHash(), admitted.getParentHash()) - || !Arrays.equals(committed.getPayloadDigest(), admitted.getPayloadDigest()) + || committed.getBlockNumber() != transition.getBlockNumber() + || !Arrays.equals(committed.getBlockHash(), transition.getBlockHash()) + || !Arrays.equals(committed.getParentHash(), transition.getParentHash()) + || !Arrays.equals(committed.getPayloadDigest(), transition.getPayloadDigest()) || !Arrays.equals(committed.getStateRoot(), candidateSnapshot.getStateRoot())) { failed = true; throw new IOException("path-state committed snapshot identity mismatch"); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PreparedPathStateTransition.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PreparedPathStateTransition.java new file mode 100644 index 00000000000..f5e7ff0d69a --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PreparedPathStateTransition.java @@ -0,0 +1,166 @@ +package org.tron.core.db2.stateroot; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** Immutable, memory-only candidate trie result for one exact block transition. */ +public final class PreparedPathStateTransition { + + private final PathStateRootMetadata parent; + private final PathStateBlockTransition transition; + private final PathStateRoot.Snapshot snapshot; + private final List nodeMutations; + + private PreparedPathStateTransition(PathStateRootMetadata parent, + PathStateBlockTransition transition, PathStateRoot.Snapshot snapshot, + List nodeMutations) { + this.parent = parent; + this.transition = transition; + this.snapshot = snapshot; + this.nodeMutations = Collections.unmodifiableList(new ArrayList<>(nodeMutations)); + } + + /** Computes candidate node changes without opening or writing any native path-state Store. */ + public static PreparedPathStateTransition prepare(PathStateRootMetadata parent, + PathStateRoot.Snapshot parentSnapshot, PathStateBlockTransition transition) { + PathStateRootMetadata admittedParent = Objects.requireNonNull(parent, "parent"); + PathStateRoot.Snapshot admittedSnapshot = Objects.requireNonNull(parentSnapshot, + "parentSnapshot"); + PathStateBlockTransition admittedTransition = Objects.requireNonNull(transition, + "transition"); + requireChild(admittedParent, admittedSnapshot, admittedTransition); + + Map stores = new LinkedHashMap<>(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + PathStateRoot root = PathStateRoot.fromSnapshot(scope, participant -> stores.computeIfAbsent( + participant.getStoreId(), ignored -> new RecordingNodeStore()), + stores.computeIfAbsent(0, ignored -> new RecordingNodeStore()), admittedSnapshot); + if (!admittedTransition.getMutations().isEmpty()) { + root.apply(admittedTransition.getMutations()); + } + PathStateRoot.Snapshot candidate = root.snapshot(); + List mutations = new ArrayList<>(); + for (Map.Entry store : stores.entrySet()) { + store.getValue().appendTo(store.getKey(), mutations); + } + return new PreparedPathStateTransition(admittedParent, admittedTransition, candidate, + mutations); + } + + public byte[] getStateRoot() { + return snapshot.getStateRoot(); + } + + public int getNodeMutationCount() { + return nodeMutations.size(); + } + + PathStateRootMetadata getParent() { + return parent; + } + + PathStateBlockTransition getTransition() { + return transition; + } + + PathStateRoot.Snapshot getSnapshot() { + return snapshot; + } + + List getNodeMutations() { + return nodeMutations; + } + + boolean extendsParent(PathStateRootMetadata expected) { + return Arrays.equals(parent.encode(), expected.encode()); + } + + private static void requireChild(PathStateRootMetadata parent, + PathStateRoot.Snapshot snapshot, PathStateBlockTransition transition) { + if (!Arrays.equals(parent.getStateRoot(), snapshot.getStateRoot())) { + throw new IllegalArgumentException("path-state prepared parent snapshot root mismatch"); + } + if (transition.getBlockNumber() != parent.getBlockNumber() + 1 + || !Arrays.equals(transition.getParentHash(), parent.getBlockHash())) { + throw new IllegalArgumentException("path-state prepared transition does not extend parent"); + } + } + + static final class NodeMutation { + + private final int storeId; + private final byte[] path; + private final byte[] encodedNode; + + private NodeMutation(int storeId, byte[] path, byte[] encodedNode) { + this.storeId = storeId; + this.path = Arrays.copyOf(path, path.length); + this.encodedNode = encodedNode == null ? null + : Arrays.copyOf(encodedNode, encodedNode.length); + } + + int getStoreId() { + return storeId; + } + + byte[] getPath() { + return Arrays.copyOf(path, path.length); + } + + byte[] getEncodedNode() { + return encodedNode == null ? null : Arrays.copyOf(encodedNode, encodedNode.length); + } + } + + private static final class RecordingNodeStore implements PathNodeStore { + + private final Map changes = new LinkedHashMap<>(); + + @Override + public byte[] get(byte[] path) { + byte[] value = changes.get(new BytesKey(path)); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + changes.put(new BytesKey(path), Arrays.copyOf(encodedNode, encodedNode.length)); + } + + @Override + public void delete(byte[] path) { + changes.put(new BytesKey(path), null); + } + + private void appendTo(int storeId, List mutations) { + for (Map.Entry change : changes.entrySet()) { + mutations.add(new NodeMutation(storeId, change.getKey().bytes, change.getValue())); + } + } + } + + private static final class BytesKey { + + private final byte[] bytes; + + private BytesKey(byte[] bytes) { + this.bytes = Arrays.copyOf(Objects.requireNonNull(bytes, "path"), bytes.length); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof BytesKey + && Arrays.equals(bytes, ((BytesKey) other).bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java index 0b15bb2d648..3ab5a2366a7 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java @@ -3,11 +3,14 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.Arrays; import java.util.Collections; +import java.util.stream.Stream; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -26,10 +29,23 @@ public void publishesOnlyCommittedContinuousSnapshotsAcrossReopen() throws Excep Fixture fixture = fixture("advance-" + engine, engine); PathStateSnapshotHead owner = PathStateSnapshotHead.open( fixture.manifest, PathStateLayerLimits.defaults()); - PathStateRootMetadata first = owner.advance(transition(101, 11, + PreparedPathStateTransition prepared = owner.prepare(transition(101, 11, fixture.base.getBlockHash(), Collections.singletonList( PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5})))); + PreparedPathStateTransition stale = owner.prepare(transition(101, 21, + fixture.base.getBlockHash(), Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{6})))); + assertTrue(prepared.getNodeMutationCount() > 0); + try (Stream layers = Files.list( + fixture.manifest.getLayersDirectory())) { + assertFalse(layers.findAny().isPresent()); + } + assertArrayEquals(fixture.base.encode(), + new PathStateCurrentStore(fixture.manifest).current().encode()); + PathStateRootMetadata first = owner.advancePrepared(prepared); assertArrayEquals(first.getStateRoot(), owner.getSnapshot().getStateRoot()); + assertThrows(IOException.class, () -> owner.advancePrepared(stale)); + assertArrayEquals(first.encode(), owner.getHead().encode()); assertThrows(IOException.class, () -> owner.advance(transition(103, 13, first.getBlockHash(), Collections.emptyList()))); From ded8348abe17603fedfd8c08b175d16f3c763397 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 14:23:48 +0800 Subject: [PATCH 078/161] test(trie): verify rebuild with oracle --- .../PathStateRebuildCoordinatorTest.java | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index e463181e163..f6d543f9fda 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -25,12 +25,14 @@ import org.junit.rules.TemporaryFolder; import org.tron.common.arch.Arch; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateParticipantDescriptor.StoreIdentity; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.EntryConsumer; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.RebuildResult; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotSource; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.StoreResult; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; +import org.tron.core.trie.TrieImpl; import org.tron.protos.Protocol.Account; public class PathStateRebuildCoordinatorTest { @@ -50,6 +52,7 @@ public void rebuildsAndPublishesExactSnapshotAcrossNativeEngines() throws Except source.add("abi", address(1), new byte[0]); RebuildResult result = new PathStateRebuildCoordinator().rebuild(manifest, source); + OracleResult oracle = independentOracle(source); assertEquals(27, result.getStores().size()); assertEquals(3, result.getTotalEntries()); @@ -58,12 +61,20 @@ public void rebuildsAndPublishesExactSnapshotAcrossNativeEngines() throws Except assertEquals(0, result.requireStore("account").getEntryCount()); assertArrayEquals(result.getSourceDigest(), result.getMetadata().getPayloadDigest()); assertTrue(source.getVerificationCount() >= 2); + assertArrayEquals(oracle.stateRoot, result.getMetadata().getStateRoot()); + for (StoreResult store : result.getStores()) { + assertArrayEquals(oracle.storeRoots.get(store.getDbName()), store.getStoreRoot()); + } - PathStateRootMetadata current = new PathStateCurrentStore(manifest).current(); + PathStateStoreManifest reopenedManifest = PathStateStoreManifest.validateExisting( + manifest.getDirectory(), engine); + assertArrayEquals(manifest.getIdentityDigest(), reopenedManifest.getIdentityDigest()); + PathStateRootMetadata current = new PathStateCurrentStore(reopenedManifest).current(); assertArrayEquals(result.getMetadata().encode(), current.encode()); - try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openCurrent(manifest)) { + assertArrayEquals(result.getSourceDigest(), current.getPayloadDigest()); + try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openCurrent(reopenedManifest)) { PathStateRoot restored = reopened.createRoot(); - assertArrayEquals(result.getMetadata().getStateRoot(), restored.rootHash()); + assertArrayEquals(oracle.stateRoot, restored.rootHash()); restored.verifyNodeStores(); } @@ -406,6 +417,45 @@ private static Engine[] availableEngines() { : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; } + private static OracleResult independentOracle(TestSnapshotSource source) { + PathStateParticipantDescriptor descriptor = PathStateParticipantDescriptor.current(); + PathStateCanonicalizer canonicalizer = new PathStateCanonicalizer(); + Map tries = new LinkedHashMap<>(); + Map roots = new LinkedHashMap<>(); + for (StoreIdentity store : descriptor.getStores()) { + TrieImpl trie = referenceTrie(); + for (Row row : source.stores.get(store.getDbName())) { + PathStateMutation mutation = canonicalizer.put(source.identity.getPhase(), + store.getDbName(), row.key, row.value); + byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(store.getStoreId(), + mutation.getCanonicalKey()); + if (mutation.isDelete()) { + trie.delete(secureKey); + } else { + trie.put(secureKey, PathStateCommitmentCodec.presentLeafValue( + mutation.getCanonicalValue())); + } + } + tries.put(store.getDbName(), trie); + roots.put(store.getDbName(), trie.getRootHash()); + } + TrieImpl superTrie = referenceTrie(); + PathStateParticipantScope scope = canonicalizer.participantScope(); + for (StoreIdentity store : descriptor.getStores()) { + PathStateParticipant participant = scope.require(store.getDbName()); + superTrie.put(PathStateCommitmentCodec.superLeafKey(store.getStoreId()), + PathStateCommitmentCodec.superLeafValue(store.getStoreId(), store.getDbName(), + participant.getStoreFormatVersion(), tries.get(store.getDbName()).getRootHash())); + } + return new OracleResult(roots, superTrie.getRootHash()); + } + + private static TrieImpl referenceTrie() { + TrieImpl trie = new TrieImpl(); + trie.setAsync(false); + return trie; + } + private static byte[] address(int suffix) { byte[] address = new byte[21]; address[0] = 0x41; @@ -517,6 +567,17 @@ public void verifyIdentity(SnapshotIdentity expected) throws IOException { } } + private static final class OracleResult { + + private final Map storeRoots; + private final byte[] stateRoot; + + private OracleResult(Map storeRoots, byte[] stateRoot) { + this.storeRoots = storeRoots; + this.stateRoot = stateRoot; + } + } + private static final class Row { private final byte[] key; From 5bebaf1a840e84bfd3ba58675625891ba0d7879f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 14:34:53 +0800 Subject: [PATCH 079/161] feat(config): admit path state root startup --- .../stateroot/PathStateRuntimeAdmission.java | 55 ++++++++++++++++ .../org/tron/core/config/args/Storage.java | 36 ++++++++++ .../tron/core/config/args/StorageConfig.java | 38 +++++++++++ common/src/main/resources/reference.conf | 11 ++++ .../core/config/args/StorageConfigTest.java | 30 +++++++++ .../java/org/tron/core/config/args/Args.java | 14 ++++ framework/src/main/resources/config.conf | 11 ++++ .../org/tron/core/config/args/ArgsTest.java | 26 ++++++++ .../PathStateRuntimeAdmissionTest.java | 66 +++++++++++++++++++ 9 files changed, 287 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmission.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmissionTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmission.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmission.java new file mode 100644 index 00000000000..9371555a48d --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmission.java @@ -0,0 +1,55 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Read-only startup admission that keeps the disabled path free of filesystem access. */ +public final class PathStateRuntimeAdmission { + + private PathStateRuntimeAdmission() { + } + + public static Result inspect(boolean enabled, Path directory, Engine engine) throws IOException { + if (!enabled) { + return new Result(Status.DISABLED, null); + } + Path root = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); + Engine selected = Objects.requireNonNull(engine, "engine"); + if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + return new Result(Status.REBUILD_REQUIRED, null); + } + PathStateStoreManifest manifest = PathStateStoreManifest.validateExisting(root, selected); + Status status = new PathStateCurrentStore(manifest).isInitialized() + ? Status.CURRENT_READY : Status.REBUILD_REQUIRED; + return new Result(status, manifest); + } + + public enum Status { + DISABLED, + REBUILD_REQUIRED, + CURRENT_READY + } + + public static final class Result { + + private final Status status; + private final PathStateStoreManifest manifest; + + private Result(Status status, PathStateStoreManifest manifest) { + this.status = status; + this.manifest = manifest; + } + + public Status getStatus() { + return status; + } + + public PathStateStoreManifest getManifest() { + return manifest; + } + } +} diff --git a/common/src/main/java/org/tron/core/config/args/Storage.java b/common/src/main/java/org/tron/core/config/args/Storage.java index b0f95c38348..46c188d83dc 100644 --- a/common/src/main/java/org/tron/core/config/args/Storage.java +++ b/common/src/main/java/org/tron/core/config/args/Storage.java @@ -101,6 +101,42 @@ public class Storage { @Setter private int stateArchiveQueueCapacity; + @Getter + @Setter + private boolean pathStateRootEnabled; + + @Getter + @Setter + private String pathStateRootMode; + + @Getter + @Setter + private String pathStateRootDirectory; + + @Getter + @Setter + private int pathStateRootFormatVersion; + + @Getter + @Setter + private int pathStateRootReversibleLayerLimit; + + @Getter + @Setter + private long pathStateRootReversibleLayerBytes; + + @Getter + @Setter + private long pathStateRootWriteBufferBytes; + + @Getter + @Setter + private boolean pathStateRootRebuildFromGenesis; + + @Getter + @Setter + private boolean pathStateRootVerifyEveryBlock; + private Options defaultDbOptions; @Getter diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index 2fbbbb1e54a..38c33c22516 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -28,6 +28,7 @@ public class StorageConfig { private CheckpointConfig checkpoint = new CheckpointConfig(); private SnapshotConfig snapshot = new SnapshotConfig(); private StateArchiveConfig stateArchive = new StateArchiveConfig(); + private PathStateRootConfig pathStateRoot = new PathStateRootConfig(); private TxCacheConfig txCache = new TxCacheConfig(); // ConfigBeanFactory requires all bean fields present per item, so we parse manually. @Setter(lombok.AccessLevel.NONE) @@ -166,6 +167,42 @@ void postProcess() { } } + @Getter + @Setter + public static class PathStateRootConfig { + + private boolean enabled = false; + private String mode = "shadow"; + private String directory = "path-state-root"; + private int formatVersion = 1; + private int reversibleLayerLimit = 128; + private long reversibleLayerBytes = 2147483648L; + private long writeBufferBytes = 268435456L; + private boolean rebuildFromGenesis = false; + private boolean verifyEveryBlock = true; + + void postProcess() { + if (!"shadow".equals(mode)) { + throw new IllegalArgumentException("pathStateRoot.mode must be shadow"); + } + if (directory == null || directory.trim().isEmpty()) { + throw new IllegalArgumentException("pathStateRoot.directory must not be empty"); + } + if (formatVersion != 1) { + throw new IllegalArgumentException("pathStateRoot.formatVersion must be 1"); + } + if (reversibleLayerLimit <= 0 || reversibleLayerBytes <= 0 || writeBufferBytes <= 0) { + throw new IllegalArgumentException("pathStateRoot limits must be positive"); + } + if (rebuildFromGenesis) { + throw new IllegalArgumentException("pathStateRoot.rebuildFromGenesis is not supported"); + } + if (!verifyEveryBlock) { + throw new IllegalArgumentException("pathStateRoot.verifyEveryBlock must remain enabled"); + } + } + } + private static final class BlockHistoryLimits { private static final long MIN_SEGMENT_SIZE = 64L * 1024 * 1024; } @@ -214,6 +251,7 @@ public static StorageConfig fromConfig(Config config) { sc.dbSettings.postProcess(); sc.snapshot.postProcess(); sc.stateArchive.postProcess(); + sc.pathStateRoot.postProcess(); sc.txCache.postProcess(); return sc; } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 45f08744dcc..894326c7e5b 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -138,6 +138,17 @@ storage { stateArchive.maxSegmentSize = 1073741824 # 1 GiB stateArchive.queueCapacity = 256 + # Experimental current-only, non-consensus path state root. Disabled by default. + pathStateRoot.enabled = false + pathStateRoot.mode = "shadow" + pathStateRoot.directory = "path-state-root" + pathStateRoot.formatVersion = 1 + pathStateRoot.reversibleLayerLimit = 128 + pathStateRoot.reversibleLayerBytes = 2147483648 # 2 GiB + pathStateRoot.writeBufferBytes = 268435456 # 256 MiB + pathStateRoot.rebuildFromGenesis = false + pathStateRoot.verifyEveryBlock = true + # Data root setting, for check data, currently only reward-vi is used. # merkleRoot = { # reward-vi = 9debcb9924055500aaae98cdee10501c5c39d4daa75800a996f4bdda73dbccd8 // main-net diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java index 0efee8ade5c..86155b7ea97 100644 --- a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -78,6 +78,36 @@ public void testStateArchiveRejectsSmallSegments() { StorageConfig.fromConfig(withRef("storage.stateArchive.maxSegmentSize = 1024")); } + @Test + public void testPathStateRootDefaultsAndOverrides() { + StorageConfig defaults = StorageConfig.fromConfig(withRef()); + assertFalse(defaults.getPathStateRoot().isEnabled()); + assertEquals("shadow", defaults.getPathStateRoot().getMode()); + assertEquals("path-state-root", defaults.getPathStateRoot().getDirectory()); + assertEquals(1, defaults.getPathStateRoot().getFormatVersion()); + assertEquals(128, defaults.getPathStateRoot().getReversibleLayerLimit()); + assertEquals(2147483648L, defaults.getPathStateRoot().getReversibleLayerBytes()); + assertEquals(268435456L, defaults.getPathStateRoot().getWriteBufferBytes()); + assertFalse(defaults.getPathStateRoot().isRebuildFromGenesis()); + assertTrue(defaults.getPathStateRoot().isVerifyEveryBlock()); + + StorageConfig configured = StorageConfig.fromConfig(withRef( + "storage.pathStateRoot { enabled = true, mode = shadow, directory = root-test, " + + "formatVersion = 1, reversibleLayerLimit = 8, reversibleLayerBytes = 4096, " + + "writeBufferBytes = 1024, rebuildFromGenesis = false, " + + "verifyEveryBlock = true }")); + assertTrue(configured.getPathStateRoot().isEnabled()); + assertEquals("root-test", configured.getPathStateRoot().getDirectory()); + assertEquals(8, configured.getPathStateRoot().getReversibleLayerLimit()); + assertEquals(4096L, configured.getPathStateRoot().getReversibleLayerBytes()); + assertEquals(1024L, configured.getPathStateRoot().getWriteBufferBytes()); + } + + @Test(expected = IllegalArgumentException.class) + public void testPathStateRootRejectsUnsupportedMode() { + StorageConfig.fromConfig(withRef("storage.pathStateRoot.mode = consensus")); + } + @Test public void testDbSettingsDefaults() { // These defaults must match develop's Args.initRocksDbSettings() fallbacks so that diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index facff23293f..5e1dba4b7f6 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -221,6 +221,20 @@ private static void applyStorageConfig(StorageConfig sc) { PARAMETER.storage.setStateArchiveDirectory(sc.getStateArchive().getDirectory()); PARAMETER.storage.setStateArchiveMaxSegmentSize(sc.getStateArchive().getMaxSegmentSize()); PARAMETER.storage.setStateArchiveQueueCapacity(sc.getStateArchive().getQueueCapacity()); + PARAMETER.storage.setPathStateRootEnabled(sc.getPathStateRoot().isEnabled()); + PARAMETER.storage.setPathStateRootMode(sc.getPathStateRoot().getMode()); + PARAMETER.storage.setPathStateRootDirectory(sc.getPathStateRoot().getDirectory()); + PARAMETER.storage.setPathStateRootFormatVersion(sc.getPathStateRoot().getFormatVersion()); + PARAMETER.storage.setPathStateRootReversibleLayerLimit( + sc.getPathStateRoot().getReversibleLayerLimit()); + PARAMETER.storage.setPathStateRootReversibleLayerBytes( + sc.getPathStateRoot().getReversibleLayerBytes()); + PARAMETER.storage.setPathStateRootWriteBufferBytes( + sc.getPathStateRoot().getWriteBufferBytes()); + PARAMETER.storage.setPathStateRootRebuildFromGenesis( + sc.getPathStateRoot().isRebuildFromGenesis()); + PARAMETER.storage.setPathStateRootVerifyEveryBlock( + sc.getPathStateRoot().isVerifyEveryBlock()); // estimatedTransactions / maxFlushCount clamping & validation run inside // TxCacheConfig.postProcess / SnapshotConfig.postProcess during bean load. diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 48102c26395..be691371a17 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -43,6 +43,17 @@ storage { stateArchive.maxSegmentSize = 1073741824 stateArchive.queueCapacity = 256 + # Experimental current-only, non-consensus path state root. Keep disabled by default. + pathStateRoot.enabled = false + pathStateRoot.mode = "shadow" + pathStateRoot.directory = "path-state-root" + pathStateRoot.formatVersion = 1 + pathStateRoot.reversibleLayerLimit = 128 + pathStateRoot.reversibleLayerBytes = 2147483648 + pathStateRoot.writeBufferBytes = 268435456 + pathStateRoot.rebuildFromGenesis = false + pathStateRoot.verifyEveryBlock = true + # If true, transaction cache initialization will be faster. Default: false txCache.initOptimization = true } diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 076a8ab5387..9d97d381873 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -311,6 +311,32 @@ public void testCliOverridesStorageConfig() { Args.clearParam(); } + @Test + public void testPathStateRootStorageConfigMapping() { + Map override = new HashMap<>(); + override.put("storage.db.directory", "database"); + override.put("storage.pathStateRoot.enabled", "true"); + override.put("storage.pathStateRoot.directory", "root-mapped"); + override.put("storage.pathStateRoot.reversibleLayerLimit", "9"); + override.put("storage.pathStateRoot.reversibleLayerBytes", "8192"); + Config config = ConfigFactory.parseMap(override) + .withFallback(ConfigFactory.defaultReference()); + + Args.applyConfigParams(config); + + Storage storage = Args.getInstance().getStorage(); + Assert.assertTrue(storage.isPathStateRootEnabled()); + Assert.assertEquals("shadow", storage.getPathStateRootMode()); + Assert.assertEquals("root-mapped", storage.getPathStateRootDirectory()); + Assert.assertEquals(1, storage.getPathStateRootFormatVersion()); + Assert.assertEquals(9, storage.getPathStateRootReversibleLayerLimit()); + Assert.assertEquals(8192L, storage.getPathStateRootReversibleLayerBytes()); + Assert.assertEquals(268435456L, storage.getPathStateRootWriteBufferBytes()); + Assert.assertFalse(storage.isPathStateRootRebuildFromGenesis()); + Assert.assertTrue(storage.isPathStateRootVerifyEveryBlock()); + Args.clearParam(); + } + /** * Verify that event.subscribe.enable = false from config is read correctly. */ diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmissionTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmissionTest.java new file mode 100644 index 00000000000..f8b8619f22d --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmissionTest.java @@ -0,0 +1,66 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateRuntimeAdmission.Status; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateRuntimeAdmissionTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void disabledAndMissingEnabledAdmissionDoNotCreateStorage() throws Exception { + Path disabled = temporaryFolder.getRoot().toPath().resolve("disabled"); + assertSame(Status.DISABLED, + PathStateRuntimeAdmission.inspect(false, null, null).getStatus()); + assertFalse(Files.exists(disabled)); + + Path missing = temporaryFolder.getRoot().toPath().resolve("missing"); + PathStateRuntimeAdmission.Result result = PathStateRuntimeAdmission.inspect( + true, missing, Engine.ROCKSDB); + assertSame(Status.REBUILD_REQUIRED, result.getStatus()); + assertNull(result.getManifest()); + assertFalse(Files.exists(missing)); + } + + @Test + public void enabledAdmissionDistinguishesRebuildFromCurrentReady() throws Exception { + Path root = temporaryFolder.getRoot().toPath().resolve("enabled"); + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen(root, Engine.ROCKSDB); + PathStateRuntimeAdmission.Result empty = PathStateRuntimeAdmission.inspect( + true, root, Engine.ROCKSDB); + assertSame(Status.REBUILD_REQUIRED, empty.getStatus()); + assertArrayEquals(manifest.getIdentityDigest(), empty.getManifest().getIdentityDigest()); + + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot state = stores.createRoot(); + PathStateRootMetadata base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), state.rootHash(), bytes(3)); + new PathStateBasePublication(manifest).publish(stores, base); + } + + PathStateRuntimeAdmission.Result ready = PathStateRuntimeAdmission.inspect( + true, root, Engine.ROCKSDB); + assertSame(Status.CURRENT_READY, ready.getStatus()); + assertArrayEquals(manifest.getIdentityDigest(), ready.getManifest().getIdentityDigest()); + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } +} From f1b75ae2c575ac4c0c2735bf4521095bc5260bde Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 15:05:47 +0800 Subject: [PATCH 080/161] feat(db): attach path state root startup --- .../main/java/org/tron/core/db/Manager.java | 56 ++++++++ ...athStateManagerStartupIntegrationTest.java | 134 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 59ef212cadd..1fe4395f87c 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -129,6 +129,11 @@ import org.tron.core.db2.archive.StateArchiveRuntimeOwner; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.stateroot.PathStateLayerLimits; +import org.tron.core.db2.stateroot.PathStateRootMetadata; +import org.tron.core.db2.stateroot.PathStateRuntimeAdmission; +import org.tron.core.db2.stateroot.PathStateSnapshotHead; +import org.tron.core.db2.stateroot.PathStateStoreManifest; import org.tron.core.exception.AccountResourceInsufficientException; import org.tron.core.exception.BadBlockException; import org.tron.core.exception.BadItemException; @@ -205,6 +210,8 @@ public class Manager { private ArchiveHistoryWriter archiveHistoryWriter; @Getter private StateArchiveRuntimeOwner stateArchiveRuntime; + @Getter + private PathStateSnapshotHead pathStateSnapshotHead; private StateArchiveRuntimeOwner.ServingIndexFaultHook stateArchiveServingIndexFaultHook = stage -> { }; private StateArchiveRuntimeOwner.ReadableStateFaultHook stateArchiveReadableStateFaultHook = @@ -588,6 +595,7 @@ public void init() { initLiteNode(); initStateArchive(); + initPathStateRoot(); long headNum = chainBaseManager.getDynamicPropertiesStore().getLatestBlockHeaderNumber(); logger.info("Current headNum is: {}.", headNum); @@ -712,6 +720,49 @@ private void initStateArchive() { } } + private void initPathStateRoot() { + org.tron.core.config.args.Storage storage = Args.getInstance().getStorage(); + if (!storage.isPathStateRootEnabled()) { + try { + PathStateRuntimeAdmission.inspect(false, null, null); + } catch (java.io.IOException impossible) { + throw new IllegalStateException("Disabled path-state admission failed", impossible); + } + return; + } + Path directory = Paths.get(Args.getInstance().getOutputDirectory(), + storage.getPathStateRootDirectory()).normalize(); + try { + PathStateStoreManifest.Engine engine = PathStateStoreManifest.Engine.valueOf( + storage.getDbEngine()); + PathStateRuntimeAdmission.Result admission = PathStateRuntimeAdmission.inspect( + true, directory, engine); + if (admission.getStatus() != PathStateRuntimeAdmission.Status.CURRENT_READY) { + throw new IllegalStateException( + "Path-state startup requires a completed admitted rebuild"); + } + PathStateLayerLimits limits = new PathStateLayerLimits( + storage.getPathStateRootReversibleLayerLimit(), + storage.getPathStateRootReversibleLayerBytes()); + PathStateSnapshotHead recovered = PathStateSnapshotHead.open( + admission.getManifest(), limits); + PathStateRootMetadata recoveredHead = recovered.getHead(); + if (recoveredHead.getBlockNumber() + != getDynamicPropertiesStore().getLatestBlockHeaderNumber() + || !Arrays.equals(recoveredHead.getBlockHash(), + getDynamicPropertiesStore().getLatestBlockHeaderHash().getBytes())) { + throw new IllegalStateException( + "Path-state CURRENT differs from the persisted Chainbase head"); + } + pathStateSnapshotHead = recovered; + logger.info("Path-state current root attached: directory={}, head={}, engine={}", + directory, recoveredHead.getBlockNumber(), storage.getDbEngine()); + } catch (java.io.IOException | RuntimeException failure) { + pathStateSnapshotHead = null; + throw new IllegalStateException("Failed to recover path-state startup", failure); + } + } + public HistoricalAccountBalanceReader.Result getArchiveAccountBalance(long blockNumber, byte[] address) throws ItemNotFoundException, BadItemException { StateArchiveRuntimeOwner runtime = stateArchiveRuntime; @@ -2862,6 +2913,7 @@ public void close() { stopFilterProcessThread(); stopValidateSignThread(); rewardViCalService.stop(); + closePathStateRoot(); closeStateArchive(); chainBaseManager.shutdown(); revokingStore.shutdown(); @@ -2882,6 +2934,10 @@ private void closeStateArchive() { } } + private void closePathStateRoot() { + pathStateSnapshotHead = null; + } + private static class ValidateSignTask implements Callable { private TransactionCapsule trx; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java new file mode 100644 index 00000000000..fee2f6bea28 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -0,0 +1,134 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.ChainBaseManager; +import org.tron.core.config.args.Storage; +import org.tron.core.db.Manager; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; +import org.tron.core.store.DynamicPropertiesStore; + +public class PathStateManagerStartupIntegrationTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void disabledAndMissingStartupDoNotCreatePathStateDirectory() throws Exception { + Path output = temporaryFolder.newFolder("startup-gates").toPath(); + Manager disabled = new Manager(); + withConfig(output, false, () -> invoke(disabled, "initPathStateRoot")); + assertNull(disabled.getPathStateSnapshotHead()); + assertFalse(Files.exists(output.resolve("path-state-root"))); + + Manager missing = new Manager(); + assertThrows(IllegalStateException.class, + () -> withConfig(output, true, () -> invoke(missing, "initPathStateRoot"))); + assertNull(missing.getPathStateSnapshotHead()); + assertFalse(Files.exists(output.resolve("path-state-root"))); + } + + @Test + public void readyCurrentAttachesExactCanonicalHeadAndCloses() throws Exception { + Path output = temporaryFolder.newFolder("startup-ready").toPath(); + Path root = output.resolve("path-state-root"); + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen(root, Engine.ROCKSDB); + PathStateRootMetadata base; + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot state = stores.createRoot(); + base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), state.rootHash(), bytes(3)); + new PathStateBasePublication(manifest).publish(stores, base); + } + + DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(100L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(base.getBlockHash())); + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); + Manager manager = new Manager(); + setChainBaseManager(manager, chainBase); + + withConfig(output, true, () -> invoke(manager, "initPathStateRoot")); + assertNotNull(manager.getPathStateSnapshotHead()); + assertArrayEquals(base.encode(), manager.getPathStateSnapshotHead().getHead().encode()); + invoke(manager, "closePathStateRoot"); + assertNull(manager.getPathStateSnapshotHead()); + + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(101L); + assertThrows(IllegalStateException.class, + () -> withConfig(output, true, () -> invoke(manager, "initPathStateRoot"))); + assertNull(manager.getPathStateSnapshotHead()); + } + + private static void withConfig(Path output, boolean enabled, ThrowingRunnable action) + throws Exception { + CommonParameter args = CommonParameter.getInstance(); + Storage oldStorage = args.getStorage(); + Storage storage = new Storage(); + String oldOutput = args.outputDirectory; + try { + args.outputDirectory = output.toString(); + args.storage = storage; + storage.setDbEngine("ROCKSDB"); + storage.setPathStateRootEnabled(enabled); + storage.setPathStateRootDirectory("path-state-root"); + storage.setPathStateRootReversibleLayerLimit(8); + storage.setPathStateRootReversibleLayerBytes(1L << 20); + action.run(); + } finally { + args.outputDirectory = oldOutput; + args.storage = oldStorage; + } + } + + private static void setChainBaseManager(Manager manager, ChainBaseManager chainBase) + throws Exception { + java.lang.reflect.Field field = Manager.class.getDeclaredField("chainBaseManager"); + field.setAccessible(true); + field.set(manager, chainBase); + } + + private static void invoke(Manager manager, String methodName) throws Exception { + Method method = Manager.class.getDeclaredMethod(methodName); + method.setAccessible(true); + try { + method.invoke(manager); + } catch (InvocationTargetException failure) { + Throwable cause = failure.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw failure; + } + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + @FunctionalInterface + private interface ThrowingRunnable { + void run() throws Exception; + } +} From da07079c3f2c3c949df44be83421cc305114d67f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 15:21:51 +0800 Subject: [PATCH 081/161] feat(db): rebuild path state root on startup --- .../main/java/org/tron/core/db/Manager.java | 70 +++++++++++++++ ...athStateManagerStartupIntegrationTest.java | 90 ++++++++++++++++++- 2 files changed, 158 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 1fe4395f87c..0afd161e156 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -129,7 +129,11 @@ import org.tron.core.db2.archive.StateArchiveRuntimeOwner; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateLayerLimits; +import org.tron.core.db2.stateroot.PathStateNativeSnapshotSource; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; import org.tron.core.db2.stateroot.PathStateRootMetadata; import org.tron.core.db2.stateroot.PathStateRuntimeAdmission; import org.tron.core.db2.stateroot.PathStateSnapshotHead; @@ -206,6 +210,8 @@ public class Manager { private static final int SLEEP_TIME_OUT = 50; private static final int TX_ID_CACHE_SIZE = 100_000; private static final int SLEEP_FOR_WAIT_LOCK = 10; + private static final int PATH_STATE_REBUILD_PAGE_SIZE = 4096; + private static final int PATH_STATE_REBUILD_MARKET_ENTRY_LIMIT = 1_000_000; @Getter private ArchiveHistoryWriter archiveHistoryWriter; @Getter @@ -737,6 +743,15 @@ private void initPathStateRoot() { storage.getDbEngine()); PathStateRuntimeAdmission.Result admission = PathStateRuntimeAdmission.inspect( true, directory, engine); + if (admission.getStatus() == PathStateRuntimeAdmission.Status.REBUILD_REQUIRED) { + if (!(revokingStore instanceof SnapshotManager)) { + throw new IllegalStateException("Path-state rebuild requires SnapshotManager"); + } + PathStateStoreManifest rebuildManifest = admission.getManifest() == null + ? PathStateStoreManifest.createOrOpen(directory, engine) : admission.getManifest(); + rebuildPathStateRoot((SnapshotManager) revokingStore, rebuildManifest); + admission = PathStateRuntimeAdmission.inspect(true, directory, engine); + } if (admission.getStatus() != PathStateRuntimeAdmission.Status.CURRENT_READY) { throw new IllegalStateException( "Path-state startup requires a completed admitted rebuild"); @@ -763,6 +778,61 @@ private void initPathStateRoot() { } } + private void rebuildPathStateRoot(SnapshotManager snapshotManager, + PathStateStoreManifest manifest) throws java.io.IOException { + java.util.Map + supplementalStores = java.util.Collections.emptyMap(); + boolean accountAssetRegistered = snapshotManager.getDbs().stream() + .anyMatch(database -> AccountAssetArchiveProjector.ACCOUNT_ASSET_DB + .equals(database.getDbName())); + if (!accountAssetRegistered) { + AccountAssetStore accountAssetStore = chainBaseManager.getAccountAssetStore(); + if (accountAssetStore == null) { + throw new java.io.IOException("Path-state rebuild requires account-asset Store"); + } + supplementalStores = java.util.Collections.singletonMap( + AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + org.tron.core.db2.archive.LatestStateGenerationAdapter.fromDataSource( + AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + accountAssetStore.getDbSource())); + } + try (PathStateNativeSnapshotSource source = PathStateNativeSnapshotSource.acquire( + snapshotManager, supplementalStores, this::readPathStateSnapshotIdentity, + PATH_STATE_REBUILD_PAGE_SIZE, PATH_STATE_REBUILD_MARKET_ENTRY_LIMIT)) { + PathStateRebuildCoordinator.RebuildResult result = new PathStateRebuildCoordinator() + .rebuild(manifest, source); + logger.info("Path-state initial root rebuilt: directory={}, head={}, entries={}", + manifest.getDirectory(), result.getMetadata().getBlockNumber(), + result.getTotalEntries()); + } + } + + private SnapshotIdentity readPathStateSnapshotIdentity() throws java.io.IOException { + DynamicPropertiesStore dynamic = getDynamicPropertiesStore(); + long blockNumber = dynamic.getLatestBlockHeaderNumber(); + try { + BlockCapsule block = chainBaseManager.getBlockByNum(blockNumber); + byte[] blockHash = dynamic.getLatestBlockHeaderHash().getBytes(); + long timestamp = dynamic.getLatestBlockHeaderTimestamp(); + if (block.getNum() != blockNumber + || !Arrays.equals(block.getBlockId().getBytes(), blockHash) + || block.getTimeStamp() != timestamp) { + throw new java.io.IOException( + "Path-state rebuild block differs from persisted Chainbase head"); + } + long allowSameTokenName = dynamic.getAllowSameTokenName(); + if (allowSameTokenName != 0 && allowSameTokenName != 1) { + throw new java.io.IOException("Path-state rebuild P66 phase is invalid"); + } + P66Phase phase = allowSameTokenName == 0 ? P66Phase.P66_OFF : P66Phase.P66_ON; + return new SnapshotIdentity(blockNumber, blockHash, block.getParentHash().getBytes(), + timestamp, phase); + } catch (BadItemException | ItemNotFoundException failure) { + throw new java.io.IOException("Path-state rebuild cannot resolve canonical head", failure); + } + } + public HistoricalAccountBalanceReader.Result getArchiveAccountBalance(long blockNumber, byte[] address) throws ItemNotFoundException, BadItemException { StateArchiveRuntimeOwner runtime = stateArchiveRuntime; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index fee2f6bea28..e9bcd5a946c 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -1,25 +1,37 @@ package org.tron.core.db2.stateroot; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.Sha256Hash; import org.tron.core.ChainBaseManager; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.config.args.Storage; import org.tron.core.db.Manager; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; +import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; +import org.tron.core.db2.common.DB; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.core.SnapshotRoot; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; import org.tron.core.store.DynamicPropertiesStore; @@ -77,6 +89,76 @@ public void readyCurrentAttachesExactCanonicalHeadAndCloses() throws Exception { assertNull(manager.getPathStateSnapshotHead()); } + @Test + public void missingCurrentRebuildsExactNativeSnapshotAndAttaches() throws Exception { + Path output = temporaryFolder.newFolder("startup-rebuild").toPath(); + long blockNumber = 100L; + long timestamp = 300L; + BlockId blockId = new BlockId(Sha256Hash.wrap(bytes(1)), blockNumber); + Sha256Hash parentHash = Sha256Hash.wrap(bytes(2)); + DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(blockNumber); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(blockId); + when(dynamic.getLatestBlockHeaderTimestamp()).thenReturn(timestamp); + when(dynamic.getAllowSameTokenName()).thenReturn(1L); + BlockCapsule block = mock(BlockCapsule.class); + when(block.getNum()).thenReturn(blockNumber); + when(block.getBlockId()).thenReturn(blockId); + when(block.getParentHash()).thenReturn(parentHash); + when(block.getTimeStamp()).thenReturn(timestamp); + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); + when(chainBase.getBlockByNum(blockNumber)).thenReturn(block); + + AtomicInteger closed = new AtomicInteger(); + Manager manager = new Manager(); + setChainBaseManager(manager, chainBase); + + withConfig(output, true, () -> { + SnapshotManager snapshots = new SnapshotManager(""); + for (PathStateParticipantDescriptor.StoreIdentity participant + : PathStateParticipantDescriptor.current().getStores()) { + snapshots.getDbs().add(emptyNativeStore(participant.getDbName(), blockNumber, + blockId.getBytes(), closed)); + } + setField(manager, "revokingStore", snapshots); + invoke(manager, "initPathStateRoot"); + }); + assertNotNull(manager.getPathStateSnapshotHead()); + PathStateRootMetadata head = manager.getPathStateSnapshotHead().getHead(); + assertArrayEquals(blockId.getBytes(), head.getBlockHash()); + assertArrayEquals(parentHash.getBytes(), head.getParentHash()); + assertNotNull(PathStateStoreManifest.validateExisting( + output.resolve("path-state-root"), Engine.ROCKSDB)); + assertEquals(PathStateParticipantDescriptor.current().getStores().size(), closed.get()); + } + + @SuppressWarnings("unchecked") + private static Chainbase emptyNativeStore(String dbName, long blockNumber, byte[] blockHash, + AtomicInteger closed) throws Exception { + DB database = mock(DB.class, + withSettings().extraInterfaces(SnapshotCapableStore.class)); + SnapshotCapableStore capable = (SnapshotCapableStore) database; + when(database.getDbName()).thenReturn(dbName); + when(capable.getDbName()).thenReturn(dbName); + when(capable.getSourceIdentity()).thenReturn("source-" + dbName); + StoreSnapshot snapshot = mock(StoreSnapshot.class); + when(snapshot.getDbName()).thenReturn(dbName); + when(snapshot.getSourceIdentity()).thenReturn("source-" + dbName); + when(snapshot.getBlockNumber()).thenReturn(blockNumber); + when(snapshot.getBlockHash()).thenReturn(blockHash); + when(snapshot.range(org.mockito.ArgumentMatchers.any(byte[].class), + org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.anyInt())) + .thenReturn(Collections.emptyList()); + org.mockito.Mockito.doAnswer(invocation -> { + closed.incrementAndGet(); + return null; + }).when(snapshot).close(); + when(capable.pin(org.mockito.ArgumentMatchers.eq(blockNumber), + org.mockito.ArgumentMatchers.any(byte[].class))).thenReturn(snapshot); + return new Chainbase(new SnapshotRoot(database)); + } + private static void withConfig(Path output, boolean enabled, ThrowingRunnable action) throws Exception { CommonParameter args = CommonParameter.getInstance(); @@ -100,9 +182,13 @@ private static void withConfig(Path output, boolean enabled, ThrowingRunnable ac private static void setChainBaseManager(Manager manager, ChainBaseManager chainBase) throws Exception { - java.lang.reflect.Field field = Manager.class.getDeclaredField("chainBaseManager"); + setField(manager, "chainBaseManager", chainBase); + } + + private static void setField(Manager manager, String name, Object value) throws Exception { + java.lang.reflect.Field field = Manager.class.getDeclaredField(name); field.setAccessible(true); - field.set(manager, chainBase); + field.set(manager, value); } private static void invoke(Manager manager, String methodName) throws Exception { From d8ac43c1255a94bbbead79d4148240a34fed57e6 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 15:40:52 +0800 Subject: [PATCH 082/161] feat(db): attach path state block transitions --- .../archive/AccountAssetArchiveProjector.java | 6 +- .../SnapshotPathStateTransitionCollector.java | 179 ++++++++++++++++++ .../tron/core/db2/core/SnapshotManager.java | 38 +++- .../stateroot/PathStateRuntimeAttachment.java | 58 ++++++ .../PathStateTransitionCollector.java | 11 ++ .../main/java/org/tron/core/db/Manager.java | 35 ++++ .../SnapshotOldValueCollectorTest.java | 64 +++++++ ...athStateManagerStartupIntegrationTest.java | 6 + 8 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateTransitionCollector.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java index add27c69f5f..19ff7a77088 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java @@ -80,7 +80,7 @@ Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue r codec.requireCanonicalLayout(phase(postAccount, projectPost), accountKey, canonicalPost.getValue(), changedAssetRows); } - return new Projection(canonicalOld, canonicalPost, reverseAssets); + return new Projection(canonicalOld, canonicalPost, reverseAssets, changedAssetRows); } boolean requiresOldPhysicalAssets(byte[] rawOld, BlockChangeView.PostValue rawPost) { @@ -163,12 +163,14 @@ static final class Projection { final OldValue oldAccount; final BlockChangeView.PostValue postAccount; final List reverseAssets; + final List changedAssetRows; private Projection(OldValue oldAccount, BlockChangeView.PostValue postAccount, - List reverseAssets) { + List reverseAssets, List changedAssetRows) { this.oldAccount = oldAccount; this.postAccount = postAccount; this.reverseAssets = Collections.unmodifiableList(new ArrayList<>(reverseAssets)); + this.changedAssetRows = Collections.unmodifiableList(new ArrayList<>(changedAssetRows)); } } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java new file mode 100644 index 00000000000..8b468142cab --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java @@ -0,0 +1,179 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import org.tron.core.db2.archive.P66AccountAssetCodec.AssetRow; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.stateroot.PathStateBlockTransition; +import org.tron.core.db2.stateroot.PathStateCanonicalizer; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateMutation; +import org.tron.core.db2.stateroot.PathStateParticipantDescriptor; +import org.tron.core.db2.stateroot.PathStateTransitionCollector; + +/** Canonicalizes the shared SnapshotManager block differ for the current path-state root. */ +public final class SnapshotPathStateTransitionCollector + implements PathStateTransitionCollector { + + private final PathStateCanonicalizer canonicalizer = new PathStateCanonicalizer(); + private final AccountAssetArchiveProjector accountAssetProjector; + private final AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource; + + public SnapshotPathStateTransitionCollector( + AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource) { + this.accountAssetProjector = new AccountAssetArchiveProjector(); + this.oldPhysicalAssetsSource = Objects.requireNonNull(oldPhysicalAssetsSource, + "oldPhysicalAssetsSource"); + } + + @Override + public PathStateBlockTransition collect(BlockChangeView view) throws IOException { + BlockChangeView admitted = Objects.requireNonNull(view, "view"); + P66Phase phase = resolvePhase(admitted); + LinkedHashMap mutations = new LinkedHashMap<>(); + for (BlockChangeView.DatabaseChanges database : admitted.getDatabases()) { + String dbName = database.getDbName(); + PathStateParticipantDescriptor.current().require(dbName); + for (BlockChangeView.Change change : database.getChanges()) { + if (AccountAssetArchiveProjector.ACCOUNT_DB.equals(dbName)) { + collectAccount(phase, database, change, mutations); + } else { + addPhysical(phase, dbName, change.getKey(), database.getPrevious(change.getKey()), + change.getPostValue(), mutations); + } + } + } + BlockSnapshotMeta meta = admitted.getMeta(); + return new PathStateBlockTransition(meta.getBlockNumber(), meta.getBlockHash(), + meta.getParentHash(), meta.getTimestamp(), phase, mutations.values()); + } + + private void collectAccount(P66Phase phase, BlockChangeView.DatabaseChanges database, + BlockChangeView.Change change, Map mutations) { + byte[] key = change.getKey(); + byte[] rawOld = database.getPrevious(key); + Map oldAssets = Collections.emptyMap(); + if (accountAssetProjector.requiresOldPhysicalAssets(rawOld, change.getPostValue())) { + oldAssets = AccountAssetOldPhysicalAssetsSource.captureRequired( + oldPhysicalAssetsSource, key); + } + AccountAssetArchiveProjector.Projection projection = accountAssetProjector.project( + key, rawOld, change.getPostValue(), phase != P66Phase.P66_OFF, oldAssets); + addCanonical(AccountAssetArchiveProjector.ACCOUNT_DB, key, projection.oldAccount, + projection.postAccount, phase, mutations); + for (AssetRow asset : projection.changedAssetRows) { + addPhysical(phase, AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + asset.getPhysicalRawKey(), null, asset.getPostValue(), mutations); + } + } + + private void addPhysical(P66Phase phase, String dbName, byte[] key, byte[] rawOld, + BlockChangeView.PostValue rawPost, Map mutations) { + PathStateMutation oldMutation = rawOld == null ? null + : canonicalizer.put(phase, dbName, key, rawOld); + PathStateMutation postMutation = rawPost.isPresent() + ? canonicalizer.put(phase, dbName, key, rawPost.getValue()) + : canonicalizer.delete(phase, dbName, key); + if (oldMutation != null && same(oldMutation, postMutation)) { + return; + } + add(postMutation, mutations); + } + + private void addCanonical(String dbName, byte[] key, OldValue oldValue, + BlockChangeView.PostValue postValue, P66Phase phase, + Map mutations) { + PathStateMutation oldMutation = oldValue.isPresent() + ? canonicalizer.put(phase, dbName, key, oldValue.getValue()) : null; + PathStateMutation postMutation = postValue.isPresent() + ? canonicalizer.put(phase, dbName, key, postValue.getValue()) + : canonicalizer.delete(phase, dbName, key); + if (oldMutation != null && same(oldMutation, postMutation)) { + return; + } + add(postMutation, mutations); + } + + private void add(PathStateMutation mutation, + Map mutations) { + MutationKey key = new MutationKey(mutation.getDbName(), mutation.getCanonicalKey()); + PathStateMutation previous = mutations.putIfAbsent(key, mutation); + if (previous != null && !same(previous, mutation)) { + throw new ArchivePersistenceException("Conflicting path-state block mutation"); + } + } + + private P66Phase resolvePhase(BlockChangeView view) throws IOException { + byte[] propertyKey = HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(); + for (BlockChangeView.DatabaseChanges database : view.getDatabases()) { + if (!HistoricalAccountAssetBalanceResolver.PROPERTIES_DATABASE.equals( + database.getDbName())) { + continue; + } + long previous = decodeP66(database.getPrevious(propertyKey), "previous"); + long target = previous; + for (BlockChangeView.Change change : database.getChanges()) { + if (Arrays.equals(propertyKey, change.getKey())) { + target = decodeP66(change.getPostValue().isPresent() + ? change.getPostValue().getValue() : null, "target"); + } + } + if (previous == 1L && target == 0L) { + throw new IOException("path-state P66 phase cannot move backwards"); + } + if (previous == 0L && target == 1L) { + throw new IOException("path-state P66 activation requires an explicit rebuild"); + } + return target == 0L ? P66Phase.P66_OFF : P66Phase.P66_ON; + } + throw new IOException("path-state properties Store is absent from block differ"); + } + + private long decodeP66(byte[] value, String label) throws IOException { + if (value == null || value.length != Long.BYTES) { + throw new IOException("path-state " + label + " P66 property is invalid"); + } + long decoded = ByteBuffer.wrap(value).getLong(); + if (decoded != 0L && decoded != 1L) { + throw new IOException("path-state " + label + " P66 property is invalid"); + } + return decoded; + } + + private static boolean same(PathStateMutation left, PathStateMutation right) { + return left.isDelete() == right.isDelete() + && left.getDbName().equals(right.getDbName()) + && Arrays.equals(left.getCanonicalKey(), right.getCanonicalKey()) + && Arrays.equals(left.getCanonicalValue(), right.getCanonicalValue()); + } + + private static final class MutationKey { + + private final String dbName; + private final byte[] key; + + private MutationKey(String dbName, byte[] key) { + this.dbName = dbName; + this.key = Arrays.copyOf(key, key.length); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof MutationKey)) { + return false; + } + MutationKey that = (MutationKey) other; + return dbName.equals(that.dbName) && Arrays.equals(key, that.key); + } + + @Override + public int hashCode() { + return 31 * dbName.hashCode() + Arrays.hashCode(key); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index 2feba4f4dc2..8911ba5fcb8 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -50,6 +50,8 @@ import org.tron.core.db2.archive.DurableHistoryMarkerRangeEvidence; import org.tron.core.db2.archive.HistoryCommitMarker; import org.tron.core.db2.archive.OldValueCollector; +import org.tron.core.db2.stateroot.PathStateBlockTransition; +import org.tron.core.db2.stateroot.PathStateRuntimeAttachment; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.IRevokingDB; import org.tron.core.db2.common.Key; @@ -102,6 +104,7 @@ public class SnapshotManager implements RevokingDatabase { private OldValueCollector oldValueCollector; private ArchiveRuntimeAttachment archiveRuntimeAttachment; + private PathStateRuntimeAttachment pathStateRuntimeAttachment; private Long submittedArchiveHistoryEpoch; private BlockReverseDiffSink blockReverseDiffSink; @Getter @@ -262,12 +265,18 @@ public synchronized void commit(BlockSnapshotMeta meta) { } } + BlockChangeView changeView = null; + if (oldValueCollector != null || pathStateRuntimeAttachment != null) { + changeView = BlockChangeView.capture(meta, dbs); + } BlockReverseDiff reverseDiff = null; if (oldValueCollector != null) { reverseDiff = Objects.requireNonNull( - oldValueCollector.collect(BlockChangeView.capture(meta, dbs)), + oldValueCollector.collect(changeView), "archive collector returned null"); } + PathStateBlockTransition pathStateTransition = pathStateRuntimeAttachment == null ? null + : pathStateRuntimeAttachment.capture(changeView); dbs.forEach(db -> { if (db.getHead().isOptimized()) { @@ -280,6 +289,9 @@ public synchronized void commit(BlockSnapshotMeta meta) { ArchiveStoreScope.isStateDatabase(db.getDbName()) ? reverseDiff : null); } --activeSession; + if (pathStateRuntimeAttachment != null) { + pathStateRuntimeAttachment.publish(pathStateTransition); + } } private void validateBlockMeta(BlockSnapshotMeta meta) { @@ -357,6 +369,30 @@ public synchronized ArchiveRuntimeAttachment detachArchiveRuntime( return candidate; } + /** Installs an independent non-consensus path-state block-final runtime. */ + public synchronized void attachPathStateRuntime(PathStateRuntimeAttachment attachment) { + ArchiveStoreScope.validate(dbs); + PathStateRuntimeAttachment candidate = Objects.requireNonNull(attachment, "attachment"); + if (pathStateRuntimeAttachment != null) { + throw new IllegalStateException("Path-state runtime is already attached"); + } + pathStateRuntimeAttachment = candidate; + } + + /** Detaches the exact borrowed path-state runtime without closing its Manager-owned state. */ + public synchronized PathStateRuntimeAttachment detachPathStateRuntime( + PathStateRuntimeAttachment expected) { + PathStateRuntimeAttachment candidate = Objects.requireNonNull(expected, "expected"); + if (pathStateRuntimeAttachment == null) { + throw new IllegalStateException("Path-state runtime is not attached"); + } + if (pathStateRuntimeAttachment != candidate) { + throw new IllegalStateException("Cannot detach a foreign path-state runtime"); + } + pathStateRuntimeAttachment = null; + return candidate; + } + /** Runs latest-state snapshot acquisition inside the canonical apply/flush monitor. */ public synchronized void withArchiveStateBarrier(ArchiveStateAction action) throws IOException { Objects.requireNonNull(action, "action").run(); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java new file mode 100644 index 00000000000..80831ae6205 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java @@ -0,0 +1,58 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.util.Objects; +import org.tron.core.db2.archive.BlockChangeView; + +/** Independent non-consensus runtime installed at the metadata-aware block commit boundary. */ +public final class PathStateRuntimeAttachment { + + private final PathStateTransitionCollector collector; + private final TransitionSink sink; + private Throwable failure; + + public PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink) { + this.collector = Objects.requireNonNull(collector, "collector"); + this.sink = Objects.requireNonNull(sink, "sink"); + } + + /** Capture failures fail only this shadow runtime and never reject the canonical block. */ + public synchronized PathStateBlockTransition capture(BlockChangeView view) { + if (failure != null) { + return null; + } + try { + return Objects.requireNonNull(collector.collect(view), + "path-state collector returned null"); + } catch (IOException | RuntimeException currentFailure) { + failure = currentFailure; + return null; + } + } + + /** Durable publication failures are retained as observable fail-stop state. */ + public synchronized void publish(PathStateBlockTransition transition) { + if (failure != null || transition == null) { + return; + } + try { + sink.accept(transition); + } catch (IOException | RuntimeException currentFailure) { + failure = currentFailure; + } + } + + public synchronized boolean isFailed() { + return failure != null; + } + + public synchronized Throwable getFailure() { + return failure; + } + + @FunctionalInterface + public interface TransitionSink { + + void accept(PathStateBlockTransition transition) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateTransitionCollector.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateTransitionCollector.java new file mode 100644 index 00000000000..14d3d672fb7 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateTransitionCollector.java @@ -0,0 +1,11 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import org.tron.core.db2.archive.BlockChangeView; + +/** Builds one immutable path-state transition from the shared block-final change view. */ +@FunctionalInterface +public interface PathStateTransitionCollector { + + PathStateBlockTransition collect(BlockChangeView view) throws IOException; +} diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 0afd161e156..ec7ed296e99 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -126,6 +126,7 @@ import org.tron.core.db2.archive.HistoricalAccountBalanceReader; import org.tron.core.db2.archive.OldValue; import org.tron.core.db2.archive.SnapshotOldValueCollector; +import org.tron.core.db2.archive.SnapshotPathStateTransitionCollector; import org.tron.core.db2.archive.StateArchiveRuntimeOwner; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; @@ -136,6 +137,7 @@ import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; import org.tron.core.db2.stateroot.PathStateRootMetadata; import org.tron.core.db2.stateroot.PathStateRuntimeAdmission; +import org.tron.core.db2.stateroot.PathStateRuntimeAttachment; import org.tron.core.db2.stateroot.PathStateSnapshotHead; import org.tron.core.db2.stateroot.PathStateStoreManifest; import org.tron.core.exception.AccountResourceInsufficientException; @@ -218,6 +220,8 @@ public class Manager { private StateArchiveRuntimeOwner stateArchiveRuntime; @Getter private PathStateSnapshotHead pathStateSnapshotHead; + @Getter + private PathStateRuntimeAttachment pathStateRuntime; private StateArchiveRuntimeOwner.ServingIndexFaultHook stateArchiveServingIndexFaultHook = stage -> { }; private StateArchiveRuntimeOwner.ReadableStateFaultHook stateArchiveReadableStateFaultHook = @@ -770,6 +774,7 @@ private void initPathStateRoot() { "Path-state CURRENT differs from the persisted Chainbase head"); } pathStateSnapshotHead = recovered; + attachPathStateBlockFinalRuntime(); logger.info("Path-state current root attached: directory={}, head={}, engine={}", directory, recoveredHead.getBlockNumber(), storage.getDbEngine()); } catch (java.io.IOException | RuntimeException failure) { @@ -778,6 +783,28 @@ private void initPathStateRoot() { } } + private void attachPathStateBlockFinalRuntime() { + if (!(revokingStore instanceof SnapshotManager)) { + throw new IllegalStateException("Path-state block-final capture requires SnapshotManager"); + } + AccountAssetStore accountAssetStore = chainBaseManager.getAccountAssetStore(); + if (accountAssetStore == null) { + throw new IllegalStateException( + "Path-state block-final capture requires account-asset Store"); + } + PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment( + new SnapshotPathStateTransitionCollector(accountAssetStore::prefixQuery), + transition -> { + PathStateSnapshotHead owner = pathStateSnapshotHead; + if (owner == null) { + throw new java.io.IOException("Path-state snapshot owner is unavailable"); + } + owner.advance(transition); + }); + ((SnapshotManager) revokingStore).attachPathStateRuntime(attachment); + pathStateRuntime = attachment; + } + private void rebuildPathStateRoot(SnapshotManager snapshotManager, PathStateStoreManifest manifest) throws java.io.IOException { java.util.Map { }); + AtomicReference published = new AtomicReference<>(); + PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment( + new SnapshotPathStateTransitionCollector(key -> Collections.emptyMap()), published::set); + manager.attachPathStateRuntime(attachment); + + byte[] key = bytes("contract"); + try (ISession block = manager.buildSession()) { + code.put(key, bytes("runtime")); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L)); + } + + assertFalse(attachment.isFailed()); + assertEquals(1, published.get().getBlockNumber()); + assertEquals(1, published.get().getMutations().size()); + assertEquals("code", published.get().getMutations().get(0).getDbName()); + assertArrayEquals(key, published.get().getMutations().get(0).getCanonicalKey()); + assertSame(attachment, manager.detachPathStateRuntime(attachment)); + manager.shutdown(); + } + + @Test + public void pathStateFailureDoesNotRejectArchiveDisabledBlockCommit() throws Exception { + SnapshotManager manager = new SnapshotManager(""); + Chainbase code = new Chainbase(new SnapshotRoot(new MemoryDb("code"))); + manager.add(code); + manager.enable(); + AtomicInteger published = new AtomicInteger(); + PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment(view -> { + throw new IOException("capture failed"); + }, transition -> published.incrementAndGet()); + manager.attachPathStateRuntime(attachment); + + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); + try (ISession block = manager.buildSession()) { + code.put(bytes("contract"), bytes("runtime")); + block.commit(meta); + } + + assertTrue(attachment.isFailed()); + assertEquals("capture failed", attachment.getFailure().getMessage()); + assertEquals(0, published.get()); + assertEquals(meta, ((SnapshotImpl) code.getHead()).getBlockSnapshotMeta()); + manager.detachPathStateRuntime(attachment); + manager.shutdown(); + } + @Test public void borrowedRuntimeAttachmentIsAtomicAndIdentityBound() throws Exception { SnapshotManager manager = new SnapshotManager(""); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index e9bcd5a946c..35b1fe32fe8 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -34,6 +34,7 @@ import org.tron.core.db2.core.SnapshotRoot; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; +import org.tron.core.store.AccountAssetStore; import org.tron.core.store.DynamicPropertiesStore; public class PathStateManagerStartupIntegrationTest { @@ -74,14 +75,18 @@ public void readyCurrentAttachesExactCanonicalHeadAndCloses() throws Exception { when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(base.getBlockHash())); ChainBaseManager chainBase = mock(ChainBaseManager.class); when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); + when(chainBase.getAccountAssetStore()).thenReturn(mock(AccountAssetStore.class)); Manager manager = new Manager(); setChainBaseManager(manager, chainBase); + setField(manager, "revokingStore", new SnapshotManager("")); withConfig(output, true, () -> invoke(manager, "initPathStateRoot")); assertNotNull(manager.getPathStateSnapshotHead()); + assertNotNull(manager.getPathStateRuntime()); assertArrayEquals(base.encode(), manager.getPathStateSnapshotHead().getHead().encode()); invoke(manager, "closePathStateRoot"); assertNull(manager.getPathStateSnapshotHead()); + assertNull(manager.getPathStateRuntime()); when(dynamic.getLatestBlockHeaderNumber()).thenReturn(101L); assertThrows(IllegalStateException.class, @@ -109,6 +114,7 @@ public void missingCurrentRebuildsExactNativeSnapshotAndAttaches() throws Except ChainBaseManager chainBase = mock(ChainBaseManager.class); when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); when(chainBase.getBlockByNum(blockNumber)).thenReturn(block); + when(chainBase.getAccountAssetStore()).thenReturn(mock(AccountAssetStore.class)); AtomicInteger closed = new AtomicInteger(); Manager manager = new Manager(); From c59a54abf7acadd2475d3ede62ab67605ab776b7 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 15:53:28 +0800 Subject: [PATCH 083/161] feat(db): rewind path state during reorg --- .../db2/stateroot/PathStateCurrentStore.java | 35 ++++++++++++ .../stateroot/PathStateRuntimeAttachment.java | 7 +++ .../db2/stateroot/PathStateSnapshotHead.java | 26 +++++++++ .../main/java/org/tron/core/db/Manager.java | 19 +++++++ ...athStateManagerStartupIntegrationTest.java | 54 +++++++++++++++++++ .../stateroot/PathStateSnapshotHeadTest.java | 26 +++++++++ 6 files changed, 167 insertions(+) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java index c09958f5cbb..a656bc788df 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java @@ -118,6 +118,35 @@ synchronized List layersAboveAncestor(PathStateRootMetada throw new IOException("path-state canonical switch exceeds the reversible window"); } + /** Resolves an exact number/hash ancestor without exposing historical-root lookup. */ + synchronized PathStateRootMetadata findAncestor(long blockNumber, byte[] blockHash, + PathStateLayerLimits limits) throws IOException { + byte[] admittedHash = Arrays.copyOf(Objects.requireNonNull(blockHash, "blockHash"), + blockHash.length); + PathStateLayerLimits admittedLimits = Objects.requireNonNull(limits, "limits"); + PathStateRootMetadata base = requireKind(PathStateMetadataFile.load(basePath()), Kind.BASE); + requireFormat(base); + PathStateRootMetadata cursor = current(); + if (matchesBlock(cursor, blockNumber, admittedHash)) { + verifyTargetState(cursor); + return cursor; + } + if (blockNumber >= cursor.getBlockNumber()) { + throw new IOException("path-state canonical target is not an ancestor"); + } + for (int depth = 1; depth <= admittedLimits.getMaxLayers(); depth++) { + cursor = parentOf(cursor, base); + if (matchesBlock(cursor, blockNumber, admittedHash)) { + verifyTargetState(cursor); + return cursor; + } + if (cursor.getKind() == Kind.BASE) { + break; + } + } + throw new IOException("path-state canonical target exceeds the reversible window"); + } + /** Loads CURRENT and verifies that every referenced layer reaches the single durable base. */ public synchronized PathStateRootMetadata current() throws IOException { PathStateRootMetadata base = requireKind(PathStateMetadataFile.load(basePath()), Kind.BASE); @@ -234,4 +263,10 @@ private static boolean same(PathStateRootMetadata left, PathStateRootMetadata ri return Arrays.equals(left.encode(), right.encode()); } + private static boolean matchesBlock(PathStateRootMetadata metadata, long blockNumber, + byte[] blockHash) { + return metadata.getBlockNumber() == blockNumber + && Arrays.equals(metadata.getBlockHash(), blockHash); + } + } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java index 80831ae6205..45946fba296 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java @@ -50,6 +50,13 @@ public synchronized Throwable getFailure() { return failure; } + /** Marks an externally coordinated lifecycle operation as failed without replacing first cause. */ + public synchronized void fail(Throwable currentFailure) { + if (failure == null) { + failure = Objects.requireNonNull(currentFailure, "currentFailure"); + } + } + @FunctionalInterface public interface TransitionSink { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java index ba4211765b1..f9b05421640 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java @@ -45,6 +45,32 @@ public synchronized PathStateRootMetadata advance(PathStateBlockTransition trans return advancePrepared(prepare(transition)); } + /** Switches the owned durable CURRENT and snapshot to an exact reversible ancestor. */ + public synchronized PathStateRootMetadata rewindTo(long blockNumber, byte[] blockHash) + throws IOException { + requireHealthy(); + PathStateRootMetadata previous = head; + try { + PathStateCurrentStore currentStore = new PathStateCurrentStore(manifest); + PathStateRootMetadata target = currentStore.findAncestor(blockNumber, blockHash, limits); + PathStateRootMetadata switched = new PathStateLayerRetirement(manifest, limits) + .switchToAncestor(target); + PathStateSnapshotHead restored = open(manifest, limits); + if (!same(switched, restored.head) + || restored.head.getBlockNumber() != blockNumber + || !Arrays.equals(restored.head.getBlockHash(), blockHash)) { + failed = true; + throw new IOException("path-state rewound snapshot identity mismatch"); + } + head = restored.head; + snapshot = restored.snapshot; + return head; + } catch (IOException | RuntimeException failure) { + failIfAuthorityMoved(previous, failure); + throw failure; + } + } + /** Computes an immutable candidate without opening or changing durable path-state storage. */ public synchronized PreparedPathStateTransition prepare(PathStateBlockTransition transition) throws IOException { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index ec7ed296e99..0603590a293 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1380,6 +1380,7 @@ public void eraseBlock() { logger.info("Start to erase block: {}.", oldHeadBlock); khaosDb.pop(); revokingStore.fastPop(); + rewindPathStateRootAfterPop(); logger.info("End to erase block: {}.", oldHeadBlock); oldHeadBlock.getTransactions().forEach(tc -> poppedTransactions.add(new TransactionCapsule(tc.getInstance()))); @@ -1391,6 +1392,24 @@ public void eraseBlock() { } } + /** Keeps the non-consensus path-state head aligned after Chainbase owns a successful pop. */ + private void rewindPathStateRootAfterPop() { + PathStateSnapshotHead owner = pathStateSnapshotHead; + if (owner == null) { + return; + } + try { + owner.rewindTo(getDynamicPropertiesStore().getLatestBlockHeaderNumber(), + getDynamicPropertiesStore().getLatestBlockHeaderHash().getBytes()); + } catch (java.io.IOException | RuntimeException failure) { + PathStateRuntimeAttachment runtime = pathStateRuntime; + if (runtime != null) { + runtime.fail(failure); + } + logger.error("Path-state short-reorg rewind failed after canonical block pop", failure); + } + } + private void applyBlock(BlockCapsule block) throws ContractValidateException, ContractExeException, ValidateSignatureException, AccountResourceInsufficientException, TransactionExpirationException, TooBigTransactionException, DupTransactionException, diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index 35b1fe32fe8..c382b9351e8 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -94,6 +94,54 @@ public void readyCurrentAttachesExactCanonicalHeadAndCloses() throws Exception { assertNull(manager.getPathStateSnapshotHead()); } + @Test + public void shortReorgRewindsToChainbaseHeadAndRetiresOldSuffix() throws Exception { + Path output = temporaryFolder.newFolder("short-reorg").toPath(); + Path root = output.resolve("path-state-root"); + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen(root, Engine.ROCKSDB); + PathStateRootMetadata base; + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot state = stores.createRoot(); + base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), state.rootHash(), bytes(3)); + new PathStateBasePublication(manifest).publish(stores, base); + } + PathStateSnapshotHead builder = PathStateSnapshotHead.open( + manifest, PathStateLayerLimits.defaults()); + PathStateRootMetadata first = builder.advance(transition(101, 11, base.getBlockHash())); + PathStateRootMetadata oldSecond = builder.advance(transition(102, 12, + first.getBlockHash())); + + DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(102L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn( + Sha256Hash.wrap(oldSecond.getBlockHash())); + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); + when(chainBase.getAccountAssetStore()).thenReturn(mock(AccountAssetStore.class)); + Manager manager = new Manager(); + setChainBaseManager(manager, chainBase); + setField(manager, "revokingStore", new SnapshotManager("")); + + withConfig(output, true, () -> invoke(manager, "initPathStateRoot")); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(101L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(first.getBlockHash())); + invoke(manager, "rewindPathStateRootAfterPop"); + + assertArrayEquals(first.encode(), manager.getPathStateSnapshotHead().getHead().encode()); + assertArrayEquals(first.encode(), new PathStateCurrentStore(manifest).current().encode()); + assertFalse(Files.exists(manifest.getLayerDirectory( + oldSecond.getBlockNumber(), oldSecond.getBlockHash()))); + assertFalse(manager.getPathStateRuntime().isFailed()); + + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(100L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(bytes(99))); + invoke(manager, "rewindPathStateRootAfterPop"); + assertNotNull(manager.getPathStateRuntime().getFailure()); + assertArrayEquals(first.encode(), new PathStateCurrentStore(manifest).current().encode()); + invoke(manager, "closePathStateRoot"); + } + @Test public void missingCurrentRebuildsExactNativeSnapshotAndAttaches() throws Exception { Path output = temporaryFolder.newFolder("startup-rebuild").toPath(); @@ -219,6 +267,12 @@ private static byte[] bytes(int seed) { return value; } + private static PathStateBlockTransition transition(long blockNumber, int seed, + byte[] parentHash) { + return new PathStateBlockTransition(blockNumber, bytes(seed), parentHash, + blockNumber * 3, P66Phase.P66_ON, Collections.emptyList()); + } + @FunctionalInterface private interface ThrowingRunnable { void run() throws Exception; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java index 3ab5a2366a7..e567a9fecca 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java @@ -77,6 +77,32 @@ public void commitAdmissionFailureKeepsOwnedSnapshotAndCurrent() throws Exceptio assertFalse(owner.isFailed()); } + @Test + public void rewindsOwnedHeadThenBuildsCanonicalSibling() throws Exception { + for (Engine engine : availableEngines()) { + Fixture fixture = fixture("rewind-" + engine, engine); + PathStateSnapshotHead owner = PathStateSnapshotHead.open( + fixture.manifest, PathStateLayerLimits.defaults()); + PathStateRootMetadata first = owner.advance(transition(101, 11, + fixture.base.getBlockHash(), Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5})))); + PathStateRootMetadata oldSecond = owner.advance(transition(102, 12, + first.getBlockHash(), Collections.emptyList())); + + assertArrayEquals(first.encode(), owner.rewindTo( + first.getBlockNumber(), first.getBlockHash()).encode()); + assertFalse(Files.exists(fixture.manifest.getLayerDirectory( + oldSecond.getBlockNumber(), oldSecond.getBlockHash()))); + PathStateRootMetadata sibling = owner.advance(transition(102, 22, + first.getBlockHash(), Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{9})))); + + assertArrayEquals(sibling.encode(), PathStateSnapshotHead.open( + fixture.manifest, PathStateLayerLimits.defaults()).getHead().encode()); + assertFalse(owner.isFailed()); + } + } + private Fixture fixture(String name, Engine engine) throws Exception { PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen( new File(temporaryFolder.getRoot(), name).toPath(), engine); From 9eba955110482ceaa7bc78062114880af9fdbae2 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 16:11:11 +0800 Subject: [PATCH 084/161] feat(db): compact durable path state base --- .../tron/core/db2/core/SnapshotManager.java | 29 ++ .../stateroot/PathStateBaseCompaction.java | 255 ++++++++++++++++++ .../db2/stateroot/PathStateCurrentStore.java | 20 ++ .../db2/stateroot/PathStateNodeStoreSet.java | 20 ++ .../stateroot/PathStateRuntimeAttachment.java | 25 ++ .../db2/stateroot/PathStateSnapshotHead.java | 20 ++ .../db2/stateroot/PathStateStoreManifest.java | 16 +- .../main/java/org/tron/core/db/Manager.java | 27 +- .../SnapshotOldValueCollectorTest.java | 37 +++ .../PathStateBaseCompactionTest.java | 188 +++++++++++++ 10 files changed, 629 insertions(+), 8 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBaseCompaction.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateBaseCompactionTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index 8911ba5fcb8..7b0ae842fc6 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -576,6 +576,7 @@ private synchronized void flush(boolean force) { if (force || shouldBeRefreshed()) { try { long start = System.currentTimeMillis(); + BlockSnapshotMeta pathStateFlushTarget = pathStateFlushTarget(); ArchiveWalBinding archiveBinding = publishArchiveHistoryForFlush(); if (!isV2Open()) { deleteCheckpoint(); @@ -591,6 +592,10 @@ private synchronized void flush(boolean force) { } } refresh(); + if (pathStateRuntimeAttachment != null && pathStateFlushTarget != null) { + pathStateRuntimeAttachment.flushBaseThrough(pathStateFlushTarget.getBlockNumber(), + pathStateFlushTarget.getBlockHash()); + } if (archiveBinding != null && archiveRuntimeAttachment != null) { try { archiveRuntimeAttachment.publishReadableState(archiveBinding.getLast()); @@ -617,6 +622,30 @@ private synchronized void flush(boolean force) { } } + private BlockSnapshotMeta pathStateFlushTarget() { + if (pathStateRuntimeAttachment == null || flushCount == 0 || dbs.isEmpty()) { + return null; + } + try { + Snapshot next = dbs.get(0).getHead().getRoot(); + BlockSnapshotMeta target = null; + for (int index = 0; index < flushCount; index++) { + next = next.getNext(); + if (!Snapshot.isImpl(next)) { + throw new IllegalStateException("Path-state flush range is missing a snapshot layer"); + } + target = ((SnapshotImpl) next).getBlockSnapshotMeta(); + if (target == null) { + throw new IllegalStateException("Path-state flush layer has no block metadata"); + } + } + return target; + } catch (RuntimeException failure) { + pathStateRuntimeAttachment.fail(failure); + return null; + } + } + private ArchiveWalBinding publishArchiveHistoryForFlush() { if (oldValueCollector == null) { return null; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBaseCompaction.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBaseCompaction.java new file mode 100644 index 00000000000..64fa01331da --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBaseCompaction.java @@ -0,0 +1,255 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; +import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; + +/** Crash-recoverable materialization of a canonical layer prefix into the single durable BASE. */ +public final class PathStateBaseCompaction { + + public static final String INTENT_FILE = "BASE_FLUSH_INTENT"; + static final String NEXT_DIRECTORY = "base.next"; + static final String PREVIOUS_DIRECTORY = "base.previous"; + + private final PathStateStoreManifest manifest; + private final PathStateLayerLimits limits; + private final FaultHook faultHook; + private final Path root; + private final Path intentPath; + private final Path basePath; + private final Path nextPath; + private final Path previousPath; + + public PathStateBaseCompaction(PathStateStoreManifest manifest, + PathStateLayerLimits limits) { + this(manifest, limits, stage -> { }); + } + + PathStateBaseCompaction(PathStateStoreManifest manifest, PathStateLayerLimits limits, + FaultHook faultHook) { + this.manifest = Objects.requireNonNull(manifest, "manifest"); + this.limits = Objects.requireNonNull(limits, "limits"); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + this.root = manifest.getDirectory(); + this.intentPath = root.resolve(INTENT_FILE); + this.basePath = manifest.getBaseDirectory(); + this.nextPath = root.resolve(NEXT_DIRECTORY); + this.previousPath = root.resolve(PREVIOUS_DIRECTORY); + } + + /** Materializes canonical layers through an exact non-head target, one prefix layer at a time. */ + public synchronized PathStateRootMetadata compactThrough(long blockNumber, byte[] blockHash) + throws IOException { + byte[] targetHash = Arrays.copyOf(Objects.requireNonNull(blockHash, "blockHash"), + blockHash.length); + recover(); + while (true) { + PathStateCurrentStore currentStore = new PathStateCurrentStore(manifest); + PathStateRootMetadata base = loadBase(); + if (base.getBlockNumber() == blockNumber + && Arrays.equals(base.getBlockHash(), targetHash)) { + return base; + } + PathStateRootMetadata head = currentStore.current(); + PathStateRootMetadata target = currentStore.findAncestor(blockNumber, targetHash, limits); + if (target.getKind() != Kind.LAYER || target.getBlockNumber() >= head.getBlockNumber()) { + throw new IOException("path-state base flush must retain a newer reversible head"); + } + PathStateRootMetadata first = currentStore.firstLayerAfterBaseToward(target, limits); + PathStateRootMetadata replacement = asBase(first); + PathStateMetadataFile.publishImmutable(intentPath, replacement); + faultHook.after(Stage.AFTER_INTENT); + finish(replacement); + } + } + + /** Completes an interrupted one-layer BASE replacement and becomes a zero-action retry. */ + public synchronized RecoveryAction recover() throws IOException { + if (!Files.exists(intentPath, LinkOption.NOFOLLOW_LINKS)) { + if (Files.exists(nextPath, LinkOption.NOFOLLOW_LINKS) + || Files.exists(previousPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state base flush has orphan replacement directories"); + } + return RecoveryAction.NONE; + } + PathStateRootMetadata replacement = requireBase(PathStateMetadataFile.load(intentPath)); + finish(replacement); + return RecoveryAction.COMPLETED_COMPACTION; + } + + private void finish(PathStateRootMetadata replacement) throws IOException { + PathStateRootMetadata installed = metadataIfPresent(basePath); + if (installed == null || !same(installed, replacement)) { + if (Files.exists(previousPath, LinkOption.NOFOLLOW_LINKS)) { + if (Files.exists(basePath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state base flush has ambiguous directory authority"); + } + verifyPreparedNext(replacement); + } else { + if (installed == null) { + throw new IOException("path-state base flush lost the previous BASE"); + } + deleteDirectory(nextPath); + buildNext(replacement); + faultHook.after(Stage.AFTER_NEXT); + moveDirectory(basePath, previousPath); + faultHook.after(Stage.AFTER_OLD_BASE); + } + moveDirectory(nextPath, basePath); + faultHook.after(Stage.AFTER_BASE); + } + verifyInstalledBase(replacement); + deleteDirectory(manifest.getLayerDirectory( + replacement.getBlockNumber(), replacement.getBlockHash())); + faultHook.after(Stage.AFTER_LAYER_RETIRE); + deleteDirectory(previousPath); + faultHook.after(Stage.AFTER_PREVIOUS_RETIRE); + PathStateMetadataFile.deleteDurable(intentPath); + faultHook.after(Stage.AFTER_RETIRE); + new PathStateCurrentStore(manifest).current(); + } + + private void buildNext(PathStateRootMetadata replacement) throws IOException { + Path layerPath = manifest.getLayerDirectory( + replacement.getBlockNumber(), replacement.getBlockHash()); + PathStateRootMetadata layer = PathStateMetadataFile.load( + layerPath.resolve(PathStateCurrentStore.METADATA_FILE)); + if (layer.getKind() != Kind.LAYER + || layer.getBlockNumber() != replacement.getBlockNumber() + || !Arrays.equals(layer.getBlockHash(), replacement.getBlockHash()) + || !Arrays.equals(layer.getStateRoot(), replacement.getStateRoot())) { + throw new IOException("path-state base flush source layer identity mismatch"); + } + Files.createDirectory(nextPath); + try (PathStateNodeStoreSet source = PathStateNodeStoreSet.openPublished(manifest, layer); + PathStateNodeStoreSet destination = PathStateNodeStoreSet.beginBaseAt(manifest, nextPath)) { + PathStateRoot sourceRoot = source.createRoot(); + sourceRoot.verifyNodeStores(); + List leaves = new ArrayList<>(source.leafRecords()); + PathStateRoot destinationRoot = destination.initializeBase(leaves, layer.getStateRoot()); + if (!Arrays.equals(destinationRoot.rootHash(), replacement.getStateRoot())) { + throw new IOException("path-state replacement BASE root mismatch"); + } + destination.commit(replacement); + } + PathStateMetadataFile.publishImmutable( + nextPath.resolve(PathStateCurrentStore.METADATA_FILE), replacement); + verifyPreparedNext(replacement); + } + + private void verifyPreparedNext(PathStateRootMetadata replacement) throws IOException { + PathStateMetadataFile.requireExact( + nextPath.resolve(PathStateCurrentStore.METADATA_FILE), replacement); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.beginBaseAt(manifest, nextPath)) { + PathStateRoot restored = stores.createRoot(); + if (!Arrays.equals(restored.rootHash(), replacement.getStateRoot())) { + throw new IOException("path-state prepared BASE root mismatch"); + } + restored.verifyNodeStores(); + } + } + + private void verifyInstalledBase(PathStateRootMetadata replacement) throws IOException { + PathStateMetadataFile.requireExact( + basePath.resolve(PathStateCurrentStore.METADATA_FILE), replacement); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot restored = stores.createRoot(); + if (!Arrays.equals(restored.rootHash(), replacement.getStateRoot())) { + throw new IOException("path-state installed BASE root mismatch"); + } + restored.verifyNodeStores(); + } + } + + private PathStateRootMetadata loadBase() throws IOException { + return requireBase(PathStateMetadataFile.load( + basePath.resolve(PathStateCurrentStore.METADATA_FILE))); + } + + private PathStateRootMetadata metadataIfPresent(Path directory) throws IOException { + Path metadata = directory.resolve(PathStateCurrentStore.METADATA_FILE); + return Files.exists(metadata, LinkOption.NOFOLLOW_LINKS) + ? PathStateMetadataFile.load(metadata) : null; + } + + private PathStateRootMetadata asBase(PathStateRootMetadata layer) { + return PathStateRootMetadata.base(layer.getBlockNumber(), layer.getBlockHash(), + layer.getParentHash(), layer.getTimestamp(), layer.getPhase(), + layer.getFormatDigest(), layer.getStateRoot(), layer.getPayloadDigest()); + } + + private PathStateRootMetadata requireBase(PathStateRootMetadata metadata) throws IOException { + if (metadata.getKind() != Kind.BASE + || !Arrays.equals(metadata.getFormatDigest(), manifest.getIdentityDigest())) { + throw new IOException("path-state base flush metadata identity mismatch"); + } + return metadata; + } + + private void moveDirectory(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("path-state base flush requires atomic directory move", unsupported); + } + PathStateMetadataFile.syncDirectory(root); + } + + private void deleteDirectory(Path directory) throws IOException { + if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + return; + } + if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS) + || Files.isSymbolicLink(directory)) { + throw new IOException("path-state base flush refuses non-directory: " + directory); + } + List entries = new ArrayList<>(); + try (Stream paths = Files.walk(directory)) { + paths.forEach(entries::add); + } + for (Path entry : entries) { + if (Files.isSymbolicLink(entry)) { + throw new IOException("path-state base flush refuses symbolic links: " + entry); + } + } + entries.sort(Comparator.reverseOrder()); + for (Path entry : entries) { + Files.deleteIfExists(entry); + } + PathStateMetadataFile.syncDirectory(root); + } + + private static boolean same(PathStateRootMetadata left, PathStateRootMetadata right) { + return Arrays.equals(left.encode(), right.encode()); + } + + public enum RecoveryAction { + NONE, + COMPLETED_COMPACTION + } + + enum Stage { + AFTER_INTENT, + AFTER_NEXT, + AFTER_OLD_BASE, + AFTER_BASE, + AFTER_LAYER_RETIRE, + AFTER_PREVIOUS_RETIRE, + AFTER_RETIRE + } + + @FunctionalInterface + interface FaultHook { + void after(Stage stage) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java index a656bc788df..8b1161e5300 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java @@ -147,6 +147,26 @@ synchronized PathStateRootMetadata findAncestor(long blockNumber, byte[] blockHa throw new IOException("path-state canonical target exceeds the reversible window"); } + synchronized PathStateRootMetadata firstLayerAfterBaseToward( + PathStateRootMetadata target, PathStateLayerLimits limits) throws IOException { + PathStateRootMetadata admittedTarget = Objects.requireNonNull(target, "target"); + PathStateLayerLimits admittedLimits = Objects.requireNonNull(limits, "limits"); + PathStateRootMetadata base = requireKind(PathStateMetadataFile.load(basePath()), Kind.BASE); + requireFormat(base); + if (admittedTarget.getKind() != Kind.LAYER) { + throw new IOException("path-state base flush target is not a layer"); + } + PathStateRootMetadata cursor = admittedTarget; + for (int depth = 1; depth <= admittedLimits.getMaxLayers(); depth++) { + PathStateRootMetadata parent = parentOf(cursor, base); + if (same(parent, base)) { + return cursor; + } + cursor = parent; + } + throw new IOException("path-state base flush target exceeds the reversible window"); + } + /** Loads CURRENT and verifies that every referenced layer reaches the single durable base. */ public synchronized PathStateRootMetadata current() throws IOException { PathStateRootMetadata base = requireKind(PathStateMetadataFile.load(basePath()), Kind.BASE); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index 0e287e6ed4c..948aba11489 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -134,6 +134,12 @@ public static PathStateNodeStoreSet openBase(PathStateStoreManifest manifest) null); } + static PathStateNodeStoreSet beginBaseAt(PathStateStoreManifest manifest, Path directory) + throws IOException { + return new PathStateNodeStoreSet(Objects.requireNonNull(directory, "directory"), + Objects.requireNonNull(manifest, "manifest"), Kind.BASE, null, null); + } + public static PathStateNodeStoreSet openLayer(PathStateStoreManifest manifest, PathStateRootMetadata metadata) throws IOException { PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); @@ -299,6 +305,20 @@ synchronized List leafRecords() { return root.leafRecords(); } + synchronized PathStateRoot initializeBase(List leaves, + byte[] expectedRoot) { + requireOpen(); + if (kind != Kind.BASE || rootClaimed || progress != null || rebuildCheckpoint != null) { + throw new IllegalStateException("path-state replacement BASE is not empty"); + } + PathStateRoot candidate = new PathStateRoot(scope, + participant -> participantStores.get(participant.getDbName()), superStore); + candidate.initializeLeaves(Objects.requireNonNull(leaves, "leaves"), expectedRoot); + root = candidate; + rootClaimed = true; + return candidate; + } + /** Atomically persists all pending path nodes and their exact root progress. */ public synchronized void commit(PathStateRootMetadata metadata) throws IOException { requireOpen(); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java index 45946fba296..7c5f97683f1 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java @@ -9,11 +9,18 @@ public final class PathStateRuntimeAttachment { private final PathStateTransitionCollector collector; private final TransitionSink sink; + private final BaseFlushSink baseFlushSink; private Throwable failure; public PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink) { + this(collector, sink, (blockNumber, blockHash) -> { }); + } + + public PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink, + BaseFlushSink baseFlushSink) { this.collector = Objects.requireNonNull(collector, "collector"); this.sink = Objects.requireNonNull(sink, "sink"); + this.baseFlushSink = Objects.requireNonNull(baseFlushSink, "baseFlushSink"); } /** Capture failures fail only this shadow runtime and never reject the canonical block. */ @@ -42,6 +49,18 @@ public synchronized void publish(PathStateBlockTransition transition) { } } + /** Compacts only after Chainbase has durably refreshed the matching prefix. */ + public synchronized void flushBaseThrough(long blockNumber, byte[] blockHash) { + if (failure != null) { + return; + } + try { + baseFlushSink.accept(blockNumber, blockHash); + } catch (IOException | RuntimeException currentFailure) { + failure = currentFailure; + } + } + public synchronized boolean isFailed() { return failure != null; } @@ -62,4 +81,10 @@ public interface TransitionSink { void accept(PathStateBlockTransition transition) throws IOException; } + + @FunctionalInterface + public interface BaseFlushSink { + + void accept(long blockNumber, byte[] blockHash) throws IOException; + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java index f9b05421640..e8ff03659b4 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java @@ -26,6 +26,7 @@ public static PathStateSnapshotHead open(PathStateStoreManifest manifest, PathStateLayerLimits limits) throws IOException { PathStateStoreManifest admitted = Objects.requireNonNull(manifest, "manifest"); PathStateLayerLimits admittedLimits = Objects.requireNonNull(limits, "limits"); + new PathStateBaseCompaction(admitted, admittedLimits).recover(); PathStateRootMetadata current = new PathStateCurrentStore(admitted).current(); try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openPublished(admitted, current)) { PathStateRoot root = stores.createRoot(); @@ -71,6 +72,25 @@ public synchronized PathStateRootMetadata rewindTo(long blockNumber, byte[] bloc } } + /** Compacts the exact Chainbase-flushed prefix while retaining this newer reversible head. */ + public synchronized PathStateRootMetadata flushBaseThrough(long blockNumber, byte[] blockHash) + throws IOException { + requireHealthy(); + PathStateRootMetadata previous = head; + try { + PathStateRootMetadata base = new PathStateBaseCompaction(manifest, limits) + .compactThrough(blockNumber, blockHash); + if (!same(previous, new PathStateCurrentStore(manifest).current())) { + failed = true; + throw new IOException("path-state base flush changed the owned head"); + } + return base; + } catch (IOException | RuntimeException failure) { + failIfAuthorityMoved(previous, failure); + throw failure; + } + } + /** Computes an immutable candidate without opening or changing durable path-state storage. */ public synchronized PreparedPathStateTransition prepare(PathStateBlockTransition transition) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java index b7ccb0160ff..676308a0c31 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreManifest.java @@ -71,11 +71,25 @@ public static PathStateStoreManifest validateExisting(Path directory, Engine eng requireDirectory(root, "path-state root"); Path manifest = root.resolve(MANIFEST_FILE); validateExisting(manifest, encode(selected)); - requireDirectory(root.resolve(BASE_DIRECTORY), "path-state base"); + Path base = root.resolve(BASE_DIRECTORY); + if (!Files.isDirectory(base, LinkOption.NOFOLLOW_LINKS)) { + requireBaseReplacementRecoveryLayout(root); + } requireDirectory(root.resolve(LAYERS_DIRECTORY), "path-state layers"); return new PathStateStoreManifest(root, selected); } + private static void requireBaseReplacementRecoveryLayout(Path root) throws IOException { + Path intent = root.resolve(PathStateBaseCompaction.INTENT_FILE); + if (!Files.isRegularFile(intent, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state base is missing outside a durable replacement"); + } + requireDirectory(root.resolve(PathStateBaseCompaction.NEXT_DIRECTORY), + "path-state next base"); + requireDirectory(root.resolve(PathStateBaseCompaction.PREVIOUS_DIRECTORY), + "path-state previous base"); + } + public Path getDirectory() { return directory; } diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 0603590a293..e16adcb33e7 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -130,6 +130,7 @@ import org.tron.core.db2.archive.StateArchiveRuntimeOwner; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.stateroot.PathStateBlockTransition; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateLayerLimits; import org.tron.core.db2.stateroot.PathStateNativeSnapshotSource; @@ -794,17 +795,29 @@ private void attachPathStateBlockFinalRuntime() { } PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment( new SnapshotPathStateTransitionCollector(accountAssetStore::prefixQuery), - transition -> { - PathStateSnapshotHead owner = pathStateSnapshotHead; - if (owner == null) { - throw new java.io.IOException("Path-state snapshot owner is unavailable"); - } - owner.advance(transition); - }); + this::advancePathStateRoot, this::flushPathStateBaseThrough); ((SnapshotManager) revokingStore).attachPathStateRuntime(attachment); pathStateRuntime = attachment; } + private void advancePathStateRoot(PathStateBlockTransition transition) + throws java.io.IOException { + PathStateSnapshotHead owner = pathStateSnapshotHead; + if (owner == null) { + throw new java.io.IOException("Path-state snapshot owner is unavailable"); + } + owner.advance(transition); + } + + private void flushPathStateBaseThrough(long blockNumber, byte[] blockHash) + throws java.io.IOException { + PathStateSnapshotHead owner = pathStateSnapshotHead; + if (owner == null) { + throw new java.io.IOException("Path-state snapshot owner is unavailable"); + } + owner.flushBaseThrough(blockNumber, blockHash); + } + private void rebuildPathStateRoot(SnapshotManager snapshotManager, PathStateStoreManifest manifest) throws java.io.IOException { java.util.Map checkpointDb = mock(DbSourceInter.class); + when(checkpointDb.iterator()).thenReturn(Collections.emptyIterator()); + when(checkpoint.getDbSource()).thenReturn(checkpointDb); + manager.setCheckTmpStore(checkpoint); + AtomicReference flushedNumber = new AtomicReference<>(); + AtomicReference flushedHash = new AtomicReference<>(); + PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment( + view -> mock(PathStateBlockTransition.class), transition -> { }, + (blockNumber, blockHash) -> { + flushedNumber.set(blockNumber); + flushedHash.set(blockHash); + }); + manager.attachPathStateRuntime(attachment); + + BlockSnapshotMeta first = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L); + BlockSnapshotMeta second = BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L); + commitBlock(manager, code, first, "first"); + commitBlock(manager, code, second, "second"); + setFlushCount(manager, 1); + + manager.flush(); + + assertEquals(Long.valueOf(1L), flushedNumber.get()); + assertArrayEquals(first.getBlockHash(), flushedHash.get()); + assertFalse(attachment.isFailed()); + manager.detachPathStateRuntime(attachment); + manager.shutdown(); + } + @Test public void borrowedRuntimeAttachmentIsAtomicAndIdentityBound() throws Exception { SnapshotManager manager = new SnapshotManager(""); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBaseCompactionTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBaseCompactionTest.java new file mode 100644 index 00000000000..160f7fe128b --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBaseCompactionTest.java @@ -0,0 +1,188 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.arch.Arch; +import org.tron.core.db2.stateroot.PathStateBaseCompaction.RecoveryAction; +import org.tron.core.db2.stateroot.PathStateBaseCompaction.Stage; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateBaseCompactionTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void materializesPrefixOneLayerAtATimeAcrossEngines() throws Exception { + for (Engine engine : availableEngines()) { + Fixture fixture = fixture("compact-" + engine, engine); + PathStateBaseCompaction compaction = new PathStateBaseCompaction( + fixture.manifest, fixture.limits); + + PathStateRootMetadata compacted = compaction.compactThrough( + fixture.second.getBlockNumber(), fixture.second.getBlockHash()); + + assertEquals(Kind.BASE, compacted.getKind()); + assertEquals(102, compacted.getBlockNumber()); + assertArrayEquals(fixture.second.getStateRoot(), compacted.getStateRoot()); + assertFalse(Files.exists(fixture.manifest.getLayerDirectory( + fixture.first.getBlockNumber(), fixture.first.getBlockHash()))); + assertFalse(Files.exists(fixture.manifest.getLayerDirectory( + fixture.second.getBlockNumber(), fixture.second.getBlockHash()))); + assertArrayEquals(fixture.head.encode(), + new PathStateCurrentStore(fixture.manifest).current().encode()); + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openCurrent(fixture.manifest)) { + PathStateRoot restored = stores.createRoot(); + assertArrayEquals(fixture.head.getStateRoot(), restored.rootHash()); + restored.verifyNodeStores(); + } + assertEquals(RecoveryAction.NONE, compaction.recover()); + } + } + + @Test + public void everyDurableStageRecoversToTheSameBase() throws Exception { + for (Engine engine : availableEngines()) { + for (Stage failure : Stage.values()) { + Fixture fixture = fixture("recover-" + engine + "-" + failure, engine); + PathStateBaseCompaction interrupted = new PathStateBaseCompaction( + fixture.manifest, fixture.limits, failAfter(failure)); + assertThrows(IOException.class, () -> interrupted.compactThrough( + fixture.first.getBlockNumber(), fixture.first.getBlockHash())); + + PathStateBaseCompaction recovery = new PathStateBaseCompaction( + fixture.manifest, fixture.limits); + RecoveryAction expected = failure == Stage.AFTER_RETIRE + ? RecoveryAction.NONE : RecoveryAction.COMPLETED_COMPACTION; + assertEquals(expected, recovery.recover()); + assertEquals(RecoveryAction.NONE, recovery.recover()); + PathStateRootMetadata base = PathStateMetadataFile.load(fixture.manifest + .getBaseDirectory().resolve(PathStateCurrentStore.METADATA_FILE)); + assertEquals(Kind.BASE, base.getKind()); + assertArrayEquals(fixture.first.getStateRoot(), base.getStateRoot()); + assertArrayEquals(fixture.head.encode(), + new PathStateCurrentStore(fixture.manifest).current().encode()); + assertFalse(Files.exists(fixture.manifest.getDirectory() + .resolve(PathStateBaseCompaction.INTENT_FILE))); + assertFalse(Files.exists(fixture.manifest.getDirectory() + .resolve(PathStateBaseCompaction.NEXT_DIRECTORY))); + assertFalse(Files.exists(fixture.manifest.getDirectory() + .resolve(PathStateBaseCompaction.PREVIOUS_DIRECTORY))); + } + } + } + + @Test + public void refusesToCompactTheReversibleHead() throws Exception { + Fixture fixture = fixture("retain-head", Engine.ROCKSDB); + PathStateBaseCompaction compaction = new PathStateBaseCompaction( + fixture.manifest, fixture.limits); + + assertThrows(IOException.class, () -> compaction.compactThrough( + fixture.head.getBlockNumber(), fixture.head.getBlockHash())); + assertArrayEquals(fixture.base.encode(), PathStateMetadataFile.load(fixture.manifest + .getBaseDirectory().resolve(PathStateCurrentStore.METADATA_FILE)).encode()); + } + + @Test + public void snapshotStartupRecoversTheDirectorySwapGap() throws Exception { + Fixture fixture = fixture("startup-recovery", Engine.ROCKSDB); + PathStateBaseCompaction interrupted = new PathStateBaseCompaction( + fixture.manifest, fixture.limits, failAfter(Stage.AFTER_OLD_BASE)); + assertThrows(IOException.class, () -> interrupted.compactThrough( + fixture.first.getBlockNumber(), fixture.first.getBlockHash())); + + PathStateStoreManifest validated = PathStateStoreManifest.validateExisting( + fixture.manifest.getDirectory(), Engine.ROCKSDB); + PathStateSnapshotHead owner = PathStateSnapshotHead.open(validated, fixture.limits); + + assertArrayEquals(fixture.head.encode(), owner.getHead().encode()); + assertEquals(Kind.BASE, PathStateMetadataFile.load(validated.getBaseDirectory() + .resolve(PathStateCurrentStore.METADATA_FILE)).getKind()); + } + + private Fixture fixture(String name, Engine engine) throws Exception { + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen( + new File(temporaryFolder.getRoot(), name).toPath(), engine); + PathStateRootMetadata base; + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + root.apply(Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}), + PathStateMutation.put("account", new byte[]{3}, new byte[]{4}))); + base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, + P66Phase.P66_ON, manifest.getIdentityDigest(), root.rootHash(), bytes(3)); + new PathStateBasePublication(manifest).publish(stores, base); + } + PathStateLayerLimits limits = new PathStateLayerLimits(10, Long.MAX_VALUE); + PathStateRootMetadata first = append(manifest, base, 101, 11, limits); + PathStateRootMetadata second = append(manifest, first, 102, 12, limits); + PathStateRootMetadata head = append(manifest, second, 103, 13, limits); + return new Fixture(manifest, limits, base, first, second, head); + } + + private static PathStateRootMetadata append(PathStateStoreManifest manifest, + PathStateRootMetadata parent, long blockNumber, int seed, PathStateLayerLimits limits) + throws Exception { + try (PathStateLayer layer = PathStateLayer.begin(manifest, parent, blockNumber, bytes(seed), + parent.getBlockHash(), blockNumber * 3, P66Phase.P66_ON, bytes(seed + 1), limits)) { + layer.apply(Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{(byte) seed}))); + return layer.commit(); + } + } + + private static Engine[] availableEngines() { + return Arch.isArm64() ? new Engine[]{Engine.ROCKSDB} + : new Engine[]{Engine.LEVELDB, Engine.ROCKSDB}; + } + + private static PathStateBaseCompaction.FaultHook failAfter(Stage failure) { + return stage -> { + if (stage == failure) { + throw new IOException("injected after " + stage); + } + }; + } + + private static byte[] bytes(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + private static final class Fixture { + private final PathStateStoreManifest manifest; + private final PathStateLayerLimits limits; + private final PathStateRootMetadata base; + private final PathStateRootMetadata first; + private final PathStateRootMetadata second; + private final PathStateRootMetadata head; + + private Fixture(PathStateStoreManifest manifest, PathStateLayerLimits limits, + PathStateRootMetadata base, PathStateRootMetadata first, + PathStateRootMetadata second, PathStateRootMetadata head) { + this.manifest = manifest; + this.limits = limits; + this.base = base; + this.first = first; + this.second = second; + this.head = head; + } + } +} From c99dbdc5dcae4189911cbc1faf7236f7f34b8560 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 16:25:10 +0800 Subject: [PATCH 085/161] feat(db): migrate path state on P66 activation --- .../SnapshotPathStateTransitionCollector.java | 55 ++++++++++- .../main/java/org/tron/core/db/Manager.java | 23 ++++- .../SnapshotOldValueCollectorTest.java | 95 +++++++++++++++++++ 3 files changed, 171 insertions(+), 2 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java index 8b468142cab..532d4b18657 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java @@ -23,12 +23,23 @@ public final class SnapshotPathStateTransitionCollector private final PathStateCanonicalizer canonicalizer = new PathStateCanonicalizer(); private final AccountAssetArchiveProjector accountAssetProjector; private final AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource; + private final ActivationAccountSource activationAccountSource; public SnapshotPathStateTransitionCollector( AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource) { + this(oldPhysicalAssetsSource, consumer -> { + throw new IOException("path-state P66 activation Account source is unavailable"); + }); + } + + public SnapshotPathStateTransitionCollector( + AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource, + ActivationAccountSource activationAccountSource) { this.accountAssetProjector = new AccountAssetArchiveProjector(); this.oldPhysicalAssetsSource = Objects.requireNonNull(oldPhysicalAssetsSource, "oldPhysicalAssetsSource"); + this.activationAccountSource = Objects.requireNonNull(activationAccountSource, + "activationAccountSource"); } @Override @@ -36,9 +47,17 @@ public PathStateBlockTransition collect(BlockChangeView view) throws IOException BlockChangeView admitted = Objects.requireNonNull(view, "view"); P66Phase phase = resolvePhase(admitted); LinkedHashMap mutations = new LinkedHashMap<>(); + if (phase == P66Phase.P66_ACTIVATION) { + collectActivationAccounts(mutations); + } for (BlockChangeView.DatabaseChanges database : admitted.getDatabases()) { String dbName = database.getDbName(); PathStateParticipantDescriptor.current().require(dbName); + if (phase == P66Phase.P66_ACTIVATION + && (AccountAssetArchiveProjector.ACCOUNT_DB.equals(dbName) + || AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(dbName))) { + continue; + } for (BlockChangeView.Change change : database.getChanges()) { if (AccountAssetArchiveProjector.ACCOUNT_DB.equals(dbName)) { collectAccount(phase, database, change, mutations); @@ -53,6 +72,27 @@ public PathStateBlockTransition collect(BlockChangeView view) throws IOException meta.getParentHash(), meta.getTimestamp(), phase, mutations.values()); } + private void collectActivationAccounts( + Map mutations) throws IOException { + activationAccountSource.scan((key, rawPost) -> { + BlockChangeView.PostValue postValue = BlockChangeView.PostValue.present(rawPost); + Map oldAssets = Collections.emptyMap(); + if (accountAssetProjector.requiresOldPhysicalAssets(null, postValue)) { + oldAssets = AccountAssetOldPhysicalAssetsSource.captureRequired( + oldPhysicalAssetsSource, key); + } + AccountAssetArchiveProjector.Projection projection = accountAssetProjector.project( + key, null, postValue, true, oldAssets); + addCanonical(AccountAssetArchiveProjector.ACCOUNT_DB, key, projection.oldAccount, + projection.postAccount, P66Phase.P66_ACTIVATION, mutations); + for (AssetRow asset : projection.changedAssetRows) { + addPhysical(P66Phase.P66_ACTIVATION, + AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + asset.getPhysicalRawKey(), null, asset.getPostValue(), mutations); + } + }); + } + private void collectAccount(P66Phase phase, BlockChangeView.DatabaseChanges database, BlockChangeView.Change change, Map mutations) { byte[] key = change.getKey(); @@ -127,7 +167,7 @@ private P66Phase resolvePhase(BlockChangeView view) throws IOException { throw new IOException("path-state P66 phase cannot move backwards"); } if (previous == 0L && target == 1L) { - throw new IOException("path-state P66 activation requires an explicit rebuild"); + return P66Phase.P66_ACTIVATION; } return target == 0L ? P66Phase.P66_OFF : P66Phase.P66_ON; } @@ -152,6 +192,19 @@ private static boolean same(PathStateMutation left, PathStateMutation right) { && Arrays.equals(left.getCanonicalValue(), right.getCanonicalValue()); } + /** Scans the canonical post-state Account domain only for the one-time P66 transition. */ + @FunctionalInterface + public interface ActivationAccountSource { + + void scan(ActivationAccountConsumer consumer) throws IOException; + } + + @FunctionalInterface + public interface ActivationAccountConsumer { + + void accept(byte[] key, byte[] value) throws IOException; + } + private static final class MutationKey { private final String dbName; diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index e16adcb33e7..ad1b9f4f74e 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -794,12 +794,33 @@ private void attachPathStateBlockFinalRuntime() { "Path-state block-final capture requires account-asset Store"); } PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment( - new SnapshotPathStateTransitionCollector(accountAssetStore::prefixQuery), + new SnapshotPathStateTransitionCollector(accountAssetStore::prefixQuery, + this::scanPathStateActivationAccounts), this::advancePathStateRoot, this::flushPathStateBaseThrough); ((SnapshotManager) revokingStore).attachPathStateRuntime(attachment); pathStateRuntime = attachment; } + private void scanPathStateActivationAccounts( + SnapshotPathStateTransitionCollector.ActivationAccountConsumer consumer) + throws java.io.IOException { + SnapshotManager snapshotManager = (SnapshotManager) revokingStore; + org.tron.core.db2.core.Chainbase account = snapshotManager.getDbs().stream() + .filter(database -> AccountAssetArchiveProjector.ACCOUNT_DB.equals(database.getDbName())) + .findFirst() + .orElseThrow(() -> new java.io.IOException( + "Path-state P66 activation requires the Account Store")); + java.util.Iterator> iterator = account.iterator(); + while (iterator.hasNext()) { + java.util.Map.Entry entry = iterator.next(); + if (entry.getKey() == null || entry.getValue() == null) { + throw new java.io.IOException("Path-state P66 activation Account scan contains null"); + } + consumer.accept(Arrays.copyOf(entry.getKey(), entry.getKey().length), + Arrays.copyOf(entry.getValue(), entry.getValue().length)); + } + } + private void advancePathStateRoot(PathStateBlockTransition transition) throws java.io.IOException { PathStateSnapshotHead owner = pathStateSnapshotHead; diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index 20e92f34318..5a6a47f2dba 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -48,6 +48,8 @@ import org.tron.core.db2.core.SnapshotManager; import org.tron.core.db2.core.SnapshotRoot; import org.tron.core.db2.stateroot.PathStateBlockTransition; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateMutation; import org.tron.core.db2.stateroot.PathStateRuntimeAttachment; import org.tron.core.exception.TronError; import org.tron.core.store.AccountAssetStore; @@ -89,6 +91,84 @@ public void pathStateRuntimeCoexistsWithArchiveAtBlockFinalBoundary() throws Exc manager.shutdown(); } + @Test + public void pathStateP66ActivationScansPostStateThenResumesIncrementalCapture() + throws Exception { + byte[] firstAddress = archiveAddress(21); + byte[] secondAddress = archiveAddress(22); + Account firstBefore = Account.newBuilder() + .setAddress(ByteString.copyFrom(firstAddress)) + .putAssetV2("1000021", 21L) + .build(); + Account firstPost = firstBefore.toBuilder().putAssetV2("1000021", 210L).build(); + Account second = Account.newBuilder() + .setAddress(ByteString.copyFrom(secondAddress)) + .putAssetV2("1000022", 22L) + .build(); + MemoryDb accountDb = new MemoryDb("account"); + accountDb.put(firstAddress, firstBefore.toByteArray()); + accountDb.put(secondAddress, second.toByteArray()); + MemoryDb propertiesDb = new MemoryDb("properties"); + propertiesDb.put(HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), + Longs.toByteArray(0L)); + SnapshotManager manager = new SnapshotManager(""); + Chainbase account = new Chainbase(new SnapshotRoot(accountDb)); + Chainbase properties = new Chainbase(new SnapshotRoot(propertiesDb)); + Chainbase code = new Chainbase(new SnapshotRoot(new MemoryDb("code"))); + manager.add(account); + manager.add(properties); + manager.add(code); + manager.enable(); + List published = new ArrayList<>(); + SnapshotPathStateTransitionCollector collector = new SnapshotPathStateTransitionCollector( + ignored -> Collections.emptyMap(), consumer -> { + Iterator> entries = account.iterator(); + while (entries.hasNext()) { + Map.Entry entry = entries.next(); + consumer.accept(entry.getKey(), entry.getValue()); + } + }); + PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment(collector, + published::add); + manager.attachPathStateRuntime(attachment); + + try (ISession block = manager.buildSession()) { + properties.put(HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), + Longs.toByteArray(1L)); + account.put(firstAddress, firstPost.toByteArray()); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + } + + assertFalse(attachment.isFailed()); + assertEquals(1, published.size()); + PathStateBlockTransition activation = published.get(0); + assertEquals(P66Phase.P66_ACTIVATION, activation.getPhase()); + assertEquals(5, activation.getMutations().size()); + assertEquals(2, mutationCount(activation, "account")); + assertEquals(2, mutationCount(activation, "account-asset")); + PathStateMutation firstAccount = mutation(activation, "account", firstAddress); + Account canonicalFirst = Account.parseFrom(firstAccount.getCanonicalValue()); + assertTrue(canonicalFirst.getAssetOptimized()); + assertTrue(canonicalFirst.getAssetV2Map().isEmpty()); + PathStateMutation firstAsset = mutation(activation, "account-asset", + Bytes.concat(firstAddress, bytes("1000021"))); + assertArrayEquals(Longs.toByteArray(210L), firstAsset.getCanonicalValue()); + + byte[] codeKey = bytes("after-activation"); + try (ISession block = manager.buildSession()) { + code.put(codeKey, bytes("incremental")); + block.commit(BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L)); + } + + assertFalse(attachment.isFailed()); + assertEquals(2, published.size()); + assertEquals(P66Phase.P66_ON, published.get(1).getPhase()); + assertEquals(1, published.get(1).getMutations().size()); + assertEquals("code", published.get(1).getMutations().get(0).getDbName()); + manager.detachPathStateRuntime(attachment); + manager.shutdown(); + } + @Test public void pathStateFailureDoesNotRejectArchiveDisabledBlockCommit() throws Exception { SnapshotManager manager = new SnapshotManager(""); @@ -885,6 +965,21 @@ private static boolean contains(DbGroup group, byte[] key) { return group.getEntries().stream().anyMatch(entry -> Arrays.equals(entry.getKey(), key)); } + private static int mutationCount(PathStateBlockTransition transition, String dbName) { + return (int) transition.getMutations().stream() + .filter(mutation -> dbName.equals(mutation.getDbName())) + .count(); + } + + private static PathStateMutation mutation(PathStateBlockTransition transition, + String dbName, byte[] key) { + return transition.getMutations().stream() + .filter(candidate -> dbName.equals(candidate.getDbName()) + && Arrays.equals(key, candidate.getCanonicalKey())) + .findFirst() + .orElseThrow(AssertionError::new); + } + private static byte[] bytes(String value) { return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); } From f9e0c6220752172524488b357342f72dff49243d Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 18:00:52 +0800 Subject: [PATCH 086/161] feat(db): expose path state runtime status --- .../stateroot/PathStateRuntimeAttachment.java | 229 +++++++++++++++++- .../main/java/org/tron/core/db/Manager.java | 15 +- .../SnapshotOldValueCollectorTest.java | 87 ++++++- ...athStateManagerStartupIntegrationTest.java | 10 + 4 files changed, 332 insertions(+), 9 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java index 7c5f97683f1..294a79eb603 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java @@ -1,16 +1,26 @@ package org.tron.core.db2.stateroot; import java.io.IOException; +import java.util.Arrays; +import java.util.Locale; import java.util.Objects; +import lombok.extern.slf4j.Slf4j; import org.tron.core.db2.archive.BlockChangeView; /** Independent non-consensus runtime installed at the metadata-aware block commit boundary. */ +@Slf4j(topic = "DB") public final class PathStateRuntimeAttachment { private final PathStateTransitionCollector collector; private final TransitionSink sink; private final BaseFlushSink baseFlushSink; private Throwable failure; + private FailureStage failureStage; + private long readyBlockNumber = -1; + private byte[] readyBlockHash; + private long observedBlockNumber = -1; + private byte[] observedBlockHash; + private PathStateBlockTransition pending; public PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink) { this(collector, sink, (blockNumber, blockHash) -> { }); @@ -25,14 +35,24 @@ public PathStateRuntimeAttachment(PathStateTransitionCollector collector, Transi /** Capture failures fail only this shadow runtime and never reject the canonical block. */ public synchronized PathStateBlockTransition capture(BlockChangeView view) { + BlockChangeView admitted = Objects.requireNonNull(view, "view"); + observe(admitted); if (failure != null) { return null; } try { - return Objects.requireNonNull(collector.collect(view), + PathStateBlockTransition transition = Objects.requireNonNull(collector.collect(admitted), "path-state collector returned null"); + if (transition.getBlockNumber() != admitted.getMeta().getBlockNumber() + || !Arrays.equals(transition.getBlockHash(), admitted.getMeta().getBlockHash()) + || !Arrays.equals(transition.getParentHash(), admitted.getMeta().getParentHash()) + || transition.getTimestamp() != admitted.getMeta().getTimestamp()) { + throw new IOException("path-state collector changed the captured block identity"); + } + pending = transition; + return transition; } catch (IOException | RuntimeException currentFailure) { - failure = currentFailure; + fail(FailureStage.CAPTURE, currentFailure); return null; } } @@ -43,9 +63,15 @@ public synchronized void publish(PathStateBlockTransition transition) { return; } try { + if (pending != transition) { + throw new IOException("path-state publication differs from captured transition"); + } sink.accept(transition); + readyBlockNumber = transition.getBlockNumber(); + readyBlockHash = transition.getBlockHash(); + pending = null; } catch (IOException | RuntimeException currentFailure) { - failure = currentFailure; + fail(FailureStage.PUBLISH, currentFailure); } } @@ -57,7 +83,7 @@ public synchronized void flushBaseThrough(long blockNumber, byte[] blockHash) { try { baseFlushSink.accept(blockNumber, blockHash); } catch (IOException | RuntimeException currentFailure) { - failure = currentFailure; + fail(FailureStage.BASE_FLUSH, currentFailure); } } @@ -69,10 +95,205 @@ public synchronized Throwable getFailure() { return failure; } + /** Returns a copy-only diagnostic snapshot; it never repairs or guesses a root. */ + public synchronized Status status() { + State state = failure != null ? State.FAILED + : pending == null && readyBlockNumber >= 0 + && readyBlockNumber == observedBlockNumber + && Arrays.equals(readyBlockHash, observedBlockHash) ? State.READY : State.NOT_READY; + return new Status(state, readyBlockNumber, readyBlockHash, observedBlockNumber, + observedBlockHash, failureStage, classify(failure), failure); + } + + /** Seeds or rewinds the exact verified durable head; failed runtimes cannot be reset. */ + public synchronized void synchronizeReadyHead(PathStateRootMetadata metadata) { + if (failure != null) { + throw new IllegalStateException("failed path-state runtime cannot become ready"); + } + if (pending != null) { + throw new IllegalStateException("captured path-state transition is not published"); + } + PathStateRootMetadata admitted = Objects.requireNonNull(metadata, "metadata"); + readyBlockNumber = admitted.getBlockNumber(); + readyBlockHash = admitted.getBlockHash(); + observedBlockNumber = readyBlockNumber; + observedBlockHash = copy(readyBlockHash); + } + /** Marks an externally coordinated lifecycle operation as failed without replacing first cause. */ public synchronized void fail(Throwable currentFailure) { + fail(FailureStage.EXTERNAL, currentFailure); + } + + public synchronized void fail(FailureStage stage, Throwable currentFailure) { if (failure == null) { failure = Objects.requireNonNull(currentFailure, "currentFailure"); + failureStage = Objects.requireNonNull(stage, "stage"); + logger.error( + "Path-state runtime fail-stop: stage={}, kind={}, readyBlock={}, observedBlock={}, " + + "rootLag={}", + failureStage, classify(failure), readyBlockNumber, observedBlockNumber, + lag(readyBlockNumber, observedBlockNumber), + failure); + } + } + + /** Records the exact canonical target of a failed external lifecycle operation. */ + public synchronized void failAt(FailureStage stage, long blockNumber, byte[] blockHash, + Throwable currentFailure) { + observedBlockNumber = blockNumber; + observedBlockHash = copy(Objects.requireNonNull(blockHash, "blockHash")); + fail(stage, currentFailure); + } + + private void observe(BlockChangeView view) { + long number = view.getMeta().getBlockNumber(); + byte[] hash = view.getMeta().getBlockHash(); + byte[] parentHash = view.getMeta().getParentHash(); + if (failure == null) { + if (pending != null) { + fail(FailureStage.CAPTURE_GAP, + new IOException("path-state previous capture is not published")); + } + long expectedParentNumber = observedBlockNumber >= 0 + ? observedBlockNumber : readyBlockNumber; + byte[] expectedParentHash = observedBlockHash != null + ? observedBlockHash : readyBlockHash; + if (expectedParentNumber >= 0 && (number != expectedParentNumber + 1 + || !Arrays.equals(parentHash, expectedParentHash))) { + fail(FailureStage.CAPTURE_GAP, + new IOException("path-state block-final capture is not continuous")); + } + } + observedBlockNumber = number; + observedBlockHash = copy(hash); + } + + private static FailureKind classify(Throwable failure) { + if (failure == null) { + return FailureKind.NONE; + } + StringBuilder messages = new StringBuilder(); + Throwable current = failure; + boolean io = false; + while (current != null) { + io |= current instanceof IOException; + if (current.getMessage() != null) { + messages.append(' ').append(current.getMessage().toLowerCase(Locale.ROOT)); + } + current = current.getCause(); + } + String text = messages.toString(); + if (text.contains("no space left") || text.contains("disk full") + || text.contains("out of space")) { + return FailureKind.STORAGE_FULL; + } + if (text.contains("corrupt") || text.contains("checksum") || text.contains("mismatch") + || text.contains("orphan") || text.contains("native progress")) { + return FailureKind.CORRUPTION; + } + return io ? FailureKind.IO : FailureKind.RUNTIME; + } + + private static byte[] copy(byte[] value) { + return value == null ? null : Arrays.copyOf(value, value.length); + } + + private static long lag(long readyBlockNumber, long observedBlockNumber) { + if (readyBlockNumber < 0 || observedBlockNumber < 0) { + return -1; + } + return observedBlockNumber >= readyBlockNumber + ? observedBlockNumber - readyBlockNumber : readyBlockNumber - observedBlockNumber; + } + + public enum State { + NOT_READY, + READY, + FAILED + } + + public enum FailureStage { + CAPTURE_GAP, + CAPTURE, + PUBLISH, + BASE_FLUSH, + REORG, + EXTERNAL + } + + public enum FailureKind { + NONE, + STORAGE_FULL, + CORRUPTION, + IO, + RUNTIME + } + + public static final class Status { + + private final State state; + private final long readyBlockNumber; + private final byte[] readyBlockHash; + private final long observedBlockNumber; + private final byte[] observedBlockHash; + private final FailureStage failureStage; + private final FailureKind failureKind; + private final String failureType; + private final String failureMessage; + + private Status(State state, long readyBlockNumber, byte[] readyBlockHash, + long observedBlockNumber, byte[] observedBlockHash, FailureStage failureStage, + FailureKind failureKind, Throwable failure) { + this.state = state; + this.readyBlockNumber = readyBlockNumber; + this.readyBlockHash = copy(readyBlockHash); + this.observedBlockNumber = observedBlockNumber; + this.observedBlockHash = copy(observedBlockHash); + this.failureStage = failureStage; + this.failureKind = failureKind; + this.failureType = failure == null ? null : failure.getClass().getName(); + this.failureMessage = failure == null ? null : failure.getMessage(); + } + + public State getState() { + return state; + } + + public long getReadyBlockNumber() { + return readyBlockNumber; + } + + public byte[] getReadyBlockHash() { + return copy(readyBlockHash); + } + + public long getObservedBlockNumber() { + return observedBlockNumber; + } + + public byte[] getObservedBlockHash() { + return copy(observedBlockHash); + } + + public long getRootLag() { + return lag(readyBlockNumber, observedBlockNumber); + } + + public FailureStage getFailureStage() { + return failureStage; + } + + public FailureKind getFailureKind() { + return failureKind; + } + + public String getFailureType() { + return failureType; + } + + public String getFailureMessage() { + return failureMessage; } } diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index ad1b9f4f74e..d5f11f0a3f8 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -784,7 +784,7 @@ private void initPathStateRoot() { } } - private void attachPathStateBlockFinalRuntime() { + private void attachPathStateBlockFinalRuntime() throws java.io.IOException { if (!(revokingStore instanceof SnapshotManager)) { throw new IllegalStateException("Path-state block-final capture requires SnapshotManager"); } @@ -797,6 +797,7 @@ private void attachPathStateBlockFinalRuntime() { new SnapshotPathStateTransitionCollector(accountAssetStore::prefixQuery, this::scanPathStateActivationAccounts), this::advancePathStateRoot, this::flushPathStateBaseThrough); + attachment.synchronizeReadyHead(pathStateSnapshotHead.getHead()); ((SnapshotManager) revokingStore).attachPathStateRuntime(attachment); pathStateRuntime = attachment; } @@ -1432,13 +1433,19 @@ private void rewindPathStateRootAfterPop() { if (owner == null) { return; } + long canonicalNumber = getDynamicPropertiesStore().getLatestBlockHeaderNumber(); + byte[] canonicalHash = getDynamicPropertiesStore().getLatestBlockHeaderHash().getBytes(); try { - owner.rewindTo(getDynamicPropertiesStore().getLatestBlockHeaderNumber(), - getDynamicPropertiesStore().getLatestBlockHeaderHash().getBytes()); + PathStateRootMetadata rewound = owner.rewindTo(canonicalNumber, canonicalHash); + PathStateRuntimeAttachment runtime = pathStateRuntime; + if (runtime != null) { + runtime.synchronizeReadyHead(rewound); + } } catch (java.io.IOException | RuntimeException failure) { PathStateRuntimeAttachment runtime = pathStateRuntime; if (runtime != null) { - runtime.fail(failure); + runtime.failAt(PathStateRuntimeAttachment.FailureStage.REORG, + canonicalNumber, canonicalHash, failure); } logger.error("Path-state short-reorg rewind failed after canonical block pop", failure); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index 5a6a47f2dba..0602440e3f1 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -195,6 +195,75 @@ public void pathStateFailureDoesNotRejectArchiveDisabledBlockCommit() throws Exc manager.shutdown(); } + @Test + public void pathStateStatusExposesLagStorageFailureAndCaptureGap() throws Exception { + SnapshotManager manager = new SnapshotManager(""); + Chainbase code = new Chainbase(new SnapshotRoot(new MemoryDb("code"))); + manager.add(code); + manager.enable(); + AtomicInteger publications = new AtomicInteger(); + PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment(view -> { + BlockSnapshotMeta meta = view.getMeta(); + return new PathStateBlockTransition(meta.getBlockNumber(), meta.getBlockHash(), + meta.getParentHash(), meta.getTimestamp(), P66Phase.P66_ON, + Collections.emptyList()); + }, transition -> { + if (publications.incrementAndGet() == 2) { + throw new IOException("No space left on device"); + } + }); + + PathStateBlockTransition first = attachment.capture( + captureView(manager, code, BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L))); + assertEquals(PathStateRuntimeAttachment.State.NOT_READY, attachment.status().getState()); + attachment.publish(first); + assertEquals(PathStateRuntimeAttachment.State.READY, attachment.status().getState()); + assertEquals(0L, attachment.status().getRootLag()); + + PathStateBlockTransition second = attachment.capture( + captureView(manager, code, BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 2L))); + assertEquals(PathStateRuntimeAttachment.State.NOT_READY, attachment.status().getState()); + assertEquals(1L, attachment.status().getRootLag()); + attachment.publish(second); + assertEquals(PathStateRuntimeAttachment.State.FAILED, attachment.status().getState()); + assertEquals(PathStateRuntimeAttachment.FailureStage.PUBLISH, + attachment.status().getFailureStage()); + assertEquals(PathStateRuntimeAttachment.FailureKind.STORAGE_FULL, + attachment.status().getFailureKind()); + attachment.capture( + captureView(manager, code, BlockSnapshotMeta.forBlock(3, hash(3), hash(2), 3L))); + assertEquals(2L, attachment.status().getRootLag()); + + PathStateRuntimeAttachment gap = new PathStateRuntimeAttachment(view -> { + BlockSnapshotMeta meta = view.getMeta(); + return new PathStateBlockTransition(meta.getBlockNumber(), meta.getBlockHash(), + meta.getParentHash(), meta.getTimestamp(), P66Phase.P66_ON, + Collections.emptyList()); + }, transition -> { }); + PathStateBlockTransition admitted = gap.capture( + captureView(manager, code, BlockSnapshotMeta.forBlock(4, hash(4), hash(3), 4L))); + gap.publish(admitted); + assertEquals(PathStateRuntimeAttachment.State.READY, gap.status().getState()); + assertEquals(null, gap.capture( + captureView(manager, code, BlockSnapshotMeta.forBlock(6, hash(6), hash(5), 6L)))); + assertEquals(PathStateRuntimeAttachment.FailureStage.CAPTURE_GAP, + gap.status().getFailureStage()); + assertEquals(2L, gap.status().getRootLag()); + + PathStateRuntimeAttachment corrupt = new PathStateRuntimeAttachment(view -> { + throw new AssertionError("capture is not used"); + }, transition -> { }, (blockNumber, blockHash) -> { + throw new IOException("native progress checksum mismatch"); + }); + corrupt.flushBaseThrough(4L, hash(4)); + assertEquals(PathStateRuntimeAttachment.State.FAILED, corrupt.status().getState()); + assertEquals(PathStateRuntimeAttachment.FailureStage.BASE_FLUSH, + corrupt.status().getFailureStage()); + assertEquals(PathStateRuntimeAttachment.FailureKind.CORRUPTION, + corrupt.status().getFailureKind()); + manager.shutdown(); + } + @Test public void pathStateCompactsOnlyAfterChainbaseRefreshesThePrefix() throws Exception { SnapshotManager manager = new SnapshotManager(""); @@ -210,7 +279,12 @@ public void pathStateCompactsOnlyAfterChainbaseRefreshesThePrefix() throws Excep AtomicReference flushedNumber = new AtomicReference<>(); AtomicReference flushedHash = new AtomicReference<>(); PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment( - view -> mock(PathStateBlockTransition.class), transition -> { }, + view -> { + BlockSnapshotMeta meta = view.getMeta(); + return new PathStateBlockTransition(meta.getBlockNumber(), meta.getBlockHash(), + meta.getParentHash(), meta.getTimestamp(), P66Phase.P66_ON, + Collections.emptyList()); + }, transition -> { }, (blockNumber, blockHash) -> { flushedNumber.set(blockNumber); flushedHash.set(blockHash); @@ -980,6 +1054,17 @@ private static PathStateMutation mutation(PathStateBlockTransition transition, .orElseThrow(AssertionError::new); } + private static BlockChangeView captureView(SnapshotManager manager, Chainbase database, + BlockSnapshotMeta meta) { + try (ISession session = manager.buildSession()) { + database.put(bytes("status-" + meta.getBlockNumber()), + bytes("value-" + meta.getBlockNumber())); + BlockChangeView view = BlockChangeView.capture(meta, manager.getDbs()); + session.commit(); + return view; + } + } + private static byte[] bytes(String value) { return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index c382b9351e8..e52704e24cd 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -84,6 +84,9 @@ public void readyCurrentAttachesExactCanonicalHeadAndCloses() throws Exception { assertNotNull(manager.getPathStateSnapshotHead()); assertNotNull(manager.getPathStateRuntime()); assertArrayEquals(base.encode(), manager.getPathStateSnapshotHead().getHead().encode()); + assertEquals(PathStateRuntimeAttachment.State.READY, + manager.getPathStateRuntime().status().getState()); + assertEquals(100L, manager.getPathStateRuntime().status().getReadyBlockNumber()); invoke(manager, "closePathStateRoot"); assertNull(manager.getPathStateSnapshotHead()); assertNull(manager.getPathStateRuntime()); @@ -133,11 +136,18 @@ public void shortReorgRewindsToChainbaseHeadAndRetiresOldSuffix() throws Excepti assertFalse(Files.exists(manifest.getLayerDirectory( oldSecond.getBlockNumber(), oldSecond.getBlockHash()))); assertFalse(manager.getPathStateRuntime().isFailed()); + assertEquals(PathStateRuntimeAttachment.State.READY, + manager.getPathStateRuntime().status().getState()); + assertEquals(101L, manager.getPathStateRuntime().status().getReadyBlockNumber()); when(dynamic.getLatestBlockHeaderNumber()).thenReturn(100L); when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(bytes(99))); invoke(manager, "rewindPathStateRootAfterPop"); assertNotNull(manager.getPathStateRuntime().getFailure()); + assertEquals(PathStateRuntimeAttachment.FailureStage.REORG, + manager.getPathStateRuntime().status().getFailureStage()); + assertEquals(100L, manager.getPathStateRuntime().status().getObservedBlockNumber()); + assertEquals(1L, manager.getPathStateRuntime().status().getRootLag()); assertArrayEquals(first.encode(), new PathStateCurrentStore(manifest).current().encode()); invoke(manager, "closePathStateRoot"); } From ece5d8ad18aad791e780d459535dba0651ac8a22 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 18:20:58 +0800 Subject: [PATCH 087/161] test(db): verify path state shadow equivalence --- .../SnapshotOldValueCollectorTest.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index 0602440e3f1..0df65701b31 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -20,6 +20,8 @@ import com.google.protobuf.ByteString; import java.io.Closeable; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; @@ -36,6 +38,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.tron.common.BaseMethodTest; +import org.tron.core.capsule.BlockCapsule; import org.tron.core.db.common.DbSourceInter; import org.tron.core.db2.ISession; import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; @@ -47,14 +50,24 @@ import org.tron.core.db2.core.SnapshotImpl; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.db2.core.SnapshotRoot; +import org.tron.core.db2.stateroot.PathStateBasePublication; import org.tron.core.db2.stateroot.PathStateBlockTransition; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateLayerLimits; import org.tron.core.db2.stateroot.PathStateMutation; +import org.tron.core.db2.stateroot.PathStateNodeStoreSet; +import org.tron.core.db2.stateroot.PathStateRoot; +import org.tron.core.db2.stateroot.PathStateRootMetadata; +import org.tron.core.db2.stateroot.PathStateRuntimeAdmission; import org.tron.core.db2.stateroot.PathStateRuntimeAttachment; +import org.tron.core.db2.stateroot.PathStateSnapshotHead; +import org.tron.core.db2.stateroot.PathStateStoreManifest; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; import org.tron.core.exception.TronError; import org.tron.core.store.AccountAssetStore; import org.tron.core.store.CheckTmpStore; import org.tron.protos.Protocol.Account; +import org.tron.protos.Protocol.Transaction; public class SnapshotOldValueCollectorTest extends BaseMethodTest { @@ -91,6 +104,101 @@ public void pathStateRuntimeCoexistsWithArchiveAtBlockFinalBoundary() throws Exc manager.shutdown(); } + @Test + public void disabledAndShadowCapturePreserveCanonicalBlockAndStateOutcome() throws Exception { + byte[] codeKey = bytes("equivalence-code"); + byte[] before = bytes("before"); + byte[] after = bytes("after"); + byte[] parentHash = hash(0); + byte[] accountStateRoot = hash(9); + Transaction transaction = Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().setTimestamp(11L)) + .addRet(Transaction.Result.newBuilder().setFee(7L) + .setContractRet(Transaction.Result.contractResult.SUCCESS)) + .build(); + BlockCapsule template = new BlockCapsule(12L, ByteString.copyFrom(parentHash), 1L, + Collections.singletonList(transaction)); + template.setMerkleRoot(); + template.setAccountStateRoot(accountStateRoot); + byte[] canonicalBlock = template.getData(); + byte[] canonicalRaw = template.getInstance().getBlockHeader().getRawData().toByteArray(); + byte[] canonicalBlockId = template.getBlockId().getBytes(); + BlockCapsule controlBlock = new BlockCapsule(canonicalBlock); + BlockCapsule shadowBlock = new BlockCapsule(canonicalBlock); + + SnapshotManager control = new SnapshotManager(""); + SnapshotManager shadow = new SnapshotManager(""); + Chainbase controlCode = equivalenceStore(control, "code", codeKey, before); + Chainbase shadowCode = equivalenceStore(shadow, "code", codeKey, before); + equivalenceProperties(control); + equivalenceProperties(shadow); + control.enable(); + shadow.enable(); + + Path controlDirectory = temporaryFolder.getRoot().toPath().resolve("disabled-path-state"); + assertEquals(PathStateRuntimeAdmission.Status.DISABLED, + PathStateRuntimeAdmission.inspect(false, controlDirectory, Engine.ROCKSDB).getStatus()); + assertFalse(Files.exists(controlDirectory)); + + Path shadowDirectory = temporaryFolder.newFolder("enabled-path-state").toPath(); + PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen( + shadowDirectory, Engine.ROCKSDB); + PathStateRootMetadata base; + try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRoot root = stores.createRoot(); + org.tron.core.db2.stateroot.PathStateCanonicalizer canonicalizer = + new org.tron.core.db2.stateroot.PathStateCanonicalizer(); + root.apply(Arrays.asList( + canonicalizer.put(P66Phase.P66_ON, "properties", + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), + Longs.toByteArray(1L)), + canonicalizer.put(P66Phase.P66_ON, "code", codeKey, before))); + base = PathStateRootMetadata.base(0L, parentHash, hash(99), 1L, + P66Phase.P66_ON, manifest.getIdentityDigest(), root.rootHash(), hash(7)); + new PathStateBasePublication(manifest).publish(stores, base); + } + PathStateSnapshotHead owner = PathStateSnapshotHead.open( + manifest, PathStateLayerLimits.defaults()); + PathStateRuntimeAttachment runtime = new PathStateRuntimeAttachment( + new SnapshotPathStateTransitionCollector(ignored -> Collections.emptyMap()), + owner::advance); + runtime.synchronizeReadyHead(base); + shadow.attachPathStateRuntime(runtime); + + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1L, canonicalBlockId, parentHash, 12L); + try (ISession session = control.buildSession()) { + controlCode.put(codeKey, after); + session.commit(meta); + } + try (ISession session = shadow.buildSession()) { + shadowCode.put(codeKey, after); + session.commit(meta); + } + + assertArrayEquals(controlCode.getUnchecked(codeKey), shadowCode.getUnchecked(codeKey)); + assertArrayEquals(after, shadowCode.getUnchecked(codeKey)); + assertEquals(((SnapshotImpl) controlCode.getHead()).getBlockSnapshotMeta(), + ((SnapshotImpl) shadowCode.getHead()).getBlockSnapshotMeta()); + assertEquals(PathStateRuntimeAttachment.State.READY, runtime.status().getState()); + assertEquals(1L, owner.getHead().getBlockNumber()); + assertFalse(Arrays.equals(base.getStateRoot(), owner.getHead().getStateRoot())); + + assertArrayEquals(canonicalBlock, controlBlock.getData()); + assertArrayEquals(canonicalBlock, shadowBlock.getData()); + assertArrayEquals(canonicalRaw, + shadowBlock.getInstance().getBlockHeader().getRawData().toByteArray()); + assertArrayEquals(canonicalBlockId, controlBlock.getBlockId().getBytes()); + assertArrayEquals(canonicalBlockId, shadowBlock.getBlockId().getBytes()); + assertArrayEquals(accountStateRoot, shadowBlock.getInstance().getBlockHeader().getRawData() + .getAccountStateRoot().toByteArray()); + assertEquals(transaction.getRetList(), shadowBlock.getInstance().getTransactions(0) + .getRetList()); + + assertSame(runtime, shadow.detachPathStateRuntime(runtime)); + control.shutdown(); + shadow.shutdown(); + } + @Test public void pathStateP66ActivationScansPostStateThenResumesIncrementalCapture() throws Exception { @@ -1065,6 +1173,21 @@ private static BlockChangeView captureView(SnapshotManager manager, Chainbase da } } + private static Chainbase equivalenceStore(SnapshotManager manager, String dbName, + byte[] key, byte[] value) { + MemoryDb database = new MemoryDb(dbName); + database.put(key, value); + Chainbase chainbase = new Chainbase(new SnapshotRoot(database)); + manager.add(chainbase); + return chainbase; + } + + private static void equivalenceProperties(SnapshotManager manager) { + equivalenceStore(manager, HistoricalAccountAssetBalanceResolver.PROPERTIES_DATABASE, + HistoricalAccountAssetBalanceResolver.proposal66PhysicalKey(), + Longs.toByteArray(1L)); + } + private static byte[] bytes(String value) { return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); } From 14d399bb36339c680b6f4dc6520c6ad585019dc6 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 18:40:57 +0800 Subject: [PATCH 088/161] feat(db): publish non-consensus path state root --- .../org/tron/core/capsule/BlockCapsule.java | 14 ++ .../tron/core/db2/core/SnapshotManager.java | 12 ++ .../stateroot/PathStateRuntimeAttachment.java | 121 ++++++++++++++++-- .../main/java/org/tron/core/db/Manager.java | 48 ++++++- .../tron/core/capsule/BlockCapsuleTest.java | 38 ++++++ .../SnapshotOldValueCollectorTest.java | 27 +++- .../adv/SanitizeUnknownFieldsTest.java | 18 +++ protocol/src/main/protos/core/Tron.proto | 1 + 8 files changed, 264 insertions(+), 15 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java index e6cbd52e595..7abdba45698 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -261,6 +261,20 @@ public void setAccountStateRoot(byte[] root) { this.block.getBlockHeader().toBuilder().setRawData(blockHeaderRaw)).build(); } + /** Sets optional non-consensus path-state metadata outside the signed raw header. */ + public void setStateRoot(byte[] root) { + if (root == null || root.length != Sha256Hash.LENGTH) { + throw new IllegalArgumentException("state root must be exactly 32 bytes"); + } + this.block = this.block.toBuilder().setBlockHeader( + this.block.getBlockHeader().toBuilder().setStateRoot(ByteString.copyFrom(root))).build(); + } + + /** Returns a copy of the optional non-consensus path-state metadata. */ + public byte[] getStateRoot() { + return this.block.getBlockHeader().getStateRoot().toByteArray(); + } + /* only for genesis */ public void setWitness(String witness) { BlockHeader.raw blockHeaderRaw = diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index 7b0ae842fc6..cbfb12bc423 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -294,6 +294,18 @@ public synchronized void commit(BlockSnapshotMeta meta) { } } + /** Previews optional producer metadata from the active block session without publishing it. */ + public synchronized byte[] previewPathStateRoot(BlockSnapshotMeta meta) { + if (activeSession <= 0) { + throw new RevokingStoreIllegalStateException(activeSession); + } + if (pathStateRuntimeAttachment == null) { + return null; + } + return pathStateRuntimeAttachment.preview(BlockChangeView.capture( + Objects.requireNonNull(meta, "meta"), dbs)); + } + private void validateBlockMeta(BlockSnapshotMeta meta) { BlockSnapshotMeta previousMeta = null; for (Chainbase db : dbs) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java index 294a79eb603..949ae1866ae 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java @@ -14,6 +14,7 @@ public final class PathStateRuntimeAttachment { private final PathStateTransitionCollector collector; private final TransitionSink sink; private final BaseFlushSink baseFlushSink; + private final TransitionPreviewer previewer; private Throwable failure; private FailureStage failureStage; private long readyBlockNumber = -1; @@ -21,16 +22,44 @@ public final class PathStateRuntimeAttachment { private long observedBlockNumber = -1; private byte[] observedBlockHash; private PathStateBlockTransition pending; + private HeaderDiagnostic headerDiagnostic = HeaderDiagnostic.NONE; + private long headerDiagnosticBlockNumber = -1; + private byte[] headerDiagnosticBlockHash; public PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink) { - this(collector, sink, (blockNumber, blockHash) -> { }); + this(collector, sink, (blockNumber, blockHash) -> { }, null); } public PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink, BaseFlushSink baseFlushSink) { + this(collector, sink, baseFlushSink, null); + } + + public PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink, + BaseFlushSink baseFlushSink, TransitionPreviewer previewer) { this.collector = Objects.requireNonNull(collector, "collector"); this.sink = Objects.requireNonNull(sink, "sink"); this.baseFlushSink = Objects.requireNonNull(baseFlushSink, "baseFlushSink"); + this.previewer = previewer; + } + + /** Computes producer metadata without observing, publishing, or failing this runtime. */ + public synchronized byte[] preview(BlockChangeView view) { + if (failure != null || previewer == null || status().getState() != State.READY) { + return null; + } + try { + PathStateBlockTransition transition = collectAndValidate(view); + byte[] candidate = previewer.prepare(transition); + if (candidate == null || candidate.length != 32) { + throw new IOException("path-state preview root must be exactly 32 bytes"); + } + return copy(candidate); + } catch (IOException | RuntimeException previewFailure) { + logger.warn("Path-state producer preview unavailable; state_root remains absent", + previewFailure); + return null; + } } /** Capture failures fail only this shadow runtime and never reject the canonical block. */ @@ -41,14 +70,7 @@ public synchronized PathStateBlockTransition capture(BlockChangeView view) { return null; } try { - PathStateBlockTransition transition = Objects.requireNonNull(collector.collect(admitted), - "path-state collector returned null"); - if (transition.getBlockNumber() != admitted.getMeta().getBlockNumber() - || !Arrays.equals(transition.getBlockHash(), admitted.getMeta().getBlockHash()) - || !Arrays.equals(transition.getParentHash(), admitted.getMeta().getParentHash()) - || transition.getTimestamp() != admitted.getMeta().getTimestamp()) { - throw new IOException("path-state collector changed the captured block identity"); - } + PathStateBlockTransition transition = collectAndValidate(admitted); pending = transition; return transition; } catch (IOException | RuntimeException currentFailure) { @@ -102,7 +124,37 @@ public synchronized Status status() { && readyBlockNumber == observedBlockNumber && Arrays.equals(readyBlockHash, observedBlockHash) ? State.READY : State.NOT_READY; return new Status(state, readyBlockNumber, readyBlockHash, observedBlockNumber, - observedBlockHash, failureStage, classify(failure), failure); + observedBlockHash, failureStage, classify(failure), failure, headerDiagnostic, + headerDiagnosticBlockNumber, headerDiagnosticBlockHash); + } + + /** Records a non-blocking comparison of carried header metadata against the local READY root. */ + public synchronized void diagnoseHeader(long blockNumber, byte[] blockHash, byte[] carriedRoot, + byte[] localRoot) { + headerDiagnosticBlockNumber = blockNumber; + headerDiagnosticBlockHash = copy(blockHash); + if (carriedRoot == null || carriedRoot.length == 0) { + headerDiagnostic = HeaderDiagnostic.ABSENT; + return; + } + if (carriedRoot.length != 32) { + headerDiagnostic = HeaderDiagnostic.INVALID_LENGTH; + logger.warn("Path-state header diagnostic: block={}, result={}, length={}", blockNumber, + headerDiagnostic, carriedRoot.length); + return; + } + if (failure != null || readyBlockNumber != blockNumber + || !Arrays.equals(readyBlockHash, blockHash) || localRoot == null + || localRoot.length != 32) { + headerDiagnostic = HeaderDiagnostic.NOT_AVAILABLE; + return; + } + headerDiagnostic = Arrays.equals(carriedRoot, localRoot) + ? HeaderDiagnostic.MATCH : HeaderDiagnostic.MISMATCH; + if (headerDiagnostic == HeaderDiagnostic.MISMATCH) { + logger.warn("Path-state header diagnostic: block={}, result={}", blockNumber, + headerDiagnostic); + } } /** Seeds or rewinds the exact verified durable head; failed runtimes cannot be reset. */ @@ -169,6 +221,19 @@ private void observe(BlockChangeView view) { observedBlockHash = copy(hash); } + private PathStateBlockTransition collectAndValidate(BlockChangeView view) throws IOException { + BlockChangeView admitted = Objects.requireNonNull(view, "view"); + PathStateBlockTransition transition = Objects.requireNonNull(collector.collect(admitted), + "path-state collector returned null"); + if (transition.getBlockNumber() != admitted.getMeta().getBlockNumber() + || !Arrays.equals(transition.getBlockHash(), admitted.getMeta().getBlockHash()) + || !Arrays.equals(transition.getParentHash(), admitted.getMeta().getParentHash()) + || transition.getTimestamp() != admitted.getMeta().getTimestamp()) { + throw new IOException("path-state collector changed the captured block identity"); + } + return transition; + } + private static FailureKind classify(Throwable failure) { if (failure == null) { return FailureKind.NONE; @@ -230,6 +295,15 @@ public enum FailureKind { RUNTIME } + public enum HeaderDiagnostic { + NONE, + ABSENT, + INVALID_LENGTH, + NOT_AVAILABLE, + MATCH, + MISMATCH + } + public static final class Status { private final State state; @@ -241,10 +315,14 @@ public static final class Status { private final FailureKind failureKind; private final String failureType; private final String failureMessage; + private final HeaderDiagnostic headerDiagnostic; + private final long headerDiagnosticBlockNumber; + private final byte[] headerDiagnosticBlockHash; private Status(State state, long readyBlockNumber, byte[] readyBlockHash, long observedBlockNumber, byte[] observedBlockHash, FailureStage failureStage, - FailureKind failureKind, Throwable failure) { + FailureKind failureKind, Throwable failure, HeaderDiagnostic headerDiagnostic, + long headerDiagnosticBlockNumber, byte[] headerDiagnosticBlockHash) { this.state = state; this.readyBlockNumber = readyBlockNumber; this.readyBlockHash = copy(readyBlockHash); @@ -254,6 +332,9 @@ private Status(State state, long readyBlockNumber, byte[] readyBlockHash, this.failureKind = failureKind; this.failureType = failure == null ? null : failure.getClass().getName(); this.failureMessage = failure == null ? null : failure.getMessage(); + this.headerDiagnostic = headerDiagnostic; + this.headerDiagnosticBlockNumber = headerDiagnosticBlockNumber; + this.headerDiagnosticBlockHash = copy(headerDiagnosticBlockHash); } public State getState() { @@ -295,6 +376,18 @@ public String getFailureType() { public String getFailureMessage() { return failureMessage; } + + public HeaderDiagnostic getHeaderDiagnostic() { + return headerDiagnostic; + } + + public long getHeaderDiagnosticBlockNumber() { + return headerDiagnosticBlockNumber; + } + + public byte[] getHeaderDiagnosticBlockHash() { + return copy(headerDiagnosticBlockHash); + } } @FunctionalInterface @@ -308,4 +401,10 @@ public interface BaseFlushSink { void accept(long blockNumber, byte[] blockHash) throws IOException; } + + @FunctionalInterface + public interface TransitionPreviewer { + + byte[] prepare(PathStateBlockTransition transition) throws IOException; + } } diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index d5f11f0a3f8..d4e021b4fb8 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -796,7 +796,8 @@ private void attachPathStateBlockFinalRuntime() throws java.io.IOException { PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment( new SnapshotPathStateTransitionCollector(accountAssetStore::prefixQuery, this::scanPathStateActivationAccounts), - this::advancePathStateRoot, this::flushPathStateBaseThrough); + this::advancePathStateRoot, this::flushPathStateBaseThrough, + transition -> pathStateSnapshotHead.prepare(transition).getStateRoot()); attachment.synchronizeReadyHead(pathStateSnapshotHead.getHead()); ((SnapshotManager) revokingStore).attachPathStateRuntime(attachment); pathStateRuntime = attachment; @@ -1498,6 +1499,29 @@ private void commitBlockSession(ISession blockSession, BlockCapsule block) { block.getBlockId().getBytes(), block.getParentHash().getBytes(), block.getTimeStamp())); + diagnosePathStateHeader(block); + } + + private void diagnosePathStateHeader(BlockCapsule block) { + PathStateRuntimeAttachment runtime = pathStateRuntime; + if (runtime == null) { + return; + } + byte[] localRoot = null; + try { + PathStateSnapshotHead owner = pathStateSnapshotHead; + if (owner != null) { + PathStateRootMetadata local = owner.getHead(); + if (local.getBlockNumber() == block.getNum() + && Arrays.equals(local.getBlockHash(), block.getBlockId().getBytes())) { + localRoot = local.getStateRoot(); + } + } + } catch (java.io.IOException | RuntimeException diagnosticFailure) { + logger.warn("Path-state local root unavailable for header diagnostic", diagnosticFailure); + } + runtime.diagnoseHeader(block.getNum(), block.getBlockId().getBytes(), block.getStateRoot(), + localRoot); } private void switchFork(BlockCapsule newHead) @@ -2166,9 +2190,10 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { blockCapsule.addAllTransactions(toBePacked); accountStateCallBack.executeGenerateFinish(); + blockCapsule.setMerkleRoot(); + previewGeneratedPathStateRoot(blockCapsule); session.reset(); - blockCapsule.setMerkleRoot(); blockCapsule.sign(miner.getPrivateKey()); BlockCapsule capsule = new BlockCapsule(blockCapsule.getInstance()); @@ -2185,6 +2210,25 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { return capsule; } + private void previewGeneratedPathStateRoot(BlockCapsule block) { + if (pathStateRuntime == null || !(revokingStore instanceof SnapshotManager)) { + return; + } + try { + byte[] targetBlockHash = new BlockCapsule(block.getInstance()).getBlockId().getBytes(); + byte[] root = ((SnapshotManager) revokingStore).previewPathStateRoot( + BlockSnapshotMeta.forBlock(block.getNum(), targetBlockHash, + block.getParentHash().getBytes(), block.getTimeStamp())); + // tag 3 plus a 32-byte length-delimited value adds exactly 34 protobuf bytes. + if (root != null && block.getSerializedSize() <= ChainConstant.BLOCK_SIZE - 34) { + block.setStateRoot(root); + } + } catch (RuntimeException previewFailure) { + logger.warn("Path-state producer preview unavailable; state_root remains absent", + previewFailure); + } + } + private void filterOwnerAddress(TransactionCapsule transactionCapsule, Set result) { byte[] owner = transactionCapsule.getOwnerAddress(); String ownerAddress = ByteArray.toHexString(owner); diff --git a/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java b/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java index b258fbf99a1..88d4d7feca4 100644 --- a/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java @@ -4,6 +4,7 @@ import static org.mockito.Mockito.when; import com.google.protobuf.ByteString; +import com.google.protobuf.util.JsonFormat; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -155,6 +156,43 @@ public void testGetData() { } + @Test + public void stateRootRoundTripsOutsideBlockIdentityAndSignature() throws Exception { + String key = PublicMethod.getRandomPrivateKey(); + byte[] witnessAddress = PublicMethod.getAddressByteByPrivateKey(key); + BlockCapsule block = new BlockCapsule(5, + Sha256Hash.wrap(ByteString.copyFrom(ByteArray.fromHexString( + "9938a342238077182498b464ac0292229938a342238077182498b464ac029222"))), + 6789, + ByteString.copyFrom(witnessAddress)); + block.setMerkleRoot(); + block.sign(ByteArray.fromHexString(key)); + byte[] rawData = block.getInstance().getBlockHeader().getRawData().toByteArray(); + byte[] blockId = block.getBlockId().getBytes(); + byte[] signature = block.getInstance().getBlockHeader().getWitnessSignature().toByteArray(); + byte[] root = Sha256Hash.of(true, "path-state".getBytes()).getBytes(); + + block.setStateRoot(root); + + Assert.assertArrayEquals(root, block.getStateRoot()); + Assert.assertArrayEquals(rawData, + block.getInstance().getBlockHeader().getRawData().toByteArray()); + Assert.assertArrayEquals(blockId, block.getBlockId().getBytes()); + Assert.assertArrayEquals(signature, + block.getInstance().getBlockHeader().getWitnessSignature().toByteArray()); + BlockCapsule restored = new BlockCapsule(block.getData()); + Assert.assertArrayEquals(root, restored.getStateRoot()); + Assert.assertEquals(3, BlockHeader.getDescriptor().findFieldByName("state_root").getNumber()); + Assert.assertTrue(JsonFormat.printer().print(restored.getInstance().getBlockHeader()) + .contains("\"stateRoot\"")); + + DynamicPropertiesStore dps = mock(DynamicPropertiesStore.class); + when(dps.getAllowMultiSign()).thenReturn(0L); + Assert.assertTrue(restored.validateSignature(dps, mock(AccountStore.class))); + Assert.assertThrows(IllegalArgumentException.class, + () -> block.setStateRoot(new byte[31])); + } + @Test public void testValidate() { diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index 0df65701b31..a4e5fb25da5 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -161,17 +162,23 @@ public void disabledAndShadowCapturePreserveCanonicalBlockAndStateOutcome() thro manifest, PathStateLayerLimits.defaults()); PathStateRuntimeAttachment runtime = new PathStateRuntimeAttachment( new SnapshotPathStateTransitionCollector(ignored -> Collections.emptyMap()), - owner::advance); + owner::advance, (blockNumber, blockHash) -> { }, + transition -> owner.prepare(transition).getStateRoot()); runtime.synchronizeReadyHead(base); shadow.attachPathStateRuntime(runtime); BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1L, canonicalBlockId, parentHash, 12L); try (ISession session = control.buildSession()) { controlCode.put(codeKey, after); + assertNull(control.previewPathStateRoot(meta)); session.commit(meta); } try (ISession session = shadow.buildSession()) { shadowCode.put(codeKey, after); + byte[] candidate = shadow.previewPathStateRoot(meta); + assertEquals(32, candidate.length); + assertArrayEquals(base.getStateRoot(), owner.getHead().getStateRoot()); + shadowBlock.setStateRoot(candidate); session.commit(meta); } @@ -184,7 +191,7 @@ public void disabledAndShadowCapturePreserveCanonicalBlockAndStateOutcome() thro assertFalse(Arrays.equals(base.getStateRoot(), owner.getHead().getStateRoot())); assertArrayEquals(canonicalBlock, controlBlock.getData()); - assertArrayEquals(canonicalBlock, shadowBlock.getData()); + assertFalse(Arrays.equals(canonicalBlock, shadowBlock.getData())); assertArrayEquals(canonicalRaw, shadowBlock.getInstance().getBlockHeader().getRawData().toByteArray()); assertArrayEquals(canonicalBlockId, controlBlock.getBlockId().getBytes()); @@ -193,6 +200,22 @@ public void disabledAndShadowCapturePreserveCanonicalBlockAndStateOutcome() thro .getAccountStateRoot().toByteArray()); assertEquals(transaction.getRetList(), shadowBlock.getInstance().getTransactions(0) .getRetList()); + assertArrayEquals(owner.getHead().getStateRoot(), shadowBlock.getStateRoot()); + + runtime.diagnoseHeader(1L, canonicalBlockId, shadowBlock.getStateRoot(), + owner.getHead().getStateRoot()); + assertEquals(PathStateRuntimeAttachment.HeaderDiagnostic.MATCH, + runtime.status().getHeaderDiagnostic()); + runtime.diagnoseHeader(1L, canonicalBlockId, new byte[31], owner.getHead().getStateRoot()); + assertEquals(PathStateRuntimeAttachment.HeaderDiagnostic.INVALID_LENGTH, + runtime.status().getHeaderDiagnostic()); + runtime.diagnoseHeader(1L, canonicalBlockId, hash(88), owner.getHead().getStateRoot()); + assertEquals(PathStateRuntimeAttachment.HeaderDiagnostic.MISMATCH, + runtime.status().getHeaderDiagnostic()); + runtime.diagnoseHeader(1L, canonicalBlockId, new byte[0], owner.getHead().getStateRoot()); + assertEquals(PathStateRuntimeAttachment.HeaderDiagnostic.ABSENT, + runtime.status().getHeaderDiagnostic()); + assertEquals(PathStateRuntimeAttachment.State.READY, runtime.status().getState()); assertSame(runtime, shadow.detachPathStateRuntime(runtime)); control.shutdown(); diff --git a/framework/src/test/java/org/tron/core/net/message/adv/SanitizeUnknownFieldsTest.java b/framework/src/test/java/org/tron/core/net/message/adv/SanitizeUnknownFieldsTest.java index 7d883b7207d..8511b99adfd 100644 --- a/framework/src/test/java/org/tron/core/net/message/adv/SanitizeUnknownFieldsTest.java +++ b/framework/src/test/java/org/tron/core/net/message/adv/SanitizeUnknownFieldsTest.java @@ -186,6 +186,24 @@ public void blockMessageSanitizeUpdatesBothCapsuleAndWireBytes() throws Exceptio paddedBytes.length, msg.getData().length); } + @Test + public void blockMessageSanitizePreservesKnownStateRoot() throws Exception { + ByteString stateRoot = ByteString.copyFrom(new byte[32]); + BlockHeader header = sampleBlock().getBlockHeader().toBuilder() + .setStateRoot(stateRoot) + .setUnknownFields(PADDING) + .build(); + Block padded = sampleBlock().toBuilder().setBlockHeader(header).build(); + BlockMessage message = new BlockMessage(padded.toByteArray()); + + message.sanitize(); + + assertEquals(stateRoot, + message.getBlockCapsule().getInstance().getBlockHeader().getStateRoot()); + assertTrue(message.getBlockCapsule().getInstance().getBlockHeader() + .getUnknownFields().asMap().isEmpty()); + } + @Test public void blockMessageSanitizeSkipsDataRewriteOnCleanBlock() throws Exception { byte[] cleanBytes = sampleBlock().toByteArray(); diff --git a/protocol/src/main/protos/core/Tron.proto b/protocol/src/main/protos/core/Tron.proto index a68e841bb60..1ffcfe2adf8 100644 --- a/protocol/src/main/protos/core/Tron.proto +++ b/protocol/src/main/protos/core/Tron.proto @@ -514,6 +514,7 @@ message BlockHeader { } raw raw_data = 1; bytes witness_signature = 2; + bytes state_root = 3; } // block From 2d7c5faaf20238bb36706a0b8ee7c4a8c1cc2ccc Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 19:08:03 +0800 Subject: [PATCH 089/161] fix(db): admit empty path state index keys --- .../db2/stateroot/PathStateBlockTransition.java | 3 --- .../db2/stateroot/PathStateCanonicalizer.java | 8 ++++++-- .../db2/stateroot/PathStateCommitmentCodec.java | 10 +++------- .../stateroot/PathStateRebuildCoordinator.java | 10 +++------- .../stateroot/PathStateBlockTransitionTest.java | 15 +++++++++++---- .../db2/stateroot/PathStateCanonicalizerTest.java | 11 +++++++++++ .../stateroot/PathStateCommitmentCodecTest.java | 10 ++++++++-- .../PathStateRebuildCoordinatorTest.java | 6 +++++- 8 files changed, 47 insertions(+), 26 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java index b5467d021a6..0c0d724aa77 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java @@ -108,9 +108,6 @@ private List prepare(Collection supplied) { PathStateMutation mutation = copyMutation(Objects.requireNonNull(candidate, "mutation")); StoreIdentity store = descriptor.require(mutation.getDbName()); byte[] key = mutation.getCanonicalKey(); - if (key.length == 0) { - throw new IllegalArgumentException("canonicalKey must not be empty"); - } if (!unique.add(new MutationKey(store.getStoreId(), key))) { throw new IllegalArgumentException("duplicate path-state mutation key"); } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java index cd6cf043902..b2a63103bc9 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java @@ -103,7 +103,7 @@ public PathStateMutation accountAsset(P66Phase phase, byte[] address, String tok public void requireSnapshotAccountLayout(P66Phase phase, byte[] physicalKey, byte[] rawValue) { P66Phase target = Objects.requireNonNull(phase, "phase"); - byte[] key = copyNonEmpty(physicalKey, "physicalKey"); + byte[] key = copy(physicalKey, "physicalKey"); requireLength(key, ADDRESS_LENGTH, "account key"); Account account = parseAccount(key, rawValue); if (target.directAssetsEnabled()) { @@ -134,7 +134,7 @@ private static void configure(Map formats, String dbName, S } private static byte[] canonicalKey(P66Phase phase, String dbName, byte[] physicalKey) { - byte[] key = copyNonEmpty(physicalKey, "physicalKey"); + byte[] key = copy(physicalKey, "physicalKey"); switch (dbName) { case "account": case "abi": @@ -286,6 +286,10 @@ private static byte[] copyNonEmpty(byte[] value, String name) { return copy; } + private static byte[] copy(byte[] value, String name) { + return Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + } + private static void requireLength(byte[] value, int length, String name) { if (value.length != length) { throw new IllegalArgumentException(name + " must be exactly " + length + " bytes"); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java index 169de35877b..26b6460addb 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java @@ -35,7 +35,7 @@ private PathStateCommitmentCodec() { /** Returns the secure per-Store trie key for one canonical present value. */ public static byte[] storeLeafKey(int stableStoreId, byte[] canonicalKey) { requireStoreId(stableStoreId); - byte[] key = nonEmpty(canonicalKey, "canonicalKey"); + byte[] key = copy(canonicalKey, "canonicalKey"); ByteBuffer material = ByteBuffer.allocate(Short.BYTES + STORE_LEAF_KEY_DOMAIN.length + Short.BYTES + Integer.BYTES + Integer.BYTES + key.length); putDomain(material, STORE_LEAF_KEY_DOMAIN); @@ -98,12 +98,8 @@ private static void requireStoreId(int stableStoreId) { } } - private static byte[] nonEmpty(byte[] value, String name) { - byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); - if (copy.length == 0) { - throw new IllegalArgumentException(name + " must not be empty"); - } - return copy; + private static byte[] copy(byte[] value, String name) { + return Arrays.copyOf(Objects.requireNonNull(value, name), value.length); } private static byte[] rlpList(byte[]... rawItems) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index a13b1a0ea83..fb34a091337 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -176,7 +176,7 @@ private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root } private void accept(byte[] physicalKey, byte[] rawValue) throws IOException { - byte[] key = copyNonEmpty(physicalKey, "physicalKey"); + byte[] key = copy(physicalKey, "physicalKey"); byte[] value = Arrays.copyOf(Objects.requireNonNull(rawValue, "rawValue"), rawValue.length); if (previousKey != null && compare(store, previousKey, key) >= 0) { @@ -252,12 +252,8 @@ private static void putLong(Hasher hasher, long value) { hasher.putBytes(ByteBuffer.allocate(Long.BYTES).putLong(value).array()); } - private static byte[] copyNonEmpty(byte[] value, String name) { - byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); - if (copy.length == 0) { - throw new IllegalArgumentException(name + " must not be empty"); - } - return copy; + private static byte[] copy(byte[] value, String name) { + return Arrays.copyOf(Objects.requireNonNull(value, name), value.length); } /** Caller-owned exact-27 native snapshot boundary. */ diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java index 4ab8b57ca29..a25602fd45e 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java @@ -78,7 +78,7 @@ public void noOpBlockHasDeterministicIndependentOracleDigest() throws Exception } @Test - public void rejectsAmbiguousOrOutOfScopeMutations() { + public void rejectsDuplicateOrOutOfScopeMutations() { PathStateMutation first = PathStateMutation.put("proposal", new byte[]{1}, new byte[]{2}); PathStateMutation duplicate = PathStateMutation.delete("proposal", new byte[]{1}); @@ -87,9 +87,16 @@ public void rejectsAmbiguousOrOutOfScopeMutations() { assertThrows(IllegalArgumentException.class, () -> transition(Collections.singletonList( PathStateMutation.put("unknown", new byte[]{1}, new byte[]{2})))); - assertThrows(IllegalArgumentException.class, - () -> transition(Collections.singletonList( - PathStateMutation.put("proposal", new byte[0], new byte[]{2})))); + } + + @Test + public void lengthDelimitedEmptyKeyIsDeterministic() { + PathStateBlockTransition first = transition(Collections.singletonList( + PathStateMutation.put("accountid-index", new byte[0], new byte[]{2}))); + PathStateBlockTransition second = transition(Collections.singletonList( + PathStateMutation.put("accountid-index", new byte[0], new byte[]{2}))); + + assertArrayEquals(first.getPayloadDigest(), second.getPayloadDigest()); } @Test diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java index 66fd65e5470..59403af02e3 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java @@ -167,6 +167,17 @@ public void genericPresentEmptyRemainsDistinctFromDelete() { () -> canonicalizer.put(P66Phase.P66_ON, "unknown", key, new byte[0])); } + @Test + public void genericIndexStoresPreserveEmptyPhysicalKeys() { + PathStateMutation accountId = canonicalizer.put( + P66Phase.P66_ON, "accountid-index", new byte[0], address(5)); + PathStateMutation accountName = canonicalizer.put( + P66Phase.P66_ON, "account-index", new byte[0], address(6)); + + assertArrayEquals(new byte[0], accountId.getCanonicalKey()); + assertArrayEquals(new byte[0], accountName.getCanonicalKey()); + } + private static Account account(byte[] address) { return Account.newBuilder().setAddress(ByteString.copyFrom(address)).build(); } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java index 07bfb5aac5d..e206c482b3a 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java @@ -49,6 +49,14 @@ public void approvedAbiAndAssetIssueStoresHaveIndependentLeafDomains() throws Ex 7, new byte[]{1}); } + @Test + public void lengthDelimitedEmptyKeyHasAnIndependentLeafIdentity() throws Exception { + byte[] empty = PathStateCommitmentCodec.storeLeafKey(2, new byte[0]); + assertArrayEquals(referenceStoreKey(2, new byte[0]), empty); + assertFalse(Arrays.equals(empty, + PathStateCommitmentCodec.storeLeafKey(2, new byte[]{0}))); + } + @Test public void presentValuesKeepEmptyAndZeroDistinct() { assertArrayEquals(Hex.decode("c20180"), @@ -83,8 +91,6 @@ public void superLeafGoldensBindStableIdentityFormatAndRoot() throws Exception { public void rejectsAmbiguousOrUnboundInputs() { assertThrows(IllegalArgumentException.class, () -> PathStateCommitmentCodec.storeLeafKey(0, new byte[]{1})); - assertThrows(IllegalArgumentException.class, - () -> PathStateCommitmentCodec.storeLeafKey(1, new byte[0])); assertThrows(NullPointerException.class, () -> PathStateCommitmentCodec.presentLeafValue(null)); assertThrows(IllegalArgumentException.class, diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index f6d543f9fda..76d043897e6 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -50,14 +50,18 @@ public void rebuildsAndPublishesExactSnapshotAcrossNativeEngines() throws Except source.add("proposal", new byte[]{1}, new byte[]{11}); source.add("proposal", new byte[]{2}, new byte[]{22}); source.add("abi", address(1), new byte[0]); + source.add("accountid-index", new byte[0], address(2)); + source.add("account-index", new byte[0], address(3)); RebuildResult result = new PathStateRebuildCoordinator().rebuild(manifest, source); OracleResult oracle = independentOracle(source); assertEquals(27, result.getStores().size()); - assertEquals(3, result.getTotalEntries()); + assertEquals(5, result.getTotalEntries()); assertEquals(2, result.requireStore("proposal").getEntryCount()); assertEquals(1, result.requireStore("abi").getEntryCount()); + assertEquals(1, result.requireStore("accountid-index").getEntryCount()); + assertEquals(1, result.requireStore("account-index").getEntryCount()); assertEquals(0, result.requireStore("account").getEntryCount()); assertArrayEquals(result.getSourceDigest(), result.getMetadata().getPayloadDigest()); assertTrue(source.getVerificationCount() >= 2); From dcaa49f2b7a1de2c0ecf41c6ad9ee86a56dea040 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 27 Aug 2026 19:17:34 +0800 Subject: [PATCH 090/161] fix(db): project lazy account assets during rebuild --- .../db2/stateroot/PathStateCanonicalizer.java | 38 ++++++++++++++++++- .../PathStateRebuildCoordinator.java | 9 ++++- .../main/java/org/tron/core/db/Manager.java | 6 +-- .../stateroot/PathStateCanonicalizerTest.java | 4 ++ ...athStateManagerStartupIntegrationTest.java | 2 +- .../PathStateRebuildCoordinatorTest.java | 10 +++++ 6 files changed, 62 insertions(+), 7 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java index b2a63103bc9..861b448519b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java @@ -107,8 +107,8 @@ public void requireSnapshotAccountLayout(P66Phase phase, byte[] physicalKey, requireLength(key, ADDRESS_LENGTH, "account key"); Account account = parseAccount(key, rawValue); if (target.directAssetsEnabled()) { - if (!account.getAssetOptimized() || !account.getAssetMap().isEmpty() - || !account.getAssetV2Map().isEmpty()) { + if (account.getAssetOptimized() && (!account.getAssetMap().isEmpty() + || !account.getAssetV2Map().isEmpty())) { throw new IllegalArgumentException("P66-on snapshot Account layout is mixed"); } } else if (account.getAssetOptimized()) { @@ -116,6 +116,40 @@ public void requireSnapshotAccountLayout(P66Phase phase, byte[] physicalKey, } } + /** Projects a lazily migrated P66 Account's embedded V2 balances into canonical direct rows. */ + public List projectSnapshotAccountAssets(P66Phase phase, + byte[] physicalKey, byte[] rawValue) { + P66Phase target = Objects.requireNonNull(phase, "phase"); + byte[] key = copy(physicalKey, "physicalKey"); + requireLength(key, ADDRESS_LENGTH, "account key"); + Account account = parseAccount(key, rawValue); + if (!target.directAssetsEnabled() || account.getAssetOptimized()) { + return Collections.emptyList(); + } + List> balances = new ArrayList<>( + account.getAssetV2Map().entrySet()); + balances.sort(Map.Entry.comparingByKey()); + List projected = new ArrayList<>(balances.size()); + for (Map.Entry balance : balances) { + projected.add(accountAsset(target, key, balance.getKey(), balance.getValue())); + } + return Collections.unmodifiableList(projected); + } + + /** Requires that a physical direct row belongs to an already optimized Account. */ + public void requirePhysicalAccountAssetOwner(P66Phase phase, byte[] physicalKey, + byte[] rawValue) { + P66Phase target = Objects.requireNonNull(phase, "phase"); + byte[] key = copy(physicalKey, "physicalKey"); + requireLength(key, ADDRESS_LENGTH, "account key"); + Account account = parseAccount(key, rawValue); + if (!target.directAssetsEnabled() || !account.getAssetOptimized() + || !account.getAssetMap().isEmpty() || !account.getAssetV2Map().isEmpty()) { + throw new IllegalArgumentException( + "physical AccountAsset row requires an optimized owning Account"); + } + } + /** Extracts and validates the owning Account address from one direct physical key. */ public byte[] accountAddressFromAssetKey(P66Phase phase, byte[] physicalKey) { P66Phase target = Objects.requireNonNull(phase, "phase"); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index fb34a091337..ae8cdd83d77 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -186,6 +186,13 @@ private void accept(byte[] physicalKey, byte[] rawValue) throws IOException { validateAccountAssetLayout(key, value); PathStateMutation mutation = canonicalizer.put(phase, store.getDbName(), key, value); root.apply(Collections.singletonList(mutation)); + if ("account".equals(store.getDbName())) { + List projected = canonicalizer.projectSnapshotAccountAssets( + phase, key, value); + if (!projected.isEmpty()) { + root.apply(projected); + } + } putBytes(inputDigest, key); putBytes(inputDigest, value); previousKey = key; @@ -205,7 +212,7 @@ private void validateAccountAssetLayout(byte[] key, byte[] value) throws IOExcep if (accountValue == null) { throw new IOException("path-state AccountAsset row has no owning Account"); } - canonicalizer.requireSnapshotAccountLayout(phase, accountKey, accountValue); + canonicalizer.requirePhysicalAccountAssetOwner(phase, accountKey, accountValue); } private StoreResult finish() { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index d4e021b4fb8..89afbc38d3f 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -884,11 +884,11 @@ private SnapshotIdentity readPathStateSnapshotIdentity() throws java.io.IOExcept throw new java.io.IOException( "Path-state rebuild block differs from persisted Chainbase head"); } - long allowSameTokenName = dynamic.getAllowSameTokenName(); - if (allowSameTokenName != 0 && allowSameTokenName != 1) { + long allowAssetOptimization = dynamic.getAllowAccountAssetOptimizationFromRoot(); + if (allowAssetOptimization != 0 && allowAssetOptimization != 1) { throw new java.io.IOException("Path-state rebuild P66 phase is invalid"); } - P66Phase phase = allowSameTokenName == 0 ? P66Phase.P66_OFF : P66Phase.P66_ON; + P66Phase phase = allowAssetOptimization == 0 ? P66Phase.P66_OFF : P66Phase.P66_ON; return new SnapshotIdentity(blockNumber, blockHash, block.getParentHash().getBytes(), timestamp, phase); } catch (BadItemException | ItemNotFoundException failure) { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java index 59403af02e3..b0f00f6efcd 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCanonicalizerTest.java @@ -84,6 +84,10 @@ public void activationAndOnProduceTheSameCanonicalAccountGolden() throws Excepti assertTrue(canonical.getAssetV2Map().isEmpty()); assertEquals("1a154100000000000000000000000000000000000000022063e00301", ByteArray.toHexString(activation.getCanonicalValue())); + assertEquals(1, canonicalizer.projectSnapshotAccountAssets( + P66Phase.P66_ON, address, raw).size()); + assertTrue(canonicalizer.projectSnapshotAccountAssets( + P66Phase.P66_ON, address, on.getCanonicalValue()).isEmpty()); } @Test diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index e52704e24cd..9e0498abf6d 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -163,7 +163,7 @@ public void missingCurrentRebuildsExactNativeSnapshotAndAttaches() throws Except when(dynamic.getLatestBlockHeaderNumber()).thenReturn(blockNumber); when(dynamic.getLatestBlockHeaderHash()).thenReturn(blockId); when(dynamic.getLatestBlockHeaderTimestamp()).thenReturn(timestamp); - when(dynamic.getAllowSameTokenName()).thenReturn(1L); + when(dynamic.getAllowAccountAssetOptimizationFromRoot()).thenReturn(1L); BlockCapsule block = mock(BlockCapsule.class); when(block.getNum()).thenReturn(blockNumber); when(block.getBlockId()).thenReturn(blockId); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index 76d043897e6..dfd37a4e17f 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -163,6 +163,16 @@ public void admitsOnlyTargetP66AccountAssetPhysicalLayout() throws Exception { RebuildResult onResult = new PathStateRebuildCoordinator().rebuild(onManifest, on); assertEquals(1, onResult.requireStore("account").getEntryCount()); assertEquals(1, onResult.requireStore("account-asset").getEntryCount()); + + PathStateStoreManifest lazyManifest = manifest("p66-on-lazy", Engine.ROCKSDB); + TestSnapshotSource lazy = exactSource(identity(P66Phase.P66_ON)); + lazy.add("account", address, + account(address).toBuilder().putAssetV2(tokenId, 11L).build().toByteArray()); + RebuildResult lazyResult = new PathStateRebuildCoordinator().rebuild(lazyManifest, lazy); + assertEquals(1, lazyResult.requireStore("account").getEntryCount()); + assertEquals(0, lazyResult.requireStore("account-asset").getEntryCount()); + assertArrayEquals(onResult.getMetadata().getStateRoot(), + lazyResult.getMetadata().getStateRoot()); } @Test From 55b1b2b3eaaa5494edf888c82cbbdf7a5fa23102 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 29 Aug 2026 14:47:10 +0800 Subject: [PATCH 091/161] perf(db): reduce path state rebuild memory --- .../stateroot/PathStateNativeNodeStore.java | 73 +++---- .../db2/stateroot/PathStateNodeStoreSet.java | 182 +++++++++--------- .../core/db2/stateroot/PathStateRoot.java | 74 +++++++ .../PathStateNativeNodeStoreTest.java | 23 +++ .../core/db2/stateroot/PathStateRootTest.java | 17 ++ 5 files changed, 245 insertions(+), 124 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java index 7cfd7a46e87..327b2b87a5b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java @@ -70,24 +70,37 @@ synchronized void delete(byte[] key) { synchronized void writeBatch(List mutations) { requireOpen(); - List owned = new ArrayList<>(Objects.requireNonNull(mutations, "mutations")); - if (owned.isEmpty()) { + List supplied = Objects.requireNonNull(mutations, "mutations"); + if (supplied.isEmpty()) { throw new IllegalArgumentException("path-state native batch must not be empty"); } - for (int index = 0; index < owned.size(); index++) { - owned.set(index, new BatchMutation(Objects.requireNonNull(owned.get(index), "mutation"))); + for (BatchMutation mutation : supplied) { + Objects.requireNonNull(mutation, "mutation"); } - delegate.writeBatch(owned); + delegate.writeBatch(supplied); } - synchronized List scanPrefix(byte[] prefix) { + synchronized List scanPrefix(byte[] prefix) throws IOException { + List entries = new ArrayList<>(); + scanPrefix(prefix, entries::add); + return entries; + } + + synchronized List scanAll() throws IOException { + List entries = new ArrayList<>(); + scanAll(entries::add); + return entries; + } + + synchronized void scanPrefix(byte[] prefix, EntryConsumer consumer) throws IOException { requireOpen(); - return delegate.scanPrefix(nonEmpty(prefix, "prefix")); + delegate.scanPrefix(nonEmpty(prefix, "prefix"), + Objects.requireNonNull(consumer, "consumer")); } - synchronized List scanAll() { + synchronized void scanAll(EntryConsumer consumer) throws IOException { requireOpen(); - return delegate.scanAll(); + delegate.scanAll(Objects.requireNonNull(consumer, "consumer")); } Path getDirectory() { @@ -126,9 +139,9 @@ private interface Delegate extends Closeable { void writeBatch(List mutations); - List scanPrefix(byte[] prefix); + void scanPrefix(byte[] prefix, EntryConsumer consumer) throws IOException; - List scanAll(); + void scanAll(EntryConsumer consumer) throws IOException; } private static final class LevelDelegate implements Delegate { @@ -163,8 +176,7 @@ public void writeBatch(List mutations) { } @Override - public List scanPrefix(byte[] prefix) { - List entries = new ArrayList<>(); + public void scanPrefix(byte[] prefix, EntryConsumer consumer) throws IOException { try (org.iq80.leveldb.DBIterator iterator = database.iterator()) { iterator.seek(prefix); while (iterator.hasNext()) { @@ -172,27 +184,20 @@ public List scanPrefix(byte[] prefix) { if (!startsWith(entry.getKey(), prefix)) { break; } - entries.add(new KeyValue(entry.getKey(), entry.getValue())); + consumer.accept(new KeyValue(entry.getKey(), entry.getValue())); } - } catch (IOException failure) { - throw new IllegalStateException("failed to scan path-state LevelDB nodes", failure); } - return entries; } @Override - public List scanAll() { - List entries = new ArrayList<>(); + public void scanAll(EntryConsumer consumer) throws IOException { try (org.iq80.leveldb.DBIterator iterator = database.iterator()) { iterator.seekToFirst(); while (iterator.hasNext()) { Map.Entry entry = iterator.next(); - entries.add(new KeyValue(entry.getKey(), entry.getValue())); + consumer.accept(new KeyValue(entry.getKey(), entry.getValue())); } - } catch (IOException failure) { - throw new IllegalStateException("failed to scan all path-state LevelDB nodes", failure); } - return entries; } @Override @@ -245,35 +250,31 @@ public void writeBatch(List mutations) { } @Override - public List scanPrefix(byte[] prefix) { - List entries = new ArrayList<>(); + public void scanPrefix(byte[] prefix, EntryConsumer consumer) throws IOException { try (org.rocksdb.RocksIterator iterator = database.newIterator()) { iterator.seek(prefix); while (iterator.isValid() && startsWith(iterator.key(), prefix)) { - entries.add(new KeyValue(iterator.key(), iterator.value())); + consumer.accept(new KeyValue(iterator.key(), iterator.value())); iterator.next(); } iterator.status(); } catch (RocksDBException failure) { throw new IllegalStateException("failed to scan path-state RocksDB nodes", failure); } - return entries; } @Override - public List scanAll() { - List entries = new ArrayList<>(); + public void scanAll(EntryConsumer consumer) throws IOException { try (org.rocksdb.RocksIterator iterator = database.newIterator()) { iterator.seekToFirst(); while (iterator.isValid()) { - entries.add(new KeyValue(iterator.key(), iterator.value())); + consumer.accept(new KeyValue(iterator.key(), iterator.value())); iterator.next(); } iterator.status(); } catch (RocksDBException failure) { throw new IllegalStateException("failed to scan all path-state RocksDB nodes", failure); } - return entries; } @Override @@ -294,10 +295,6 @@ private BatchMutation(byte[] key, byte[] value) { this.value = value == null ? null : nonEmpty(value, "value"); } - private BatchMutation(BatchMutation mutation) { - this(mutation.key, mutation.value); - } - static BatchMutation put(byte[] key, byte[] value) { return new BatchMutation(key, Objects.requireNonNull(value, "value")); } @@ -326,6 +323,12 @@ byte[] getValue() { } } + @FunctionalInterface + interface EntryConsumer { + + void accept(KeyValue entry) throws IOException; + } + private static boolean startsWith(byte[] value, byte[] prefix) { return value.length >= prefix.length && Arrays.equals(Arrays.copyOf(value, prefix.length), prefix); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index 948aba11489..a6fd0491827 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -109,8 +109,10 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K loadPersistedLeaves(); loadLeafTombstones(); validateNodeTombstones(); + boolean hasUnexpectedLeaves = kind == Kind.BASE + ? !persistedLeaves.isEmpty() : !localLeaves.isEmpty(); if (progress == null && rebuildCheckpoint == null - && (!localLeaves.isEmpty() || !leafTombstones.isEmpty())) { + && (hasUnexpectedLeaves || !leafTombstones.isEmpty())) { throw new IOException("path-state leaf inventory exists without native progress"); } for (PathStateParticipant participant : scope.getParticipants()) { @@ -282,6 +284,9 @@ synchronized PathStateRoot createRootFrom(PreparedPathStateTransition prepared) PathStateRoot next = PathStateRoot.fromSnapshot(scope, participant -> participantStores.get(participant.getDbName()), superStore, candidate.getSnapshot()); + if (!candidate.getTransition().getMutations().isEmpty()) { + next.recordPendingLeafMutations(candidate.getTransition().getMutations()); + } for (PreparedPathStateTransition.NodeMutation mutation : candidate.getNodeMutations()) { PathNodeStore store = nodeStore(mutation.getStoreId()); byte[] encoded = mutation.getEncodedNode(); @@ -313,7 +318,9 @@ synchronized PathStateRoot initializeBase(List leaves, } PathStateRoot candidate = new PathStateRoot(scope, participant -> participantStores.get(participant.getDbName()), superStore); - candidate.initializeLeaves(Objects.requireNonNull(leaves, "leaves"), expectedRoot); + List admittedLeaves = Objects.requireNonNull(leaves, "leaves"); + candidate.initializeLeaves(admittedLeaves, expectedRoot); + candidate.recordPendingLeafRecords(admittedLeaves); root = candidate; rootClaimed = true; return candidate; @@ -330,11 +337,11 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti } PathStateRootMetadata next = Objects.requireNonNull(metadata, "metadata"); long nextLogicalBytes = projectedLogicalBytes(next); + List leafMutations = root.pendingLeafMutations(); List mutations = - new ArrayList<>(pending.size() + persistedLeaves.size() + 1); + new ArrayList<>(pending.size() + leafMutations.size() + 4); appendPendingMutations(mutations); - Map nextLeaves = leafMap(root.leafRecords()); - appendLeafMutations(mutations, nextLeaves); + appendLeafMutations(mutations, leafMutations); mutations.add(PathStateNativeNodeStore.BatchMutation.put(PROGRESS_KEY, next.encode())); if (leafOverlay) { mutations.add(PathStateNativeNodeStore.BatchMutation.put( @@ -347,7 +354,8 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti } nativeStore.writeBatch(mutations); pending.clear(); - recordCommittedLeaves(nextLeaves); + recordCommittedLeaves(leafMutations); + root.clearPendingLeafMutations(); progress = next; rebuildCheckpoint = null; logicalBytes = nextLogicalBytes; @@ -379,11 +387,13 @@ synchronized void checkpointRebuild(PathStateRebuildCheckpoint checkpoint) throw } } } + List leafMutations = root.pendingLeafMutations(); List mutations = - durableStateMutations(next.encode()); + durableStateMutations(next.encode(), leafMutations); nativeStore.writeBatch(mutations); pending.clear(); - recordCommittedLeaves(leafMap(root.leafRecords())); + recordCommittedLeaves(leafMutations); + root.clearPendingLeafMutations(); rebuildCheckpoint = next; } @@ -405,8 +415,7 @@ synchronized long projectedLogicalBytes(PathStateRootMetadata metadata) throws I long total = rebuildCheckpoint == null ? (logicalBytes == null ? 0 : logicalBytes) : rebuildLogicalBytes(); total = projectedPendingBytes(total); - Map nextLeaves = leafMap(root.leafRecords()); - total = projectedLeafBytes(total, nextLeaves); + total = projectedLeafBytes(total, root.pendingLeafMutations()); if (leafOverlay) { total = replaceLogicalEntry(total, LEAF_OVERLAY_KEY, nativeStore.get(LEAF_OVERLAY_KEY), LEAF_OVERLAY_VALUE); @@ -452,19 +461,19 @@ static Long loadLogicalBytes(Path ownerDirectory, PathStateStoreManifest manifes if (expected == null) { return null; } - long actual = 0; + long[] actual = new long[]{0}; try { - for (PathStateNativeNodeStore.KeyValue entry : store.scanAll()) { + store.scanAll(entry -> { byte[] key = entry.getKey(); if (!Arrays.equals(key, LOGICAL_BYTES_KEY)) { - actual = Math.addExact(actual, + actual[0] = Math.addExact(actual[0], Math.addExact(key.length, entry.getValue().length)); } - } + }); } catch (ArithmeticException overflow) { throw new IOException("path-state logical bytes verification overflow", overflow); } - if (actual != expected) { + if (actual[0] != expected) { throw new IOException("path-state logical bytes marker does not match native entries"); } return expected; @@ -566,7 +575,7 @@ private void requireProgressIdentity(PathStateRootMetadata metadata) throws IOEx } private void loadPersistedLeaves() throws IOException { - for (PathStateNativeNodeStore.KeyValue entry : nativeStore.scanPrefix(LEAF_PREFIX)) { + nativeStore.scanPrefix(LEAF_PREFIX, entry -> { byte[] key = entry.getKey(); if (key.length != LEAF_KEY_LENGTH || ByteBuffer.wrap(key).getInt() != LEAF_DOMAIN) { throw new IOException("path-state durable leaf key is malformed"); @@ -574,11 +583,16 @@ private void loadPersistedLeaves() throws IOException { int storeId = ByteBuffer.wrap(key, Integer.BYTES, Integer.BYTES).getInt(); requireParticipant(storeId); BytesKey leafKey = new BytesKey(key); - if (localLeaves.put(leafKey, entry.getValue()) != null) { + byte[] value = entry.getValue(); + if (kind == Kind.LAYER) { + if (localLeaves.put(leafKey, value) != null) { + throw new IOException("duplicate path-state durable leaf key"); + } + persistedLeaves.put(leafKey, value); + } else if (persistedLeaves.put(leafKey, value) != null) { throw new IOException("duplicate path-state durable leaf key"); } - persistedLeaves.put(leafKey, entry.getValue()); - } + }); } private void inheritParentLeaves() { @@ -591,12 +605,10 @@ private void inheritParentLeaves() { } private void loadLeafTombstones() throws IOException { - List tombstones = - nativeStore.scanPrefix(LEAF_TOMBSTONE_PREFIX); - if (!tombstones.isEmpty() && (!leafOverlay || kind != Kind.LAYER || progress == null)) { - throw new IOException("path-state leaf tombstones require durable LAYER progress"); - } - for (PathStateNativeNodeStore.KeyValue entry : tombstones) { + nativeStore.scanPrefix(LEAF_TOMBSTONE_PREFIX, entry -> { + if (!leafOverlay || kind != Kind.LAYER || progress == null) { + throw new IOException("path-state leaf tombstones require durable LAYER progress"); + } byte[] key = entry.getKey(); if (key.length != LEAF_KEY_LENGTH || ByteBuffer.wrap(key).getInt() != LEAF_TOMBSTONE_DOMAIN @@ -614,16 +626,14 @@ private void loadLeafTombstones() throws IOException { } leafTombstones.add(leafKey); persistedLeaves.remove(leafKey); - } + }); } private void validateNodeTombstones() throws IOException { - List tombstones = - nativeStore.scanPrefix(NODE_TOMBSTONE_PREFIX); - if (!tombstones.isEmpty() && (kind != Kind.LAYER || progress == null)) { - throw new IOException("path-state node tombstones require durable LAYER progress"); - } - for (PathStateNativeNodeStore.KeyValue entry : tombstones) { + nativeStore.scanPrefix(NODE_TOMBSTONE_PREFIX, entry -> { + if (kind != Kind.LAYER || progress == null) { + throw new IOException("path-state node tombstones require durable LAYER progress"); + } byte[] key = entry.getKey(); if (key.length < Integer.BYTES * 2 || ByteBuffer.wrap(key).getInt() != NODE_TOMBSTONE_DOMAIN @@ -643,31 +653,31 @@ private void validateNodeTombstones() throws IOException { if (nativeStore.get(nodeKey) != null) { throw new IOException("path-state node and tombstone coexist"); } - } + }); } private List durableStateMutations( - byte[] rebuildValue) { + byte[] rebuildValue, List leafMutations) { List mutations = - new ArrayList<>(pending.size() + persistedLeaves.size() + 1); + new ArrayList<>(pending.size() + leafMutations.size() + 1); appendPendingMutations(mutations); - Map nextLeaves = leafMap(root.leafRecords()); - appendLeafMutations(mutations, nextLeaves); + appendLeafMutations(mutations, leafMutations); mutations.add(PathStateNativeNodeStore.BatchMutation.put(REBUILD_CHECKPOINT_KEY, rebuildValue)); return mutations; } private long rebuildLogicalBytes() throws IOException { - long total = 0; + long[] total = new long[]{0}; try { - for (PathStateNativeNodeStore.KeyValue entry : nativeStore.scanAll()) { + nativeStore.scanAll(entry -> { byte[] key = entry.getKey(); if (!Arrays.equals(key, LOGICAL_BYTES_KEY)) { - total = Math.addExact(total, Math.addExact(key.length, entry.getValue().length)); + total[0] = Math.addExact(total[0], + Math.addExact(key.length, entry.getValue().length)); } - } - return total; + }); + return total[0]; } catch (ArithmeticException overflow) { throw new IOException("path-state rebuild logical bytes overflow", overflow); } @@ -701,22 +711,6 @@ private List restoredLeafRecords() { return records; } - private Map leafMap(List records) { - Map leaves = new LinkedHashMap<>(); - for (PathStateRoot.LeafRecord record : records) { - requireParticipant(record.getStoreId()); - BytesKey key = new BytesKey(ByteBuffer.allocate(LEAF_KEY_LENGTH) - .putInt(LEAF_DOMAIN) - .putInt(record.getStoreId()) - .put(record.getSecureKey()) - .array()); - if (leaves.put(key, record.getEncodedValue()) != null) { - throw new IllegalStateException("duplicate path-state durable leaf key"); - } - } - return leaves; - } - private PathStateParticipant requireParticipant(int storeId) { for (PathStateParticipant participant : scope.getParticipants()) { if (participant.getStoreId() == storeId) { @@ -790,15 +784,12 @@ private void appendPendingMutations( } private void appendLeafMutations(List mutations, - Map nextLeaves) { - for (BytesKey persisted : persistedLeaves.keySet()) { - if (!nextLeaves.containsKey(persisted)) { - appendLeafMutation(mutations, persisted.copy(), null); - } - } - for (Map.Entry entry : nextLeaves.entrySet()) { - if (!Arrays.equals(persistedLeaves.get(entry.getKey()), entry.getValue())) { - appendLeafMutation(mutations, entry.getKey().copy(), entry.getValue()); + List leafMutations) { + for (PathStateRoot.LeafMutationRecord mutation : leafMutations) { + byte[] key = leafKey(mutation); + byte[] value = mutation.getEncodedValue(); + if (!Arrays.equals(persistedLeaves.get(new BytesKey(key)), value)) { + appendLeafMutation(mutations, key, value); } } } @@ -816,17 +807,15 @@ private void appendLeafMutation(List mut } } - private long projectedLeafBytes(long total, Map nextLeaves) + private long projectedLeafBytes(long total, + List leafMutations) throws IOException { long projected = total; - for (BytesKey persisted : persistedLeaves.keySet()) { - if (!nextLeaves.containsKey(persisted)) { - projected = projectedLeafMutation(projected, persisted.copy(), null); - } - } - for (Map.Entry entry : nextLeaves.entrySet()) { - if (!Arrays.equals(persistedLeaves.get(entry.getKey()), entry.getValue())) { - projected = projectedLeafMutation(projected, entry.getKey().copy(), entry.getValue()); + for (PathStateRoot.LeafMutationRecord mutation : leafMutations) { + byte[] key = leafKey(mutation); + byte[] value = mutation.getEncodedValue(); + if (!Arrays.equals(persistedLeaves.get(new BytesKey(key)), value)) { + projected = projectedLeafMutation(projected, key, value); } } return projected; @@ -842,23 +831,38 @@ private long projectedLeafMutation(long total, byte[] key, byte[] value) throws return projected; } - private void recordCommittedLeaves(Map nextLeaves) { - for (BytesKey persisted : persistedLeaves.keySet()) { - if (!nextLeaves.containsKey(persisted)) { - localLeaves.remove(persisted); + private void recordCommittedLeaves(List leafMutations) { + for (PathStateRoot.LeafMutationRecord mutation : leafMutations) { + BytesKey key = new BytesKey(leafKey(mutation)); + byte[] value = mutation.getEncodedValue(); + if (Arrays.equals(persistedLeaves.get(key), value)) { + continue; + } + if (value == null) { + persistedLeaves.remove(key); + if (kind == Kind.LAYER) { + localLeaves.remove(key); + } if (leafOverlay) { - leafTombstones.add(persisted); + leafTombstones.add(key); } + } else if (!Arrays.equals(persistedLeaves.get(key), value)) { + byte[] owned = Arrays.copyOf(value, value.length); + persistedLeaves.put(key, owned); + if (kind == Kind.LAYER) { + localLeaves.put(key, owned); + } + leafTombstones.remove(key); } } - for (Map.Entry entry : nextLeaves.entrySet()) { - if (!Arrays.equals(persistedLeaves.get(entry.getKey()), entry.getValue())) { - localLeaves.put(entry.getKey(), Arrays.copyOf(entry.getValue(), entry.getValue().length)); - leafTombstones.remove(entry.getKey()); - } - } - persistedLeaves.clear(); - persistedLeaves.putAll(nextLeaves); + } + + private static byte[] leafKey(PathStateRoot.LeafMutationRecord mutation) { + return ByteBuffer.allocate(LEAF_KEY_LENGTH) + .putInt(LEAF_DOMAIN) + .putInt(mutation.getStoreId()) + .put(mutation.getSecureKey()) + .array(); } private long projectedPendingBytes(long total) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index 338529a328b..accaff7db9c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -31,6 +31,8 @@ public final class PathStateRoot { private final PathStateParticipantScope scope; private final Map participantTries = new LinkedHashMap<>(); + private final Map pendingLeafMutations = + new LinkedHashMap<>(); private final PathMerkleTrie superTrie; private boolean rootMaterialized; @@ -92,9 +94,21 @@ public synchronized void apply(Collection mutations) { trie.put(mutation.secureKey, mutation.encodedValue); } } + recordPendingLeafMutations(prepared); rootMaterialized = false; } + synchronized void recordPendingLeafMutations(Collection mutations) { + recordPendingLeafMutations(prepare(mutations)); + } + + private void recordPendingLeafMutations(List prepared) { + for (PreparedMutation mutation : prepared) { + MutationKey key = new MutationKey(mutation.participant.getStoreId(), mutation.secureKey); + pendingLeafMutations.put(key, mutation.participant); + } + } + public synchronized byte[] participantRoot(String dbName) { PathStateParticipant participant = scope.require(dbName); return participantTries.get(participant.getDbName()).rootHash(); @@ -139,6 +153,30 @@ synchronized List leafRecords() { return records; } + /** Returns only leaf mutations accumulated since the last durable commit/checkpoint. */ + synchronized List pendingLeafMutations() { + List mutations = new ArrayList<>(pendingLeafMutations.size()); + for (Map.Entry entry : pendingLeafMutations.entrySet()) { + MutationKey key = entry.getKey(); + PathStateParticipant participant = entry.getValue(); + mutations.add(new LeafMutationRecord(participant.getStoreId(), key.secureKey, + participantTries.get(participant.getDbName()).get(key.secureKey))); + } + return mutations; + } + + synchronized void clearPendingLeafMutations() { + pendingLeafMutations.clear(); + } + + synchronized void recordPendingLeafRecords(Collection records) { + for (LeafRecord record : Objects.requireNonNull(records, "records")) { + LeafRecord present = Objects.requireNonNull(record, "record"); + PathStateParticipant participant = participant(present.storeId); + pendingLeafMutations.put(new MutationKey(present.storeId, present.secureKey), participant); + } + } + synchronized Snapshot snapshot() { byte[] stateRoot = rootHash(); Map snapshots = new LinkedHashMap<>(); @@ -231,6 +269,15 @@ private List prepare(Collection mutations) return prepared; } + private PathStateParticipant participant(int storeId) { + for (PathStateParticipant participant : scope.getParticipants()) { + if (participant.getStoreId() == storeId) { + return participant; + } + } + throw new IllegalArgumentException("unknown path-state Store ID: " + storeId); + } + private static int compareUnsigned(byte[] left, byte[] right) { for (int i = 0; i < Math.min(left.length, right.length); i++) { int result = Integer.compare(left[i] & 0xff, right[i] & 0xff); @@ -292,6 +339,33 @@ byte[] getEncodedValue() { } } + static final class LeafMutationRecord { + + private final int storeId; + private final byte[] secureKey; + private final byte[] encodedValue; + + private LeafMutationRecord(int storeId, byte[] secureKey, byte[] encodedValue) { + this.storeId = storeId; + this.secureKey = Arrays.copyOf(Objects.requireNonNull(secureKey, "secureKey"), + secureKey.length); + this.encodedValue = encodedValue == null ? null + : Arrays.copyOf(encodedValue, encodedValue.length); + } + + int getStoreId() { + return storeId; + } + + byte[] getSecureKey() { + return Arrays.copyOf(secureKey, secureKey.length); + } + + byte[] getEncodedValue() { + return encodedValue == null ? null : Arrays.copyOf(encodedValue, encodedValue.length); + } + } + private static final class PreparedMutation { private final PathStateParticipant participant; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index 627af275a92..e829958735c 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -9,9 +9,11 @@ import java.io.File; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import org.junit.Rule; import org.junit.Test; @@ -60,6 +62,27 @@ public void nativeStoreRejectsInvalidEntriesAndUseAfterClose() throws Exception assertThrows(IllegalStateException.class, () -> store.get(new byte[0])); } + @Test + public void streamsNativeScansWithoutCollectingTheResultSet() throws Exception { + for (Engine engine : availableEngines()) { + Path directory = new File(temporaryFolder.getRoot(), "stream-" + engine).toPath(); + List mutations = new ArrayList<>(); + for (int index = 0; index < 64; index++) { + mutations.add(PathStateNativeNodeStore.BatchMutation.put( + new byte[]{1, (byte) index}, new byte[]{(byte) (index + 1)})); + } + try (PathStateNativeNodeStore store = PathStateNativeNodeStore.open(directory, engine)) { + store.writeBatch(mutations); + AtomicInteger prefixCount = new AtomicInteger(); + AtomicInteger allCount = new AtomicInteger(); + store.scanPrefix(new byte[]{1}, entry -> prefixCount.incrementAndGet()); + store.scanAll(entry -> allCount.incrementAndGet()); + assertEquals(64, prefixCount.get()); + assertEquals(64, allCount.get()); + } + } + } + @Test public void baseStoreSetCreatesExact27PlusSuperAndPersistsRootNodes() throws Exception { PathStateStoreManifest manifest = manifest("base-set", Engine.ROCKSDB); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java index 98b5146129b..c1127af6af8 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java @@ -1,6 +1,7 @@ package org.tron.core.db2.stateroot; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import java.util.ArrayList; @@ -146,6 +147,22 @@ public void emptyAndZeroValuesProduceDifferentRoots() { org.junit.Assert.assertFalse(Arrays.equals(empty.rootHash(), zero.rootHash())); } + @Test + public void retainsOnlyLeafDeltaSinceLastDurableBoundary() { + PathStateRoot stateRoot = stateRoot(participants()); + stateRoot.put("abi", bytes("one"), bytes("first")); + stateRoot.put("account", bytes("two"), bytes("second")); + assertEquals(2, stateRoot.pendingLeafMutations().size()); + + stateRoot.clearPendingLeafMutations(); + assertEquals(0, stateRoot.pendingLeafMutations().size()); + stateRoot.put("abi", bytes("one"), bytes("third")); + stateRoot.put("abi", bytes("one"), bytes("fourth")); + stateRoot.delete("account", bytes("two")); + + assertEquals(2, stateRoot.pendingLeafMutations().size()); + } + @Test public void verificationRequiresCurrentMaterializedSuperRoot() { PathStateRoot stateRoot = stateRoot(participants()); From d61d1ee29c82facc1217e18eee86fdb6a12da532 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sun, 30 Aug 2026 00:11:57 +0800 Subject: [PATCH 092/161] perf(chainbase): avoid path state cross-store reads Remove per-row Account lookups from AccountAsset rebuilds and narrow the snapshot source to sequential Store scans. Keep row-local canonical validation while avoiding hundreds of millions of random reads during initial root construction. --- .../db2/stateroot/PathStateCanonicalizer.java | 25 ----------------- .../PathStateNativeSnapshotSource.java | 12 --------- .../PathStateRebuildCoordinator.java | 25 +++-------------- .../PathStateRebuildCoordinatorTest.java | 27 +++++++------------ 4 files changed, 13 insertions(+), 76 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java index 861b448519b..6d564a52ed4 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCanonicalizer.java @@ -136,31 +136,6 @@ public List projectSnapshotAccountAssets(P66Phase phase, return Collections.unmodifiableList(projected); } - /** Requires that a physical direct row belongs to an already optimized Account. */ - public void requirePhysicalAccountAssetOwner(P66Phase phase, byte[] physicalKey, - byte[] rawValue) { - P66Phase target = Objects.requireNonNull(phase, "phase"); - byte[] key = copy(physicalKey, "physicalKey"); - requireLength(key, ADDRESS_LENGTH, "account key"); - Account account = parseAccount(key, rawValue); - if (!target.directAssetsEnabled() || !account.getAssetOptimized() - || !account.getAssetMap().isEmpty() || !account.getAssetV2Map().isEmpty()) { - throw new IllegalArgumentException( - "physical AccountAsset row requires an optimized owning Account"); - } - } - - /** Extracts and validates the owning Account address from one direct physical key. */ - public byte[] accountAddressFromAssetKey(P66Phase phase, byte[] physicalKey) { - P66Phase target = Objects.requireNonNull(phase, "phase"); - if (!target.directAssetsEnabled()) { - throw new IllegalArgumentException("P66-off state must not contain account-asset rows"); - } - byte[] key = copyNonEmpty(physicalKey, "physicalKey"); - decodeAccountAssetKey(key); - return Arrays.copyOf(key, ADDRESS_LENGTH); - } - private static void configure(Map formats, String dbName, String codecId) { if (formats.replace(dbName, new StoreFormat(dbName, codecId)) == null) { throw new IllegalStateException("missing path-state format participant: " + dbName); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java index b3833c6f6e9..5f64fc60dbd 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java @@ -162,18 +162,6 @@ public byte[] sourceIdentityDigest() { return Arrays.copyOf(sourceIdentityDigest, sourceIdentityDigest.length); } - @Override - public synchronized byte[] get(String dbName, byte[] physicalKey) throws IOException { - ensureOpen(); - descriptor.require(dbName); - StoreSnapshot snapshot = snapshots.get(dbName); - if (snapshot == null) { - throw new IOException("database is outside pinned path-state snapshot: " + dbName); - } - byte[] value = snapshot.get(copy(physicalKey, "physicalKey")); - return value == null ? null : Arrays.copyOf(value, value.length); - } - @Override public synchronized void scan(String dbName, EntryConsumer consumer) throws IOException { ensureOpen(); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index ae8cdd83d77..a5d575749b8 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -98,8 +98,7 @@ private RebuildResult rebuildInternal(PathStateStoreManifest manifest, SnapshotS } for (int index = storeResults.size(); index < descriptor.getStores().size(); index++) { StoreIdentity store = descriptor.getStores().get(index); - StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root, - admittedSource); + StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root); admittedSource.scan(store.getDbName(), accumulator::accept); StoreResult result = accumulator.finish(); storeResults.add(result); @@ -157,17 +156,14 @@ private final class StoreAccumulator { private final StoreIdentity store; private final P66Phase phase; private final PathStateRoot root; - private final SnapshotSource source; private final Hasher inputDigest; private byte[] previousKey; private long entryCount; - private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root, - SnapshotSource source) { + private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root) { this.store = store; this.phase = phase; this.root = root; - this.source = source; inputDigest = domainHasher(STORE_DIGEST_DOMAIN); putInt(inputDigest, store.getStoreId()); putString(inputDigest, store.getDbName()); @@ -183,7 +179,7 @@ private void accept(byte[] physicalKey, byte[] rawValue) throws IOException { throw new IllegalArgumentException( "path-state snapshot keys are not strictly increasing: " + store.getDbName()); } - validateAccountAssetLayout(key, value); + validateAccountLayout(key, value); PathStateMutation mutation = canonicalizer.put(phase, store.getDbName(), key, value); root.apply(Collections.singletonList(mutation)); if ("account".equals(store.getDbName())) { @@ -199,20 +195,10 @@ private void accept(byte[] physicalKey, byte[] rawValue) throws IOException { entryCount = Math.addExact(entryCount, 1L); } - private void validateAccountAssetLayout(byte[] key, byte[] value) throws IOException { + private void validateAccountLayout(byte[] key, byte[] value) { if ("account".equals(store.getDbName())) { canonicalizer.requireSnapshotAccountLayout(phase, key, value); - return; } - if (!"account-asset".equals(store.getDbName())) { - return; - } - byte[] accountKey = canonicalizer.accountAddressFromAssetKey(phase, key); - byte[] accountValue = source.get("account", accountKey); - if (accountValue == null) { - throw new IOException("path-state AccountAsset row has no owning Account"); - } - canonicalizer.requirePhysicalAccountAssetOwner(phase, accountKey, accountValue); } private StoreResult finish() { @@ -273,9 +259,6 @@ public interface SnapshotSource { /** Stable identity of the exact physical Store generations held by this snapshot. */ byte[] sourceIdentityDigest(); - /** Returns one value from the same pinned snapshot, or {@code null} when physically absent. */ - byte[] get(String dbName, byte[] physicalKey) throws IOException; - void scan(String dbName, EntryConsumer consumer) throws IOException; void verifyIdentity(SnapshotIdentity expected) throws IOException; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index dfd37a4e17f..a93fa55ab33 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -176,7 +176,7 @@ public void admitsOnlyTargetP66AccountAssetPhysicalLayout() throws Exception { } @Test - public void rejectsMixedOrOrphanAccountAssetSnapshotWithoutPublication() throws Exception { + public void buildsAccountAssetRowsWithoutCrossStoreOwnerValidation() throws Exception { byte[] address = address(8); String tokenId = "1000001"; @@ -185,17 +185,18 @@ public void rejectsMixedOrOrphanAccountAssetSnapshotWithoutPublication() throws mixed.add("account", address, account(address).toBuilder().putAssetV2(tokenId, 9L).build().toByteArray()); mixed.add("account-asset", accountAssetKey(address, tokenId), longBytes(9L)); - assertThrows(IllegalArgumentException.class, - () -> new PathStateRebuildCoordinator().rebuild(mixedManifest, mixed)); - assertFalse(new PathStateCurrentStore(mixedManifest).isInitialized()); + RebuildResult mixedResult = new PathStateRebuildCoordinator() + .rebuild(mixedManifest, mixed); + assertEquals(1, mixedResult.requireStore("account-asset").getEntryCount()); + assertTrue(new PathStateCurrentStore(mixedManifest).isInitialized()); PathStateStoreManifest orphanManifest = manifest("p66-orphan", Engine.ROCKSDB); TestSnapshotSource orphan = exactSource(identity(P66Phase.P66_ON)); orphan.add("account-asset", accountAssetKey(address, tokenId), longBytes(10L)); - IOException orphanFailure = assertThrows(IOException.class, - () -> new PathStateRebuildCoordinator().rebuild(orphanManifest, orphan)); - assertTrue(orphanFailure.getMessage().contains("no owning Account")); - assertFalse(new PathStateCurrentStore(orphanManifest).isInitialized()); + RebuildResult orphanResult = new PathStateRebuildCoordinator() + .rebuild(orphanManifest, orphan); + assertEquals(1, orphanResult.requireStore("account-asset").getEntryCount()); + assertTrue(new PathStateCurrentStore(orphanManifest).isInitialized()); PathStateStoreManifest offDirectManifest = manifest("p66-off-direct", Engine.ROCKSDB); TestSnapshotSource offDirect = exactSource(identity(P66Phase.P66_OFF)); @@ -554,16 +555,6 @@ public byte[] sourceIdentityDigest() { return java.util.Arrays.copyOf(sourceIdentityDigest, sourceIdentityDigest.length); } - @Override - public byte[] get(String dbName, byte[] physicalKey) { - for (Row row : stores.get(dbName)) { - if (java.util.Arrays.equals(row.key, physicalKey)) { - return java.util.Arrays.copyOf(row.value, row.value.length); - } - } - return null; - } - @Override public void scan(String dbName, EntryConsumer consumer) throws IOException { scanCounts.put(dbName, getScanCount(dbName) + 1); From 121b91cb3d9642fe6aad2e4a1f1a210be72ea923 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sun, 30 Aug 2026 00:49:45 +0800 Subject: [PATCH 093/161] perf(chainbase): parallelize state root rebuild stores --- .../stateroot/PathStateNativeNodeStore.java | 4 +- .../PathStateNativeSnapshotSource.java | 4 +- .../db2/stateroot/PathStateNodeStoreSet.java | 171 ++++++++++++++---- .../stateroot/PathStateRebuildCheckpoint.java | 41 ++++- .../PathStateRebuildCoordinator.java | 165 +++++++++++++++-- .../core/db2/stateroot/PathStateRoot.java | 132 +++++++++++--- .../PathStateRebuildCoordinatorTest.java | 126 ++++++++++++- 7 files changed, 548 insertions(+), 95 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java index 327b2b87a5b..a04cc8957c1 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java @@ -29,7 +29,7 @@ final class PathStateNativeNodeStore implements Closeable { private final Path directory; private final Engine engine; private final Delegate delegate; - private boolean closed; + private volatile boolean closed; private PathStateNativeNodeStore(Path directory, Engine engine, Delegate delegate) { this.directory = directory; @@ -53,7 +53,7 @@ static PathStateNativeNodeStore open(Path directory, Engine engine) throws IOExc return new PathStateNativeNodeStore(path, selected, opened); } - synchronized byte[] get(byte[] key) { + byte[] get(byte[] key) { requireOpen(); byte[] ownedKey = nonEmpty(key, "key"); byte[] value = delegate.get(ownedKey); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java index 5f64fc60dbd..88d71e63f68 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java @@ -37,7 +37,7 @@ public final class PathStateNativeSnapshotSource private final Map snapshots; private final int pageSize; private final int marketEntryLimit; - private boolean closed; + private volatile boolean closed; private PathStateNativeSnapshotSource(PathStateParticipantDescriptor descriptor, SnapshotIdentity identity, Map stores, @@ -163,7 +163,7 @@ public byte[] sourceIdentityDigest() { } @Override - public synchronized void scan(String dbName, EntryConsumer consumer) throws IOException { + public void scan(String dbName, EntryConsumer consumer) throws IOException { ensureOpen(); Objects.requireNonNull(consumer, "consumer"); PathStateParticipantDescriptor.StoreIdentity participant = descriptor.require(dbName); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index a6fd0491827..75406a3f1b0 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -8,6 +8,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -48,7 +49,7 @@ public final class PathStateNodeStoreSet implements Closeable { private final Path directory; private final PathStateParticipantScope scope; private final Map participantStores = new LinkedHashMap<>(); - private final Map pending = new LinkedHashMap<>(); + private final Map> pending = new LinkedHashMap<>(); private final Map localLeaves = new LinkedHashMap<>(); private final Map persistedLeaves = new LinkedHashMap<>(); private final Set leafTombstones = new LinkedHashSet<>(); @@ -116,9 +117,11 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K throw new IOException("path-state leaf inventory exists without native progress"); } for (PathStateParticipant participant : scope.getParticipants()) { + pending.put(participant.getStoreId(), new LinkedHashMap<>()); participantStores.put(participant.getDbName(), new NamespacedNodeStore(this, participant.getStoreId())); } + pending.put(0, new LinkedHashMap<>()); superStore = new NamespacedNodeStore(this, 0); } catch (RuntimeException | IOException failure) { nativeStore.close(); @@ -210,10 +213,14 @@ public synchronized PathStateRoot createRoot() { participant -> participantStores.get(participant.getDbName()), superStore); if (progress != null || rebuildCheckpoint != null) { - byte[] expectedRoot = progress == null ? rebuildCheckpoint.getPartialRoot() - : progress.getStateRoot(); - candidate.restoreLeaves(restoredLeafRecords(), expectedRoot); - if (!pending.isEmpty()) { + if (progress == null && rebuildCheckpoint.hasIndependentStores()) { + candidate.restoreRebuildLeaves(restoredLeafRecords(), rebuildCheckpoint); + } else { + byte[] expectedRoot = progress == null ? rebuildCheckpoint.getPartialRoot() + : progress.getStateRoot(); + candidate.restoreLeaves(restoredLeafRecords(), expectedRoot); + } + if (hasPending()) { throw new IllegalStateException("path-state leaf restoration attempted to repair nodes"); } } @@ -237,7 +244,7 @@ synchronized PathStateRoot createRootFrom(List parentL throw new IllegalStateException("path-state layer has no parent node overlay"); } candidate.restoreLeaves(parentLeaves, parentRoot); - if (!pending.isEmpty()) { + if (hasPending()) { throw new IllegalStateException("path-state parent restore attempted to copy nodes"); } root = candidate; @@ -261,7 +268,7 @@ synchronized PathStateRoot createRootFrom(PathStateRoot.Snapshot snapshot, } PathStateRoot candidate = PathStateRoot.fromSnapshot(scope, participant -> participantStores.get(participant.getDbName()), superStore, snapshot); - if (!pending.isEmpty()) { + if (hasPending()) { throw new IllegalStateException("path-state snapshot fork attempted to copy nodes"); } root = candidate; @@ -339,7 +346,7 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti long nextLogicalBytes = projectedLogicalBytes(next); List leafMutations = root.pendingLeafMutations(); List mutations = - new ArrayList<>(pending.size() + leafMutations.size() + 4); + new ArrayList<>(pendingSize() + leafMutations.size() + 4); appendPendingMutations(mutations); appendLeafMutations(mutations, leafMutations); mutations.add(PathStateNativeNodeStore.BatchMutation.put(PROGRESS_KEY, next.encode())); @@ -353,7 +360,7 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti mutations.add(PathStateNativeNodeStore.BatchMutation.delete(REBUILD_CHECKPOINT_KEY)); } nativeStore.writeBatch(mutations); - pending.clear(); + clearPending(); recordCommittedLeaves(leafMutations); root.clearPendingLeafMutations(); progress = next; @@ -362,38 +369,52 @@ public synchronized void commit(PathStateRootMetadata metadata) throws IOExcepti } /** Persists one more completed rebuild Store without creating BASE authority. */ - synchronized void checkpointRebuild(PathStateRebuildCheckpoint checkpoint) throws IOException { + synchronized void checkpointRebuild(PathStateRebuildCheckpoint checkpoint, + Collection participantStoreIds) throws IOException { requireOpen(); if (kind != Kind.BASE || sealed || progress != null || root == null) { throw new IOException("path-state rebuild checkpoint is not admissible"); } PathStateRebuildCheckpoint next = Objects.requireNonNull(checkpoint, "checkpoint"); requireRebuildCheckpointIdentity(next); - if (!Arrays.equals(root.rootHash(), next.getPartialRoot())) { - throw new IOException("path-state rebuild checkpoint root mismatch"); - } int previousCount = rebuildCheckpoint == null ? 0 : rebuildCheckpoint.getCompletedStores().size(); if (next.getCompletedStores().size() != previousCount + 1) { throw new IOException("path-state rebuild checkpoint must advance one Store"); } if (rebuildCheckpoint != null) { - List previous = - rebuildCheckpoint.getCompletedStores(); - List advanced = next.getCompletedStores(); - for (int index = 0; index < previous.size(); index++) { - if (!sameStoreResult(previous.get(index), advanced.get(index))) { + for (PathStateRebuildCoordinator.StoreResult previous + : rebuildCheckpoint.getCompletedStores()) { + boolean retained = false; + for (PathStateRebuildCoordinator.StoreResult advanced : next.getCompletedStores()) { + if (sameStoreResult(previous, advanced)) { + retained = true; + break; + } + } + if (!retained) { throw new IOException("path-state rebuild checkpoint rewrites completed Store"); } } } - List leafMutations = root.pendingLeafMutations(); + Set storeIds = new LinkedHashSet<>(Objects.requireNonNull(participantStoreIds, + "participantStoreIds")); + if (storeIds.isEmpty() || storeIds.contains(0)) { + throw new IOException("path-state rebuild checkpoint Store ownership is invalid"); + } + List leafMutations = new ArrayList<>(); + for (Integer storeId : storeIds) { + requireParticipant(storeId); + leafMutations.addAll(root.pendingLeafMutations(storeId)); + } List mutations = - durableStateMutations(next.encode(), leafMutations); + durableStateMutations(next.encode(), leafMutations, storeIds); nativeStore.writeBatch(mutations); - pending.clear(); + for (Integer storeId : storeIds) { + clearPending(storeId); + root.clearPendingLeafMutations(storeId); + } recordCommittedLeaves(leafMutations); - root.clearPendingLeafMutations(); rebuildCheckpoint = next; } @@ -526,11 +547,14 @@ private void requireOpen() { } } - private synchronized byte[] get(byte[] key) { + private byte[] get(byte[] key) { BytesKey ownedKey = new BytesKey(key); - if (pending.containsKey(ownedKey)) { - byte[] value = pending.get(ownedKey); - return value == null ? null : Arrays.copyOf(value, value.length); + Map participantPending = pending(key); + synchronized (participantPending) { + if (participantPending.containsKey(ownedKey)) { + byte[] value = participantPending.get(ownedKey); + return value == null ? null : Arrays.copyOf(value, value.length); + } } byte[] owned = ownedKey.copy(); byte[] local = nativeStore.get(owned); @@ -543,12 +567,18 @@ private synchronized byte[] get(byte[] key) { return parentStores == null ? null : parentStores.get(owned); } - private synchronized void put(byte[] key, byte[] value) { - pending.put(new BytesKey(key), Arrays.copyOf(value, value.length)); + private void put(byte[] key, byte[] value) { + Map participantPending = pending(key); + synchronized (participantPending) { + participantPending.put(new BytesKey(key), Arrays.copyOf(value, value.length)); + } } - private synchronized void delete(byte[] key) { - pending.put(new BytesKey(key), null); + private void delete(byte[] key) { + Map participantPending = pending(key); + synchronized (participantPending) { + participantPending.put(new BytesKey(key), null); + } } private PathNodeStore nodeStore(int storeId) { @@ -657,10 +687,11 @@ private void validateNodeTombstones() throws IOException { } private List durableStateMutations( - byte[] rebuildValue, List leafMutations) { + byte[] rebuildValue, List leafMutations, + Collection storeIds) { List mutations = - new ArrayList<>(pending.size() + leafMutations.size() + 1); - appendPendingMutations(mutations); + new ArrayList<>(pendingSize(storeIds) + leafMutations.size() + 1); + appendPendingMutations(mutations, storeIds); appendLeafMutations(mutations, leafMutations); mutations.add(PathStateNativeNodeStore.BatchMutation.put(REBUILD_CHECKPOINT_KEY, rebuildValue)); @@ -764,7 +795,22 @@ private static long replaceLogicalEntry(long total, byte[] key, byte[] previous, private void appendPendingMutations( List mutations) { - for (Map.Entry entry : pending.entrySet()) { + appendPendingMutations(mutations, pending.keySet()); + } + + private void appendPendingMutations(List mutations, + Collection storeIds) { + for (Integer storeId : storeIds) { + Map participantPending = pending.get(storeId); + synchronized (participantPending) { + appendPendingMutations(mutations, participantPending); + } + } + } + + private void appendPendingMutations(List mutations, + Map participantPending) { + for (Map.Entry entry : participantPending.entrySet()) { byte[] key = entry.getKey().copy(); byte[] value = entry.getValue(); if (kind == Kind.LAYER) { @@ -867,7 +913,18 @@ private static byte[] leafKey(PathStateRoot.LeafMutationRecord mutation) { private long projectedPendingBytes(long total) throws IOException { long projected = total; - for (Map.Entry entry : pending.entrySet()) { + for (Map participantPending : pending.values()) { + synchronized (participantPending) { + projected = projectedPendingBytes(projected, participantPending); + } + } + return projected; + } + + private long projectedPendingBytes(long total, Map participantPending) + throws IOException { + long projected = total; + for (Map.Entry entry : participantPending.entrySet()) { byte[] key = entry.getKey().copy(); byte[] value = entry.getValue(); projected = replaceLogicalEntry(projected, key, nativeStore.get(key), value); @@ -880,6 +937,50 @@ private long projectedPendingBytes(long total) throws IOException { return projected; } + private Map pending(byte[] key) { + if (key.length < Integer.BYTES) { + throw new IllegalArgumentException("path-state namespaced key is too short"); + } + int storeId = ByteBuffer.wrap(key).getInt(); + Map participantPending = pending.get(storeId); + if (participantPending == null) { + throw new IllegalArgumentException("unknown path-state node Store ID: " + storeId); + } + return participantPending; + } + + private boolean hasPending() { + return pendingSize() != 0; + } + + private int pendingSize() { + return pendingSize(pending.keySet()); + } + + private int pendingSize(Collection storeIds) { + int size = 0; + for (Integer storeId : storeIds) { + Map participantPending = pending.get(storeId); + synchronized (participantPending) { + size = Math.addExact(size, participantPending.size()); + } + } + return size; + } + + private void clearPending() { + for (Integer storeId : pending.keySet()) { + clearPending(storeId); + } + } + + private void clearPending(int storeId) { + Map participantPending = pending.get(storeId); + synchronized (participantPending) { + participantPending.clear(); + } + } + private static byte[] tombstoneKey(byte[] nodeKey) { return ByteBuffer.allocate(Integer.BYTES + nodeKey.length) .putInt(NODE_TOMBSTONE_DOMAIN) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCheckpoint.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCheckpoint.java index d7bf912e525..1a8a4fec9ee 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCheckpoint.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCheckpoint.java @@ -21,7 +21,8 @@ final class PathStateRebuildCheckpoint { private static final int MAGIC = 0x50535243; // PSRC - private static final short VERSION = 1; + private static final short LEGACY_VERSION = 1; + private static final short VERSION = 2; private static final int MAX_LENGTH = 64 * 1024; private final byte[] manifestDigest; @@ -29,14 +30,28 @@ final class PathStateRebuildCheckpoint { private final SnapshotIdentity identity; private final List completedStores; private final byte[] partialRoot; + private final boolean independentStores; PathStateRebuildCheckpoint(byte[] manifestDigest, byte[] sourceIdentityDigest, SnapshotIdentity identity, List completedStores, byte[] partialRoot) { + this(manifestDigest, sourceIdentityDigest, identity, completedStores, partialRoot, true); + } + + PathStateRebuildCheckpoint(byte[] manifestDigest, byte[] sourceIdentityDigest, + SnapshotIdentity identity, List completedStores) { + this(manifestDigest, sourceIdentityDigest, identity, completedStores, + new byte[PathStateRootMetadata.DIGEST_LENGTH], true); + } + + private PathStateRebuildCheckpoint(byte[] manifestDigest, byte[] sourceIdentityDigest, + SnapshotIdentity identity, List completedStores, byte[] partialRoot, + boolean independentStores) { this.manifestDigest = copy32(manifestDigest, "manifestDigest"); this.sourceIdentityDigest = copy32(sourceIdentityDigest, "sourceIdentityDigest"); this.identity = Objects.requireNonNull(identity, "identity"); this.completedStores = validateStores(completedStores); this.partialRoot = copy32(partialRoot, "partialRoot"); + this.independentStores = independentStores; } byte[] getManifestDigest() { @@ -59,6 +74,10 @@ byte[] getPartialRoot() { return Arrays.copyOf(partialRoot, partialRoot.length); } + boolean hasIndependentStores() { + return independentStores; + } + byte[] encode() { try { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -108,7 +127,11 @@ static PathStateRebuildCheckpoint decode(byte[] encoded) throws IOException { throw new IOException("path-state rebuild checkpoint checksum mismatch"); } try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes))) { - if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0 + if (input.readInt() != MAGIC) { + throw new IOException("unsupported path-state rebuild checkpoint header"); + } + short version = input.readShort(); + if ((version != LEGACY_VERSION && version != VERSION) || input.readShort() != 0 || input.readInt() != bytes.length) { throw new IOException("unsupported path-state rebuild checkpoint header"); } @@ -136,7 +159,7 @@ static PathStateRebuildCheckpoint decode(byte[] encoded) throws IOException { throw new IOException("path-state rebuild checkpoint payload mismatch"); } return new PathStateRebuildCheckpoint(manifestDigest, sourceIdentityDigest, identity, stores, - partialRoot); + partialRoot, version == VERSION); } catch (IllegalArgumentException invalid) { throw new IOException("path-state rebuild checkpoint is invalid", invalid); } @@ -149,13 +172,19 @@ private static List validateStores(List stores) { if (supplied.size() > expected.size() || supplied.contains(null)) { throw new IllegalArgumentException("rebuild checkpoint Store count is invalid"); } - for (int index = 0; index < supplied.size(); index++) { - StoreResult actual = supplied.get(index); - PathStateParticipantDescriptor.StoreIdentity participant = expected.get(index); + int previousStoreId = 0; + for (StoreResult actual : supplied) { + if (actual.getStoreId() <= previousStoreId + || actual.getStoreId() > expected.size()) { + throw new IllegalArgumentException("rebuild checkpoint Store order is invalid"); + } + PathStateParticipantDescriptor.StoreIdentity participant = + expected.get(actual.getStoreId() - 1); if (actual.getStoreId() != participant.getStoreId() || !actual.getDbName().equals(participant.getDbName())) { throw new IllegalArgumentException("rebuild checkpoint Store order is invalid"); } + previousStoreId = actual.getStoreId(); } return Collections.unmodifiableList(supplied); } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index a5d575749b8..62e90852267 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -10,9 +10,18 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; import org.tron.core.capsule.utils.MarketUtils; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateParticipantDescriptor.StoreIdentity; @@ -22,6 +31,11 @@ public final class PathStateRebuildCoordinator { private static final String STORE_DIGEST_DOMAIN = "path-state-rebuild-store/v1"; private static final String SOURCE_DIGEST_DOMAIN = "path-state-rebuild-source/v1"; + private static final int LARGE_STORE_WORKERS = 2; + private static final int SMALL_STORE_WORKERS = 2; + private static final Set LARGE_STORES = Collections.unmodifiableSet( + new LinkedHashSet<>(Arrays.asList( + "account", "account-asset", "delegation", "storage-row"))); private final PathStateParticipantDescriptor descriptor; private final PathStateCanonicalizer canonicalizer; @@ -79,8 +93,12 @@ private RebuildResult rebuildInternal(PathStateStoreManifest manifest, SnapshotS try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(admittedManifest)) { PathStateRoot root = stores.createRoot(); PathStateRebuildCheckpoint checkpoint = stores.getRebuildCheckpoint(); - List storeResults = checkpoint == null ? new ArrayList<>() - : new ArrayList<>(checkpoint.getCompletedStores()); + Map completedStores = new TreeMap<>(); + if (checkpoint != null) { + for (StoreResult completed : checkpoint.getCompletedStores()) { + completedStores.put(completed.getStoreId(), completed); + } + } if (checkpoint != null && !identity.sameAs(checkpoint.getIdentity())) { throw new IOException("path-state rebuild checkpoint snapshot identity mismatch"); } @@ -92,22 +110,13 @@ private RebuildResult rebuildInternal(PathStateStoreManifest manifest, SnapshotS checkpoint.getSourceIdentityDigest())) { throw new IOException("path-state rebuild checkpoint source identity mismatch"); } + buildStoresInParallel(admittedManifest, admittedSource, identity, sourceIdentityDigest, + root, stores, completedStores); + List storeResults = new ArrayList<>(completedStores.values()); long totalEntries = 0; for (StoreResult completed : storeResults) { totalEntries = Math.addExact(totalEntries, completed.getEntryCount()); } - for (int index = storeResults.size(); index < descriptor.getStores().size(); index++) { - StoreIdentity store = descriptor.getStores().get(index); - StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root); - admittedSource.scan(store.getDbName(), accumulator::accept); - StoreResult result = accumulator.finish(); - storeResults.add(result); - totalEntries = Math.addExact(totalEntries, result.getEntryCount()); - checkpoint = new PathStateRebuildCheckpoint(admittedManifest.getIdentityDigest(), - sourceIdentityDigest, identity, storeResults, root.rootHash()); - stores.checkpointRebuild(checkpoint); - faultHook.afterStore(result); - } admittedSource.verifyIdentity(identity); byte[] stateRoot = root.rootHash(); @@ -130,6 +139,130 @@ private RebuildResult rebuildInternal(PathStateStoreManifest manifest, SnapshotS return rebuildResult; } + private void buildStoresInParallel(PathStateStoreManifest manifest, SnapshotSource source, + SnapshotIdentity identity, byte[] sourceIdentityDigest, PathStateRoot root, + PathStateNodeStoreSet stores, Map completedStores) throws IOException { + Object checkpointLock = new Object(); + ExecutorService largeExecutor = Executors.newFixedThreadPool(LARGE_STORE_WORKERS, + rebuildThreadFactory("large")); + ExecutorService smallExecutor = Executors.newFixedThreadPool(SMALL_STORE_WORKERS, + rebuildThreadFactory("small")); + List> futures = new ArrayList<>(); + Future accountFuture = null; + StoreIdentity accountAsset = null; + try { + for (StoreIdentity store : descriptor.getStores()) { + if (completedStores.containsKey(store.getStoreId())) { + continue; + } + if ("account-asset".equals(store.getDbName())) { + accountAsset = store; + continue; + } + ExecutorService executor = LARGE_STORES.contains(store.getDbName()) + ? largeExecutor : smallExecutor; + Future future = submitStore(executor, null, store, manifest, source, identity, + sourceIdentityDigest, root, stores, completedStores, checkpointLock); + futures.add(future); + if ("account".equals(store.getDbName())) { + accountFuture = future; + } + } + if (accountAsset != null) { + futures.add(submitStore(largeExecutor, accountFuture, accountAsset, manifest, source, + identity, sourceIdentityDigest, root, stores, completedStores, checkpointLock)); + } + Throwable failure = null; + for (Future future : futures) { + try { + future.get(); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + failure = appendFailure(failure, + new IOException("path-state rebuild interrupted", interrupted)); + } catch (ExecutionException failed) { + Throwable cause = failed.getCause(); + failure = appendFailure(failure, cause instanceof IOException + || cause instanceof RuntimeException ? cause + : new IOException("path-state Store rebuild failed", cause)); + } + } + if (failure != null) { + if (failure instanceof IOException) { + throw (IOException) failure; + } + throw (RuntimeException) failure; + } + } finally { + largeExecutor.shutdownNow(); + smallExecutor.shutdownNow(); + } + } + + private Future submitStore(ExecutorService executor, Future dependency, + StoreIdentity store, PathStateStoreManifest manifest, SnapshotSource source, + SnapshotIdentity identity, byte[] sourceIdentityDigest, PathStateRoot root, + PathStateNodeStoreSet stores, Map completedStores, + Object checkpointLock) { + return executor.submit(() -> { + awaitDependency(dependency); + StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root); + source.scan(store.getDbName(), accumulator::accept); + StoreResult result = accumulator.finish(); + if ("account".equals(store.getDbName())) { + root.participantRoot("account-asset"); + } + synchronized (checkpointLock) { + completedStores.put(result.getStoreId(), result); + PathStateRebuildCheckpoint next = new PathStateRebuildCheckpoint( + manifest.getIdentityDigest(), sourceIdentityDigest, identity, + new ArrayList<>(completedStores.values())); + stores.checkpointRebuild(next, checkpointStoreIds(store)); + } + faultHook.afterStore(result); + return null; + }); + } + + private static void awaitDependency(Future dependency) throws IOException { + if (dependency == null) { + return; + } + try { + dependency.get(); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("path-state dependent Store rebuild interrupted", interrupted); + } catch (ExecutionException failed) { + throw new IOException("path-state dependent Store rebuild failed", failed.getCause()); + } + } + + private static Collection checkpointStoreIds(StoreIdentity store) { + if ("account".equals(store.getDbName())) { + return Arrays.asList(store.getStoreId(), store.getStoreId() + 1); + } + return Collections.singletonList(store.getStoreId()); + } + + private static ThreadFactory rebuildThreadFactory(String tier) { + AtomicInteger sequence = new AtomicInteger(); + return task -> { + Thread thread = new Thread(task, + "path-state-rebuild-" + tier + "-" + sequence.incrementAndGet()); + thread.setDaemon(true); + return thread; + }; + } + + private static Throwable appendFailure(Throwable previous, Throwable next) { + if (previous == null) { + return next; + } + previous.addSuppressed(next); + return previous; + } + private byte[] sourceDigest(SnapshotIdentity identity, byte[] sourceIdentityDigest, List stores, byte[] stateRoot) { Hasher hasher = domainHasher(SOURCE_DIGEST_DOMAIN); @@ -181,12 +314,12 @@ private void accept(byte[] physicalKey, byte[] rawValue) throws IOException { } validateAccountLayout(key, value); PathStateMutation mutation = canonicalizer.put(phase, store.getDbName(), key, value); - root.apply(Collections.singletonList(mutation)); + root.applyRebuild(Collections.singletonList(mutation)); if ("account".equals(store.getDbName())) { List projected = canonicalizer.projectSnapshotAccountAssets( phase, key, value); if (!projected.isEmpty()) { - root.apply(projected); + root.applyRebuild(projected); } } putBytes(inputDigest, key); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index accaff7db9c..676752a29a3 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -31,10 +31,10 @@ public final class PathStateRoot { private final PathStateParticipantScope scope; private final Map participantTries = new LinkedHashMap<>(); - private final Map pendingLeafMutations = - new LinkedHashMap<>(); + private final Map> + pendingLeafMutations = new LinkedHashMap<>(); private final PathMerkleTrie superTrie; - private boolean rootMaterialized; + private volatile boolean rootMaterialized; public PathStateRoot(PathStateParticipantScope scope, PathNodeStoreFactory storeFactory, PathNodeStore superNodeStore) { @@ -59,6 +59,7 @@ private PathStateRoot(PathStateParticipantScope scope, PathNodeStoreFactory stor } participantTries.put(participant.getDbName(), snapshot == null ? new PathMerkleTrie(nodeStore) : PathMerkleTrie.fromSnapshot(nodeStore, trieSnapshot)); + pendingLeafMutations.put(participant.getStoreId(), new LinkedHashMap<>()); } PathNodeStore rootStore = Objects.requireNonNull(superNodeStore, "superNodeStore"); if (!uniqueStores.add(rootStore)) { @@ -98,6 +99,21 @@ public synchronized void apply(Collection mutations) { rootMaterialized = false; } + /** Applies one rebuild batch while locking only the participant tries touched by that batch. */ + void applyRebuild(Collection mutations) { + List prepared = prepare(mutations); + for (PreparedMutation mutation : prepared) { + PathMerkleTrie trie = participantTries.get(mutation.participant.getDbName()); + if (mutation.encodedValue == null) { + trie.delete(mutation.secureKey); + } else { + trie.put(mutation.secureKey, mutation.encodedValue); + } + } + recordPendingLeafMutations(prepared); + rootMaterialized = false; + } + synchronized void recordPendingLeafMutations(Collection mutations) { recordPendingLeafMutations(prepare(mutations)); } @@ -105,7 +121,11 @@ synchronized void recordPendingLeafMutations(Collection mutat private void recordPendingLeafMutations(List prepared) { for (PreparedMutation mutation : prepared) { MutationKey key = new MutationKey(mutation.participant.getStoreId(), mutation.secureKey); - pendingLeafMutations.put(key, mutation.participant); + Map participantMutations = + pendingLeafMutations.get(mutation.participant.getStoreId()); + synchronized (participantMutations) { + participantMutations.put(key, mutation.participant); + } } } @@ -155,25 +175,58 @@ synchronized List leafRecords() { /** Returns only leaf mutations accumulated since the last durable commit/checkpoint. */ synchronized List pendingLeafMutations() { - List mutations = new ArrayList<>(pendingLeafMutations.size()); - for (Map.Entry entry : pendingLeafMutations.entrySet()) { - MutationKey key = entry.getKey(); - PathStateParticipant participant = entry.getValue(); - mutations.add(new LeafMutationRecord(participant.getStoreId(), key.secureKey, - participantTries.get(participant.getDbName()).get(key.secureKey))); + List mutations = new ArrayList<>(); + for (Integer storeId : pendingLeafMutations.keySet()) { + mutations.addAll(pendingLeafMutations(storeId)); } return mutations; } + List pendingLeafMutations(int storeId) { + Map participantMutations = + pendingLeafMutations.get(storeId); + if (participantMutations == null) { + throw new IllegalArgumentException("unknown path-state Store ID: " + storeId); + } + synchronized (participantMutations) { + List mutations = new ArrayList<>(participantMutations.size()); + for (Map.Entry entry + : participantMutations.entrySet()) { + MutationKey key = entry.getKey(); + PathStateParticipant participant = entry.getValue(); + mutations.add(new LeafMutationRecord(participant.getStoreId(), key.secureKey, + participantTries.get(participant.getDbName()).get(key.secureKey))); + } + return mutations; + } + } + synchronized void clearPendingLeafMutations() { - pendingLeafMutations.clear(); + for (Integer storeId : pendingLeafMutations.keySet()) { + clearPendingLeafMutations(storeId); + } + } + + void clearPendingLeafMutations(int storeId) { + Map participantMutations = + pendingLeafMutations.get(storeId); + if (participantMutations == null) { + throw new IllegalArgumentException("unknown path-state Store ID: " + storeId); + } + synchronized (participantMutations) { + participantMutations.clear(); + } } synchronized void recordPendingLeafRecords(Collection records) { for (LeafRecord record : Objects.requireNonNull(records, "records")) { LeafRecord present = Objects.requireNonNull(record, "record"); PathStateParticipant participant = participant(present.storeId); - pendingLeafMutations.put(new MutationKey(present.storeId, present.secureKey), participant); + Map participantMutations = + pendingLeafMutations.get(present.storeId); + synchronized (participantMutations) { + participantMutations.put(new MutationKey(present.storeId, present.secureKey), participant); + } } } @@ -201,8 +254,45 @@ synchronized void restoreLeaves(Collection records, byte[] expectedR restoreLeaves(records, expectedRoot, false); } + synchronized void restoreRebuildLeaves(Collection records, + PathStateRebuildCheckpoint checkpoint) { + restoreParticipantLeaves(records, false); + for (PathStateRebuildCoordinator.StoreResult result : checkpoint.getCompletedStores()) { + if (!Arrays.equals(participantRoot(result.getDbName()), result.getStoreRoot())) { + throw new IllegalStateException( + "restored path-state participant root differs from durable progress"); + } + } + rootMaterialized = false; + } + private void restoreLeaves(Collection records, byte[] expectedRoot, boolean initialize) { + restoreParticipantLeaves(records, initialize); + List superLeaves = new ArrayList<>(); + for (PathStateParticipant participant : scope.getParticipants()) { + PathMerkleTrie trie = participantTries.get(participant.getDbName()); + byte[] storeRoot = trie.rootHash(); + superLeaves.add(new PathMerkleTrie.LeafEntry( + PathStateCommitmentCodec.superLeafKey(participant.getStoreId()), + PathStateCommitmentCodec.superLeafValue(participant.getStoreId(), + participant.getDbName(), participant.getStoreFormatVersion(), storeRoot))); + } + if (initialize) { + superTrie.initializeLeaves(superLeaves); + } else { + superTrie.restoreLeaves(superLeaves); + } + byte[] restoredRoot = superTrie.rootHash(); + if (!Arrays.equals(restoredRoot, Objects.requireNonNull(expectedRoot, "expectedRoot"))) { + throw new IllegalStateException("restored path-state root differs from durable progress"); + } + rootMaterialized = true; + verifyNodeStores(); + } + + private Map> restoreParticipantLeaves( + Collection records, boolean initialize) { Map participants = new LinkedHashMap<>(); Map> leaves = new LinkedHashMap<>(); for (PathStateParticipant participant : scope.getParticipants()) { @@ -218,7 +308,6 @@ private void restoreLeaves(Collection records, byte[] expectedRoot, present.secureKey, present.encodedValue)); } - List superLeaves = new ArrayList<>(); for (PathStateParticipant participant : scope.getParticipants()) { PathMerkleTrie trie = participantTries.get(participant.getDbName()); if (initialize) { @@ -226,23 +315,8 @@ private void restoreLeaves(Collection records, byte[] expectedRoot, } else { trie.restoreLeaves(leaves.get(participant.getStoreId())); } - byte[] storeRoot = trie.rootHash(); - superLeaves.add(new PathMerkleTrie.LeafEntry( - PathStateCommitmentCodec.superLeafKey(participant.getStoreId()), - PathStateCommitmentCodec.superLeafValue(participant.getStoreId(), - participant.getDbName(), participant.getStoreFormatVersion(), storeRoot))); } - if (initialize) { - superTrie.initializeLeaves(superLeaves); - } else { - superTrie.restoreLeaves(superLeaves); - } - byte[] restoredRoot = superTrie.rootHash(); - if (!Arrays.equals(restoredRoot, Objects.requireNonNull(expectedRoot, "expectedRoot"))) { - throw new IllegalStateException("restored path-state root differs from durable progress"); - } - rootMaterialized = true; - verifyNodeStores(); + return leaves; } private List prepare(Collection mutations) { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index a93fa55ab33..587effbfd56 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -7,6 +7,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.google.common.hash.Hashing; import com.google.protobuf.ByteString; import java.io.IOException; import java.nio.ByteBuffer; @@ -18,6 +19,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.junit.Rule; @@ -94,6 +97,67 @@ public void rebuildsAndPublishesExactSnapshotAcrossNativeEngines() throws Except } } + @Test + public void schedulesLargeAndSmallStoresInParallelAndOrdersAccountAsset() throws Exception { + PathStateStoreManifest manifest = manifest("parallel-tiers", Engine.ROCKSDB); + TestSnapshotSource delegate = exactSource(identity()); + CountDownLatch concurrentScans = new CountDownLatch(3); + Map threads = Collections.synchronizedMap(new LinkedHashMap<>()); + AtomicBoolean accountReturned = new AtomicBoolean(); + SnapshotSource source = new SnapshotSource() { + @Override + public SnapshotIdentity identity() { + return delegate.identity(); + } + + @Override + public Collection databases() { + return delegate.databases(); + } + + @Override + public byte[] sourceIdentityDigest() { + return delegate.sourceIdentityDigest(); + } + + @Override + public void scan(String dbName, EntryConsumer consumer) throws IOException { + threads.put(dbName, Thread.currentThread().getName()); + if ("account-asset".equals(dbName) && !accountReturned.get()) { + throw new IOException("account-asset started before account completed"); + } + if ("account".equals(dbName) || "storage-row".equals(dbName) + || "abi".equals(dbName)) { + concurrentScans.countDown(); + try { + if (!concurrentScans.await(5, TimeUnit.SECONDS)) { + throw new IOException("Store tiers did not scan concurrently"); + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("parallel Store test interrupted", interrupted); + } + } + delegate.scan(dbName, consumer); + if ("account".equals(dbName)) { + accountReturned.set(true); + } + } + + @Override + public void verifyIdentity(SnapshotIdentity expected) throws IOException { + delegate.verifyIdentity(expected); + } + }; + + assertEquals(27, new PathStateRebuildCoordinator().rebuild(manifest, source) + .getStores().size()); + assertTrue(threads.get("account").startsWith("path-state-rebuild-large-")); + assertTrue(threads.get("storage-row").startsWith("path-state-rebuild-large-")); + assertTrue(threads.get("abi").startsWith("path-state-rebuild-small-")); + assertTrue(accountReturned.get()); + } + @Test public void rejectsScopeMismatchBeforeOpeningBaseNodes() throws Exception { PathStateStoreManifest manifest = manifest("scope-mismatch", Engine.ROCKSDB); @@ -212,6 +276,10 @@ public void buildsAccountAssetRowsWithoutCrossStoreOwnerValidation() throws Exce public void resumesCompletedStoresWhileKeepingGenerationInvisible() throws Exception { PathStateStoreManifest manifest = manifest("resume", Engine.ROCKSDB); TestSnapshotSource first = exactSource(identity()); + byte[] lazyAddress = address(9); + byte[] lazyAccount = account(lazyAddress).toBuilder() + .putAssetV2("1000001", 17L).build().toByteArray(); + first.add("account", lazyAddress, lazyAccount); first.add("proposal", new byte[]{1}, new byte[]{2}); AtomicBoolean failed = new AtomicBoolean(); PathStateRebuildCoordinator interrupted = new PathStateRebuildCoordinator(store -> { @@ -227,6 +295,7 @@ public void resumesCompletedStoresWhileKeepingGenerationInvisible() throws Excep assertNull(PathStateNodeStoreSet.loadProgress(manifest.getBaseDirectory(), manifest)); TestSnapshotSource resumed = exactSource(identity()); + resumed.add("account", lazyAddress, lazyAccount); resumed.add("proposal", new byte[]{1}, new byte[]{2}); RebuildResult result = new PathStateRebuildCoordinator().rebuild(manifest, resumed); @@ -236,11 +305,13 @@ public void resumesCompletedStoresWhileKeepingGenerationInvisible() throws Excep assertEquals(0, resumed.getScanCount("account-index")); assertEquals(0, resumed.getScanCount("account")); assertEquals(1, resumed.getScanCount("account-asset")); - assertEquals(1, resumed.getScanCount("proposal")); + assertTrue(resumed.getScanCount("proposal") == 0 + || resumed.getScanCount("proposal") == 1); assertTrue(new PathStateCurrentStore(manifest).isInitialized()); PathStateStoreManifest freshManifest = manifest("resume-fresh", Engine.ROCKSDB); TestSnapshotSource fresh = exactSource(identity()); + fresh.add("account", lazyAddress, lazyAccount); fresh.add("proposal", new byte[]{1}, new byte[]{2}); RebuildResult freshResult = new PathStateRebuildCoordinator().rebuild(freshManifest, fresh); assertArrayEquals(freshResult.getMetadata().getStateRoot(), @@ -252,6 +323,36 @@ public void resumesCompletedStoresWhileKeepingGenerationInvisible() throws Excep } } + @Test + public void resumesNonPrefixCheckpointWithoutPersistingFailedStoreResidue() throws Exception { + PathStateStoreManifest manifest = manifest("resume-non-prefix", Engine.ROCKSDB); + TestSnapshotSource failedSource = exactSource(identity()); + byte[] accountKey = address(7); + byte[] accountValue = account(accountKey).toByteArray(); + failedSource.add("account", accountKey, accountValue); + failedSource.add("account", accountKey, accountValue); + + assertThrows(IllegalArgumentException.class, + () -> new PathStateRebuildCoordinator().rebuild(manifest, failedSource)); + assertFalse(new PathStateCurrentStore(manifest).isInitialized()); + try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openBase(manifest)) { + PathStateRebuildCheckpoint checkpoint = reopened.getRebuildCheckpoint(); + assertTrue(checkpoint.hasIndependentStores()); + assertTrue(checkpoint.getCompletedStores().stream() + .noneMatch(store -> "account".equals(store.getDbName()))); + assertTrue(checkpoint.getCompletedStores().stream() + .anyMatch(store -> store.getStoreId() > 4)); + } + + RebuildResult resumed = new PathStateRebuildCoordinator().rebuild(manifest, + exactSource(identity())); + PathStateStoreManifest freshManifest = manifest("resume-non-prefix-fresh", Engine.ROCKSDB); + RebuildResult fresh = new PathStateRebuildCoordinator().rebuild(freshManifest, + exactSource(identity())); + assertArrayEquals(fresh.getMetadata().getStateRoot(), resumed.getMetadata().getStateRoot()); + assertArrayEquals(fresh.getSourceDigest(), resumed.getSourceDigest()); + } + @Test public void rejectsResumeAgainstAnotherSnapshotIdentity() throws Exception { PathStateStoreManifest manifest = manifest("resume-identity", Engine.ROCKSDB); @@ -287,7 +388,8 @@ public void rejectsResumeAgainstReplacedPhysicalSources() throws Exception { } @Test - public void rebuildCheckpointCodecRejectsCorruptionAndNonPrefixStores() throws Exception { + public void rebuildCheckpointCodecSupportsNonPrefixStoresAndRejectsCorruption() + throws Exception { StoreResult abi = StoreResult.restore(1, "abi", 2, bytes(5), bytes(6)); PathStateRebuildCheckpoint checkpoint = new PathStateRebuildCheckpoint(bytes(7), bytes(11), identity(), Collections.singletonList(abi), bytes(8)); @@ -298,10 +400,22 @@ public void rebuildCheckpointCodecRejectsCorruptionAndNonPrefixStores() throws E assertEquals(1, decoded.getCompletedStores().size()); assertArrayEquals(checkpoint.getPartialRoot(), decoded.getPartialRoot()); + byte[] legacy = checkpoint.encode(); + ByteBuffer.wrap(legacy).putShort(Integer.BYTES, (short) 1); + int payloadLength = legacy.length - Integer.BYTES; + ByteBuffer.wrap(legacy).putInt(payloadLength, + Hashing.crc32c().hashBytes(legacy, 0, payloadLength).asInt()); + assertFalse(PathStateRebuildCheckpoint.decode(legacy).hasIndependentStores()); + byte[] corrupt = checkpoint.encode(); corrupt[corrupt.length - 1] ^= 1; assertThrows(IOException.class, () -> PathStateRebuildCheckpoint.decode(corrupt)); - StoreResult wrongFirst = StoreResult.restore(2, "accountid-index", 0, bytes(9), bytes(10)); + StoreResult nonPrefix = StoreResult.restore(2, "accountid-index", 0, bytes(9), bytes(10)); + PathStateRebuildCheckpoint nonPrefixCheckpoint = new PathStateRebuildCheckpoint(bytes(7), + bytes(11), identity(), Collections.singletonList(nonPrefix), bytes(8)); + assertEquals(2, PathStateRebuildCheckpoint.decode(nonPrefixCheckpoint.encode()) + .getCompletedStores().get(0).getStoreId()); + StoreResult wrongFirst = StoreResult.restore(2, "abi", 0, bytes(9), bytes(10)); assertThrows(IllegalArgumentException.class, () -> new PathStateRebuildCheckpoint(bytes(7), bytes(11), identity(), Collections.singletonList(wrongFirst), bytes(8))); } @@ -529,7 +643,7 @@ private int getVerificationCount() { return verificationCount; } - private int getScanCount(String dbName) { + private synchronized int getScanCount(String dbName) { Integer count = scanCounts.get(dbName); return count == null ? 0 : count; } @@ -557,7 +671,9 @@ public byte[] sourceIdentityDigest() { @Override public void scan(String dbName, EntryConsumer consumer) throws IOException { - scanCounts.put(dbName, getScanCount(dbName) + 1); + synchronized (this) { + scanCounts.put(dbName, getScanCount(dbName) + 1); + } for (Row row : stores.get(dbName)) { consumer.accept(row.key, row.value); } From 90120013967257ac923d196b037f98eb760751a3 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sun, 30 Aug 2026 09:16:08 +0800 Subject: [PATCH 094/161] perf(chainbase): bound large state root rebuilds --- .../tron/core/db2/stateroot/PathStateRebuildCoordinator.java | 2 +- .../core/db2/stateroot/PathStateRebuildCoordinatorTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index 62e90852267..5b63b759249 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -31,7 +31,7 @@ public final class PathStateRebuildCoordinator { private static final String STORE_DIGEST_DOMAIN = "path-state-rebuild-store/v1"; private static final String SOURCE_DIGEST_DOMAIN = "path-state-rebuild-source/v1"; - private static final int LARGE_STORE_WORKERS = 2; + private static final int LARGE_STORE_WORKERS = 1; private static final int SMALL_STORE_WORKERS = 2; private static final Set LARGE_STORES = Collections.unmodifiableSet( new LinkedHashSet<>(Arrays.asList( diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index 587effbfd56..6b57ad97425 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -126,7 +126,7 @@ public void scan(String dbName, EntryConsumer consumer) throws IOException { if ("account-asset".equals(dbName) && !accountReturned.get()) { throw new IOException("account-asset started before account completed"); } - if ("account".equals(dbName) || "storage-row".equals(dbName) + if ("account".equals(dbName) || "accountid-index".equals(dbName) || "abi".equals(dbName)) { concurrentScans.countDown(); try { From e3fee30a834ac05fe77ca85ef9a0e630cfe4e6eb Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sun, 30 Aug 2026 09:44:07 +0800 Subject: [PATCH 095/161] perf(chainbase): report state root rebuild progress --- .../PathStateRebuildCoordinator.java | 63 ++++++++++++++++++- .../PathStateRebuildCoordinatorTest.java | 5 +- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index 5b63b759249..218ed830b04 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -21,7 +21,10 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.tron.core.capsule.utils.MarketUtils; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateParticipantDescriptor.StoreIdentity; @@ -29,10 +32,13 @@ /** Builds and atomically publishes the first current path-state root from one admitted snapshot. */ public final class PathStateRebuildCoordinator { + private static final Logger logger = LoggerFactory.getLogger("DB"); private static final String STORE_DIGEST_DOMAIN = "path-state-rebuild-store/v1"; private static final String SOURCE_DIGEST_DOMAIN = "path-state-rebuild-source/v1"; + private static final long PROGRESS_LOG_INTERVAL_NANOS = TimeUnit.SECONDS.toNanos(30); + private static final long PROGRESS_LOG_ROW_MASK = 1023L; private static final int LARGE_STORE_WORKERS = 1; - private static final int SMALL_STORE_WORKERS = 2; + private static final int SMALL_STORE_WORKERS = 1; private static final Set LARGE_STORES = Collections.unmodifiableSet( new LinkedHashSet<>(Arrays.asList( "account", "account-asset", "delegation", "storage-row"))); @@ -98,6 +104,14 @@ private RebuildResult rebuildInternal(PathStateStoreManifest manifest, SnapshotS for (StoreResult completed : checkpoint.getCompletedStores()) { completedStores.put(completed.getStoreId(), completed); } + logger.info("Path-state rebuild checkpoint restored: completedStores={}, " + + "remainingStores={}, reuseScope=completed-store, " + + "inProgressStoreResume=from-beginning", + completedStores.size(), descriptor.getStores().size() - completedStores.size()); + } else { + logger.info("Path-state rebuild starting without checkpoint: stores={}, " + + "reuseScope=completed-store, inProgressStoreResume=from-beginning", + descriptor.getStores().size()); } if (checkpoint != null && !identity.sameAs(checkpoint.getIdentity())) { throw new IOException("path-state rebuild checkpoint snapshot identity mismatch"); @@ -153,6 +167,9 @@ private void buildStoresInParallel(PathStateStoreManifest manifest, SnapshotSour try { for (StoreIdentity store : descriptor.getStores()) { if (completedStores.containsKey(store.getStoreId())) { + StoreResult completed = completedStores.get(store.getStoreId()); + logger.info("Path-state rebuild Store reused: storeId={}, dbName={}, entries={}", + store.getStoreId(), store.getDbName(), completed.getEntryCount()); continue; } if ("account-asset".equals(store.getDbName())) { @@ -207,8 +224,11 @@ private Future submitStore(ExecutorService executor, Future dependency, return executor.submit(() -> { awaitDependency(dependency); StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root); + logger.info("Path-state rebuild Store started: storeId={}, dbName={}, tier={}", + store.getStoreId(), store.getDbName(), storeTier(store)); source.scan(store.getDbName(), accumulator::accept); StoreResult result = accumulator.finish(); + accumulator.logCompleted(); if ("account".equals(store.getDbName())) { root.participantRoot("account-asset"); } @@ -218,6 +238,10 @@ private Future submitStore(ExecutorService executor, Future dependency, manifest.getIdentityDigest(), sourceIdentityDigest, identity, new ArrayList<>(completedStores.values())); stores.checkpointRebuild(next, checkpointStoreIds(store)); + logger.info("Path-state rebuild Store checkpointed: storeId={}, dbName={}, entries={}, " + + "completedStores={}, remainingStores={}", + result.getStoreId(), result.getDbName(), result.getEntryCount(), + completedStores.size(), descriptor.getStores().size() - completedStores.size()); } faultHook.afterStore(result); return null; @@ -255,6 +279,10 @@ private static ThreadFactory rebuildThreadFactory(String tier) { }; } + private static String storeTier(StoreIdentity store) { + return LARGE_STORES.contains(store.getDbName()) ? "large" : "small"; + } + private static Throwable appendFailure(Throwable previous, Throwable next) { if (previous == null) { return next; @@ -290,8 +318,12 @@ private final class StoreAccumulator { private final P66Phase phase; private final PathStateRoot root; private final Hasher inputDigest; + private final long startedNanos = System.nanoTime(); private byte[] previousKey; private long entryCount; + private long keyBytes; + private long valueBytes; + private long lastProgressLogNanos = startedNanos; private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root) { this.store = store; @@ -326,6 +358,9 @@ private void accept(byte[] physicalKey, byte[] rawValue) throws IOException { putBytes(inputDigest, value); previousKey = key; entryCount = Math.addExact(entryCount, 1L); + keyBytes = Math.addExact(keyBytes, key.length); + valueBytes = Math.addExact(valueBytes, value.length); + logProgressIfDue(); } private void validateAccountLayout(byte[] key, byte[] value) { @@ -338,6 +373,32 @@ private StoreResult finish() { return new StoreResult(store.getStoreId(), store.getDbName(), entryCount, inputDigest.hash().asBytes(), root.participantRoot(store.getDbName())); } + + private void logProgressIfDue() { + if ((entryCount & PROGRESS_LOG_ROW_MASK) != 0) { + return; + } + long now = System.nanoTime(); + if (now - lastProgressLogNanos < PROGRESS_LOG_INTERVAL_NANOS) { + return; + } + lastProgressLogNanos = now; + logProgress("scanning", now); + } + + private void logCompleted() { + logProgress("completed", System.nanoTime()); + } + + private void logProgress(String status, long now) { + long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(now - startedNanos); + long entriesPerSecond = elapsedMillis == 0 ? entryCount + : (long) (entryCount * 1000.0d / elapsedMillis); + logger.info("Path-state rebuild Store {}: storeId={}, dbName={}, tier={}, rows={}, " + + "keyBytes={}, valueBytes={}, elapsedMs={}, rowsPerSecond={}", + status, store.getStoreId(), store.getDbName(), storeTier(store), entryCount, + keyBytes, valueBytes, elapsedMillis, entriesPerSecond); + } } private static int compare(StoreIdentity store, byte[] left, byte[] right) { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index 6b57ad97425..810ba0470bb 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -101,7 +101,7 @@ public void rebuildsAndPublishesExactSnapshotAcrossNativeEngines() throws Except public void schedulesLargeAndSmallStoresInParallelAndOrdersAccountAsset() throws Exception { PathStateStoreManifest manifest = manifest("parallel-tiers", Engine.ROCKSDB); TestSnapshotSource delegate = exactSource(identity()); - CountDownLatch concurrentScans = new CountDownLatch(3); + CountDownLatch concurrentScans = new CountDownLatch(2); Map threads = Collections.synchronizedMap(new LinkedHashMap<>()); AtomicBoolean accountReturned = new AtomicBoolean(); SnapshotSource source = new SnapshotSource() { @@ -126,8 +126,7 @@ public void scan(String dbName, EntryConsumer consumer) throws IOException { if ("account-asset".equals(dbName) && !accountReturned.get()) { throw new IOException("account-asset started before account completed"); } - if ("account".equals(dbName) || "accountid-index".equals(dbName) - || "abi".equals(dbName)) { + if ("account".equals(dbName) || "abi".equals(dbName)) { concurrentScans.countDown(); try { if (!concurrentScans.await(5, TimeUnit.SECONDS)) { From f1aeb47802fe429e462174e5d3180e72ffd0dc31 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sun, 30 Aug 2026 18:06:38 +0800 Subject: [PATCH 096/161] fix(chainbase): bound path-state rebuild memory --- .../core/db2/stateroot/PathMerkleTrie.java | 285 ++++++++++++- .../core/db2/stateroot/PathStateLayer.java | 2 +- .../stateroot/PathStateLayerPublication.java | 1 + .../db2/stateroot/PathStateNodeStoreSet.java | 200 ++++++++- .../PathStateRebuildCoordinator.java | 147 ++++++- .../core/db2/stateroot/PathStateRoot.java | 44 +- .../db2/stateroot/PathStateStackTrie.java | 386 ++++++++++++++++++ .../stateroot/PathStateStoreTrieBuilder.java | 178 ++++++++ .../db2/stateroot/PathMerkleTrieTest.java | 37 ++ .../db2/stateroot/PathStateStackTrieTest.java | 139 +++++++ .../PathStateStoreTrieBuilderTest.java | 61 +++ 11 files changed, 1432 insertions(+), 48 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStackTrie.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateStackTrieTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilderTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index 1c7754946f6..284befd19e9 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -60,7 +60,7 @@ public synchronized void put(byte[] secureKey, byte[] encodedValue) { if (previous == null) { leafCount++; } - rootNode = update(rootNode, toNibbles(key.bytes), 0, value); + rootNode = update(rootNode, toNibbles(key.bytes), 0, EMPTY_PATH, value); dirty = true; } @@ -70,7 +70,7 @@ public synchronized void delete(byte[] secureKey) { if (leafValue(key) != null) { leaves.put(key, null); leafCount--; - rootNode = update(rootNode, toNibbles(key.bytes), 0, null); + rootNode = update(rootNode, toNibbles(key.bytes), 0, EMPTY_PATH, null); dirty = true; } } @@ -150,6 +150,38 @@ synchronized void restoreLeaves(Collection entries) { rootHash = hash(rootNode); } + /** Restores only the durable root node; descendants are decoded from path storage on demand. */ + synchronized void restoreRoot(byte[] expectedRoot) { + if (!leaves.isEmpty() || inheritedSnapshot != null || rootNode != null + || materializedRoot != null || dirty) { + throw new IllegalStateException("path trie is not empty before root restoration"); + } + byte[] expected = Arrays.copyOf(Objects.requireNonNull(expectedRoot, "expectedRoot"), + expectedRoot.length); + if (expected.length != SECURE_KEY_LENGTH) { + throw new IllegalArgumentException("expectedRoot must contain exactly 32 bytes"); + } + if (Arrays.equals(expected, Hash.EMPTY_TRIE_HASH)) { + rootHash = expected; + return; + } + byte[] encoded = nodeStore.get(EMPTY_PATH); + if (encoded == null || !Arrays.equals(Hash.sha3(encoded), expected)) { + throw new IllegalStateException("durable path trie root is missing or corrupt"); + } + rootNode = new StoredNode(encoded, EMPTY_PATH); + materializedRoot = rootNode; + materializedNodes.put(rootNode, new BytesKey(EMPTY_PATH)); + rootHash = expected; + } + + synchronized byte[] restoreRoot() { + byte[] encoded = nodeStore.get(EMPTY_PATH); + byte[] expected = encoded == null ? Hash.EMPTY_TRIE_HASH : Hash.sha3(encoded); + restoreRoot(expected); + return Arrays.copyOf(expected, expected.length); + } + private void importLeaves(Collection entries, String operation) { if (!leaves.isEmpty() || inheritedSnapshot != null || rootNode != null || materializedRoot != null || dirty) { @@ -251,7 +283,10 @@ private byte[] leafValue(BytesKey key) { if (leaves.containsKey(key)) { return leaves.get(key); } - return inheritedSnapshot == null ? null : inheritedSnapshot.leafValue(key); + if (inheritedSnapshot != null && inheritedSnapshot.containsLeaf(key)) { + return inheritedSnapshot.leafValue(key); + } + return valueAt(rootNode, toNibbles(key.bytes), 0, EMPTY_PATH); } private Map effectiveLeaves() { @@ -311,16 +346,17 @@ private static Node build(List entries, int depth) { return new BranchNode(children); } - private static Node update(Node node, byte[] key, int offset, byte[] value) { + private Node update(Node node, byte[] key, int offset, byte[] path, byte[] value) { if (node == null) { return value == null ? null : new LeafNode(Arrays.copyOfRange(key, offset, key.length), value); } + node = resolve(node, path); if (node instanceof LeafNode) { return updateLeaf((LeafNode) node, key, offset, value); } if (node instanceof ExtensionNode) { - return updateExtension((ExtensionNode) node, key, offset, value); + return updateExtension((ExtensionNode) node, key, offset, path, value); } BranchNode branch = (BranchNode) node; if (offset >= key.length) { @@ -328,13 +364,14 @@ private static Node update(Node node, byte[] key, int offset, byte[] value) { } int nibble = key[offset]; Node previous = branch.children[nibble]; - Node changed = update(previous, key, offset + 1, value); + Node changed = update(previous, key, offset + 1, + append(path, new byte[]{(byte) nibble}), value); if (previous == changed) { return branch; } Node[] children = Arrays.copyOf(branch.children, branch.children.length); children[nibble] = changed; - return normalizeBranch(children); + return normalizeBranch(children, path); } private static Node updateLeaf(LeafNode leaf, byte[] key, int offset, byte[] value) { @@ -364,15 +401,16 @@ private static Node updateLeaf(LeafNode leaf, byte[] key, int offset, byte[] val : new ExtensionNode(Arrays.copyOf(leaf.path, shared), branch); } - private static Node updateExtension(ExtensionNode extension, byte[] key, int offset, - byte[] value) { + private Node updateExtension(ExtensionNode extension, byte[] key, int offset, + byte[] path, byte[] value) { int shared = commonPrefix(extension.path, 0, key, offset); if (shared == extension.path.length) { - Node changed = update(extension.child, key, offset + shared, value); + Node changed = update(extension.child, key, offset + shared, + append(path, extension.path), value); if (changed == extension.child) { return extension; } - return normalizeExtension(extension.path, changed); + return normalizeExtension(extension.path, changed, path); } if (value == null) { return extension; @@ -390,7 +428,7 @@ private static Node updateExtension(ExtensionNode extension, byte[] key, int off : new ExtensionNode(Arrays.copyOf(extension.path, shared), branch); } - private static Node normalizeBranch(Node[] children) { + private Node normalizeBranch(Node[] children, byte[] path) { int count = 0; int only = -1; for (int i = 0; i < children.length; i++) { @@ -405,7 +443,7 @@ private static Node normalizeBranch(Node[] children) { if (count > 1) { return new BranchNode(children); } - Node child = children[only]; + Node child = resolve(children[only], append(path, new byte[]{(byte) only})); byte[] prefix = new byte[]{(byte) only}; if (child instanceof LeafNode) { LeafNode leaf = (LeafNode) child; @@ -418,19 +456,20 @@ private static Node normalizeBranch(Node[] children) { return new ExtensionNode(prefix, child); } - private static Node normalizeExtension(byte[] path, Node child) { + private Node normalizeExtension(byte[] extensionPath, Node child, byte[] parentPath) { if (child == null) { return null; } + child = resolve(child, append(parentPath, extensionPath)); if (child instanceof LeafNode) { LeafNode leaf = (LeafNode) child; - return new LeafNode(append(path, leaf.path), leaf.value); + return new LeafNode(append(extensionPath, leaf.path), leaf.value); } if (child instanceof ExtensionNode) { ExtensionNode extension = (ExtensionNode) child; - return new ExtensionNode(append(path, extension.path), extension.child); + return new ExtensionNode(append(extensionPath, extension.path), extension.child); } - return new ExtensionNode(path, child); + return new ExtensionNode(extensionPath, child); } private static Map collectNodes(Node root) { @@ -501,6 +540,176 @@ private static int commonPrefix(byte[] left, int leftOffset, byte[] right, int r return shared; } + private byte[] valueAt(Node node, byte[] key, int offset, byte[] path) { + if (node == null) { + return null; + } + Node present = resolve(node, path); + if (present instanceof LeafNode) { + LeafNode leaf = (LeafNode) present; + int remaining = key.length - offset; + return remaining == leaf.path.length + && commonPrefix(leaf.path, 0, key, offset) == remaining + ? Arrays.copyOf(leaf.value, leaf.value.length) : null; + } + if (present instanceof ExtensionNode) { + ExtensionNode extension = (ExtensionNode) present; + int shared = commonPrefix(extension.path, 0, key, offset); + return shared == extension.path.length + ? valueAt(extension.child, key, offset + shared, append(path, extension.path)) : null; + } + BranchNode branch = (BranchNode) present; + if (offset >= key.length) { + return null; + } + int nibble = key[offset]; + return valueAt(branch.children[nibble], key, offset + 1, + append(path, new byte[]{(byte) nibble})); + } + + private Node resolve(Node node, byte[] expectedPath) { + if (!(node instanceof StoredNode)) { + return node; + } + StoredNode stored = (StoredNode) node; + byte[] storedEncoding = node.encoded; + if (!Arrays.equals(stored.path, expectedPath)) { + throw new IllegalStateException("stored path trie node moved from its durable path"); + } + List elements = decodeList(storedEncoding); + Node decoded; + if (elements.size() == 17) { + if (!elements.get(16).isEmptyString()) { + throw new IllegalStateException("path-state branch contains a value slot"); + } + Node[] children = new Node[16]; + for (int index = 0; index < children.length; index++) { + byte[] childPath = append(expectedPath, new byte[]{(byte) index}); + children[index] = storedChild(elements.get(index), childPath); + } + decoded = new BranchNode(children); + } else if (elements.size() == 2 && !elements.get(0).list) { + Compact compact = decodeCompact(elements.get(0).payload); + if (compact.leaf) { + if (elements.get(1).list) { + throw new IllegalStateException("path-state leaf value must be an RLP item"); + } + decoded = new LeafNode(compact.path, elements.get(1).payload); + } else { + if (compact.path.length == 0) { + throw new IllegalStateException("path-state extension path must not be empty"); + } + decoded = new ExtensionNode(compact.path, + requiredStoredChild(elements.get(1), append(expectedPath, compact.path))); + } + } else { + throw new IllegalStateException("path-state durable node has invalid arity"); + } + if (!Arrays.equals(decoded.encoded, storedEncoding)) { + throw new IllegalStateException("path-state durable node is not canonically encoded"); + } + return decoded; + } + + private Node requiredStoredChild(RlpElement reference, byte[] path) { + Node child = storedChild(reference, path); + if (child == null) { + throw new IllegalStateException("path-state extension has an empty child"); + } + return child; + } + + private Node storedChild(RlpElement reference, byte[] path) { + if (reference.isEmptyString()) { + return null; + } + if (reference.list) { + return new StoredNode(reference.encoded, path); + } + if (reference.payload.length != SECURE_KEY_LENGTH) { + throw new IllegalStateException("path-state child hash must contain exactly 32 bytes"); + } + byte[] encoded = nodeStore.get(path); + if (encoded == null || !Arrays.equals(Hash.sha3(encoded), reference.payload)) { + throw new IllegalStateException("path-state durable child is missing or corrupt"); + } + return new StoredNode(encoded, path); + } + + private static List decodeList(byte[] encoded) { + RlpElement root = decodeElement(encoded, 0); + if (!root.list || root.encoded.length != encoded.length) { + throw new IllegalStateException("path-state durable node must be one RLP list"); + } + List elements = new ArrayList<>(); + int offset = 0; + while (offset < root.payload.length) { + RlpElement child = decodeElement(root.payload, offset); + elements.add(child); + offset += child.encoded.length; + } + return elements; + } + + private static RlpElement decodeElement(byte[] encoded, int offset) { + if (offset < 0 || offset >= encoded.length) { + throw new IllegalStateException("path-state RLP offset is invalid"); + } + int marker = encoded[offset] & 0xff; + if (marker < 0x80) { + return new RlpElement(false, new byte[]{encoded[offset]}, new byte[]{encoded[offset]}); + } + boolean list = marker >= 0xc0; + int shortBase = list ? 0xc0 : 0x80; + int longBase = list ? 0xf7 : 0xb7; + int payloadOffset; + int payloadLength; + if (marker <= longBase) { + payloadOffset = offset + 1; + payloadLength = marker - shortBase; + } else { + int lengthBytes = marker - longBase; + if (lengthBytes > Integer.BYTES || offset + 1 + lengthBytes > encoded.length) { + throw new IllegalStateException("path-state RLP length is invalid"); + } + payloadOffset = offset + 1 + lengthBytes; + payloadLength = 0; + for (int index = offset + 1; index < payloadOffset; index++) { + payloadLength = Math.addExact(Math.multiplyExact(payloadLength, 256), + encoded[index] & 0xff); + } + } + int end = Math.addExact(payloadOffset, payloadLength); + if (end > encoded.length) { + throw new IllegalStateException("path-state RLP payload exceeds its node"); + } + return new RlpElement(list, Arrays.copyOfRange(encoded, offset, end), + Arrays.copyOfRange(encoded, payloadOffset, end)); + } + + private static Compact decodeCompact(byte[] encoded) { + if (encoded.length == 0) { + throw new IllegalStateException("path-state compact path must not be empty"); + } + int flags = (encoded[0] >>> 4) & 0x0f; + if (flags > 3) { + throw new IllegalStateException("path-state compact path has invalid flags"); + } + boolean odd = (flags & 1) != 0; + byte[] path = new byte[encoded.length * 2 - (odd ? 1 : 2)]; + int target = 0; + if (odd) { + path[target++] = (byte) (encoded[0] & 0x0f); + } else if ((encoded[0] & 0x0f) != 0) { + throw new IllegalStateException("path-state compact path has non-zero padding"); + } + for (int index = 1; index < encoded.length; index++) { + path[target++] = (byte) ((encoded[index] >>> 4) & 0x0f); + path[target++] = (byte) (encoded[index] & 0x0f); + } + return new Compact((flags & 2) != 0, path); + } + private static byte[] nodeReference(byte[] encodedNode) { return encodedNode.length < SECURE_KEY_LENGTH ? encodedNode : rlpItem(Hash.sha3(encodedNode)); } @@ -606,6 +815,16 @@ private Node(byte[] encoded) { } } + private static final class StoredNode extends Node { + + private final byte[] path; + + private StoredNode(byte[] encoded, byte[] path) { + super(Arrays.copyOf(Objects.requireNonNull(encoded, "encoded"), encoded.length)); + this.path = Arrays.copyOf(Objects.requireNonNull(path, "path"), path.length); + } + } + private static final class LeafNode extends Node { private final byte[] path; @@ -683,6 +902,34 @@ private NodePath(Node node, byte[] path) { } } + private static final class RlpElement { + + private final boolean list; + private final byte[] encoded; + private final byte[] payload; + + private RlpElement(boolean list, byte[] encoded, byte[] payload) { + this.list = list; + this.encoded = encoded; + this.payload = payload; + } + + private boolean isEmptyString() { + return !list && payload.length == 0; + } + } + + private static final class Compact { + + private final boolean leaf; + private final byte[] path; + + private Compact(boolean leaf, byte[] path) { + this.leaf = leaf; + this.path = path; + } + } + @FunctionalInterface private interface NodeVisitor { @@ -716,6 +963,10 @@ private byte[] leafValue(BytesKey key) { return parent == null ? null : parent.leafValue(key); } + private boolean containsLeaf(BytesKey key) { + return leaves.containsKey(key) || parent != null && parent.containsLeaf(key); + } + private void populateLeaves(Map target) { if (parent != null) { parent.populateLeaves(target); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java index 1706588475b..0ad8f2081fb 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayer.java @@ -148,7 +148,7 @@ private static PathStateLayer begin(PathStateStoreManifest manifest, PathStateRoot childRoot = preparedTransition != null ? childStores.createRootFrom(preparedTransition) : parentSnapshot == null - ? childStores.createRootFrom(parentStores.leafRecords(), parentRoot.rootHash()) + ? childStores.createRootFrom(parentRoot.snapshot(), parentRoot.rootHash()) : childStores.createRootFrom(parentSnapshot, admittedParent.getStateRoot()); return new PathStateLayer(admitted, new PathStateLayerPublication(admitted, admittedLimits, faultHook), diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java index 1b50163956c..b9e83048cdd 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java @@ -55,6 +55,7 @@ public synchronized PathStateRootMetadata publish(PathStateNodeStoreSet stores, throw new IllegalArgumentException("path-state LAYER node database directory mismatch"); } requireCurrentParentOrChild(layer); + nodeStores.resolvePendingLeafValues(); nodeStores.releaseParentReadHandles(); limits.verifyAdmission(manifest, directory, layer, nodeStores.projectedLogicalBytes(layer)); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index 75406a3f1b0..e5a22931f3c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -30,6 +31,10 @@ public final class PathStateNodeStoreSet implements Closeable { private static final byte[] REBUILD_CHECKPOINT_KEY = new byte[]{ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 'r', 'e', 'b', 'u', 'i', 'l', 'd'}; + private static final byte[] STREAMED_REBUILD_KEY = new byte[]{ + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + 's', 't', 'r', 'e', 'a', 'm', 'e', 'd'}; + private static final byte[] STREAMED_REBUILD_VALUE = new byte[]{1}; private static final byte[] LEAF_OVERLAY_KEY = new byte[]{ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 'l', 'e', 'a', 'f', '-', 'o', 'v', 'e', 'r', 'l', 'a', 'y'}; @@ -45,6 +50,7 @@ public final class PathStateNodeStoreSet implements Closeable { private static final int LEAF_KEY_LENGTH = Integer.BYTES * 2 + PathMerkleTrie.SECURE_KEY_LENGTH; private static final byte[] LEAF_TOMBSTONE_PREFIX = ByteBuffer.allocate(Integer.BYTES) .putInt(LEAF_TOMBSTONE_DOMAIN).array(); + private static final int REBUILD_WRITE_BATCH_ENTRIES = 4096; private final Path directory; private final PathStateParticipantScope scope; @@ -52,6 +58,7 @@ public final class PathStateNodeStoreSet implements Closeable { private final Map> pending = new LinkedHashMap<>(); private final Map localLeaves = new LinkedHashMap<>(); private final Map persistedLeaves = new LinkedHashMap<>(); + private final Map resolvedLeafValues = new LinkedHashMap<>(); private final Set leafTombstones = new LinkedHashSet<>(); private final PathStateNativeNodeStore nativeStore; private final PathStateNodeStoreSet parentStores; @@ -64,6 +71,8 @@ public final class PathStateNodeStoreSet implements Closeable { private PathStateRebuildCheckpoint rebuildCheckpoint; private Long logicalBytes; private boolean leafOverlay; + private boolean streamedRebuild; + private boolean lazyRoots; private PathStateRoot root; private boolean rootClaimed; private boolean closed; @@ -84,6 +93,14 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K progress = decodeProgress(nativeStore.get(PROGRESS_KEY)); logicalBytes = decodeLogicalBytes(nativeStore.get(LOGICAL_BYTES_KEY)); rebuildCheckpoint = decodeRebuildCheckpoint(nativeStore.get(REBUILD_CHECKPOINT_KEY)); + byte[] streamedValue = nativeStore.get(STREAMED_REBUILD_KEY); + if (streamedValue != null && (kind != Kind.BASE + || !Arrays.equals(streamedValue, STREAMED_REBUILD_VALUE))) { + throw new IOException("path-state streamed rebuild marker is invalid"); + } + streamedRebuild = streamedValue != null; + lazyRoots = kind == Kind.BASE ? streamedRebuild + : parentStores != null && parentStores.lazyRoots; byte[] leafOverlayValue = nativeStore.get(LEAF_OVERLAY_KEY); if ((progress == null) != (logicalBytes == null)) { throw new IOException("path-state native progress and logical bytes marker differ"); @@ -106,13 +123,15 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K } leafOverlay = leafOverlayValue != null || kind == Kind.LAYER && progress == null && parentStores != null; - inheritParentLeaves(); - loadPersistedLeaves(); - loadLeafTombstones(); + if (!lazyRoots) { + inheritParentLeaves(); + loadPersistedLeaves(); + loadLeafTombstones(); + } validateNodeTombstones(); boolean hasUnexpectedLeaves = kind == Kind.BASE ? !persistedLeaves.isEmpty() : !localLeaves.isEmpty(); - if (progress == null && rebuildCheckpoint == null + if (progress == null && rebuildCheckpoint == null && !streamedRebuild && (hasUnexpectedLeaves || !leafTombstones.isEmpty())) { throw new IOException("path-state leaf inventory exists without native progress"); } @@ -213,8 +232,10 @@ public synchronized PathStateRoot createRoot() { participant -> participantStores.get(participant.getDbName()), superStore); if (progress != null || rebuildCheckpoint != null) { - if (progress == null && rebuildCheckpoint.hasIndependentStores()) { - candidate.restoreRebuildLeaves(restoredLeafRecords(), rebuildCheckpoint); + if (progress == null && rebuildCheckpoint.hasIndependentStores() && streamedRebuild) { + candidate.restoreRebuildParticipants(rebuildCheckpoint); + } else if (progress != null && lazyRoots) { + candidate.restoreStoredRoots(progress.getStateRoot()); } else { byte[] expectedRoot = progress == null ? rebuildCheckpoint.getPartialRoot() : progress.getStateRoot(); @@ -423,6 +444,74 @@ PathStateRebuildCheckpoint getRebuildCheckpoint() { return rebuildCheckpoint; } + /** Opens a bounded direct writer for one not-yet-checkpointed rebuild participant. */ + synchronized RebuildWriter openRebuildWriter(int storeId) throws IOException { + requireOpen(); + if (kind != Kind.BASE || sealed || progress != null || root == null) { + throw new IOException("path-state direct rebuild writer is not admissible"); + } + requireParticipant(storeId); + if (!streamedRebuild) { + nativeStore.writeBatch(Collections.singletonList( + PathStateNativeNodeStore.BatchMutation.put( + STREAMED_REBUILD_KEY, STREAMED_REBUILD_VALUE))); + streamedRebuild = true; + } + clearRebuildPrefix(ByteBuffer.allocate(Integer.BYTES).putInt(storeId).array()); + clearRebuildPrefix(ByteBuffer.allocate(Integer.BYTES * 2) + .putInt(LEAF_DOMAIN).putInt(storeId).array()); + return new RebuildWriter(storeId); + } + + private void clearRebuildPrefix(byte[] prefix) throws IOException { + List deletes = + new ArrayList<>(REBUILD_WRITE_BATCH_ENTRIES); + nativeStore.scanPrefix(prefix, entry -> { + deletes.add(PathStateNativeNodeStore.BatchMutation.delete(entry.getKey())); + if (deletes.size() >= REBUILD_WRITE_BATCH_ENTRIES) { + nativeStore.writeBatch(new ArrayList<>(deletes)); + deletes.clear(); + } + }); + if (!deletes.isEmpty()) { + nativeStore.writeBatch(deletes); + } + } + + /** Publishes only the Store-completion marker after its streamed nodes and leaves are durable. */ + synchronized void checkpointStreamedRebuild(PathStateRebuildCheckpoint checkpoint) + throws IOException { + requireOpen(); + if (kind != Kind.BASE || sealed || progress != null || root == null) { + throw new IOException("path-state streamed rebuild checkpoint is not admissible"); + } + PathStateRebuildCheckpoint next = Objects.requireNonNull(checkpoint, "checkpoint"); + requireRebuildCheckpointIdentity(next); + int previousCount = rebuildCheckpoint == null ? 0 + : rebuildCheckpoint.getCompletedStores().size(); + if (next.getCompletedStores().size() != previousCount + 1) { + throw new IOException("path-state streamed rebuild checkpoint must advance one Store"); + } + if (rebuildCheckpoint != null) { + for (PathStateRebuildCoordinator.StoreResult previous + : rebuildCheckpoint.getCompletedStores()) { + boolean retained = false; + for (PathStateRebuildCoordinator.StoreResult advanced : next.getCompletedStores()) { + if (sameStoreResult(previous, advanced)) { + retained = true; + break; + } + } + if (!retained) { + throw new IOException("path-state streamed rebuild rewrites completed Store"); + } + } + } + nativeStore.writeBatch(Collections.singletonList( + PathStateNativeNodeStore.BatchMutation.put(REBUILD_CHECKPOINT_KEY, next.encode()))); + rebuildCheckpoint = next; + } + synchronized long projectedLogicalBytes(PathStateRootMetadata metadata) throws IOException { requireOpen(); if (root == null) { @@ -513,6 +602,21 @@ synchronized void releaseParentReadHandles() throws IOException { } } + /** Resolves only this block's changed leaf baselines before inherited database handles close. */ + synchronized void resolvePendingLeafValues() { + requireOpen(); + if (root == null) { + throw new IllegalStateException("path-state node database set has no trie owner"); + } + for (PathStateRoot.LeafMutationRecord mutation : root.pendingLeafMutations()) { + byte[] key = leafKey(mutation); + BytesKey owned = new BytesKey(key); + if (!resolvedLeafValues.containsKey(owned)) { + resolvedLeafValues.put(owned, persistedLeafValue(key)); + } + } + } + @Override public synchronized void close() throws IOException { if (closed) { @@ -651,7 +755,7 @@ private void loadLeafTombstones() throws IOException { if (localLeaves.containsKey(leafKey)) { throw new IOException("path-state leaf and tombstone coexist"); } - if (!persistedLeaves.containsKey(leafKey)) { + if (parentStores == null || parentStores.persistedLeafValue(leafKey.copy()) == null) { throw new IOException("path-state leaf tombstone does not mask a parent leaf"); } leafTombstones.add(leafKey); @@ -834,7 +938,7 @@ private void appendLeafMutations(List mu for (PathStateRoot.LeafMutationRecord mutation : leafMutations) { byte[] key = leafKey(mutation); byte[] value = mutation.getEncodedValue(); - if (!Arrays.equals(persistedLeaves.get(new BytesKey(key)), value)) { + if (!Arrays.equals(persistedLeafValue(key), value)) { appendLeafMutation(mutations, key, value); } } @@ -860,7 +964,7 @@ private long projectedLeafBytes(long total, for (PathStateRoot.LeafMutationRecord mutation : leafMutations) { byte[] key = leafKey(mutation); byte[] value = mutation.getEncodedValue(); - if (!Arrays.equals(persistedLeaves.get(new BytesKey(key)), value)) { + if (!Arrays.equals(persistedLeafValue(key), value)) { projected = projectedLeafMutation(projected, key, value); } } @@ -903,6 +1007,24 @@ private void recordCommittedLeaves(List leafMu } } + private byte[] persistedLeafValue(byte[] key) { + BytesKey leafKey = new BytesKey(key); + if (resolvedLeafValues.containsKey(leafKey)) { + return resolvedLeafValues.get(leafKey); + } + if (persistedLeaves.containsKey(leafKey)) { + return persistedLeaves.get(leafKey); + } + byte[] local = nativeStore.get(key); + if (local != null) { + return local; + } + if (leafOverlay && nativeStore.get(leafTombstoneKey(key)) != null) { + return null; + } + return parentStores == null ? null : parentStores.persistedLeafValue(key); + } + private static byte[] leafKey(PathStateRoot.LeafMutationRecord mutation) { return ByteBuffer.allocate(LEAF_KEY_LENGTH) .putInt(LEAF_DOMAIN) @@ -911,6 +1033,18 @@ private static byte[] leafKey(PathStateRoot.LeafMutationRecord mutation) { .array(); } + private static byte[] rebuildLeafKey(int storeId, byte[] secureKey) { + byte[] key = Arrays.copyOf(Objects.requireNonNull(secureKey, "secureKey"), secureKey.length); + if (key.length != PathMerkleTrie.SECURE_KEY_LENGTH) { + throw new IllegalArgumentException("secureKey must contain exactly 32 bytes"); + } + return ByteBuffer.allocate(LEAF_KEY_LENGTH) + .putInt(LEAF_DOMAIN) + .putInt(storeId) + .put(key) + .array(); + } + private long projectedPendingBytes(long total) throws IOException { long projected = total; for (Map participantPending : pending.values()) { @@ -1083,6 +1217,54 @@ private byte[] key(byte[] path) { } } + final class RebuildWriter implements Closeable { + + private final int storeId; + private final NamespacedNodeStore namespace; + private final List mutations = + new ArrayList<>(REBUILD_WRITE_BATCH_ENTRIES); + private boolean writerClosed; + + private RebuildWriter(int storeId) { + this.storeId = storeId; + namespace = new NamespacedNodeStore(PathStateNodeStoreSet.this, storeId); + } + + void putNode(byte[] path, byte[] encodedNode) { + add(PathStateNativeNodeStore.BatchMutation.put(namespace.key(path), encodedNode)); + } + + void putLeaf(byte[] secureKey, byte[] encodedValue) { + add(PathStateNativeNodeStore.BatchMutation.put( + rebuildLeafKey(storeId, secureKey), encodedValue)); + } + + private void add(PathStateNativeNodeStore.BatchMutation mutation) { + if (writerClosed) { + throw new IllegalStateException("path-state rebuild writer is closed"); + } + mutations.add(mutation); + if (mutations.size() >= REBUILD_WRITE_BATCH_ENTRIES) { + flush(); + } + } + + void flush() { + if (!mutations.isEmpty()) { + nativeStore.writeBatch(new ArrayList<>(mutations)); + mutations.clear(); + } + } + + @Override + public void close() { + if (!writerClosed) { + flush(); + writerClosed = true; + } + } + } + private static final class BytesKey { private final byte[] bytes; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index 218ed830b04..bfda744b0e1 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -101,6 +102,7 @@ private RebuildResult rebuildInternal(PathStateStoreManifest manifest, SnapshotS PathStateRebuildCheckpoint checkpoint = stores.getRebuildCheckpoint(); Map completedStores = new TreeMap<>(); if (checkpoint != null) { + root.restoreRebuildParticipants(checkpoint); for (StoreResult completed : checkpoint.getCompletedStores()) { completedStores.put(completed.getStoreId(), completed); } @@ -124,8 +126,11 @@ private RebuildResult rebuildInternal(PathStateStoreManifest manifest, SnapshotS checkpoint.getSourceIdentityDigest())) { throw new IOException("path-state rebuild checkpoint source identity mismatch"); } - buildStoresInParallel(admittedManifest, admittedSource, identity, sourceIdentityDigest, - root, stores, completedStores); + try (StoreBuilders builders = new StoreBuilders(admittedManifest, sourceIdentityDigest, + stores)) { + buildStoresInParallel(admittedManifest, admittedSource, identity, sourceIdentityDigest, + root, stores, builders, completedStores); + } List storeResults = new ArrayList<>(completedStores.values()); long totalEntries = 0; for (StoreResult completed : storeResults) { @@ -155,7 +160,8 @@ private RebuildResult rebuildInternal(PathStateStoreManifest manifest, SnapshotS private void buildStoresInParallel(PathStateStoreManifest manifest, SnapshotSource source, SnapshotIdentity identity, byte[] sourceIdentityDigest, PathStateRoot root, - PathStateNodeStoreSet stores, Map completedStores) throws IOException { + PathStateNodeStoreSet stores, StoreBuilders builders, + Map completedStores) throws IOException { Object checkpointLock = new Object(); ExecutorService largeExecutor = Executors.newFixedThreadPool(LARGE_STORE_WORKERS, rebuildThreadFactory("large")); @@ -179,7 +185,7 @@ private void buildStoresInParallel(PathStateStoreManifest manifest, SnapshotSour ExecutorService executor = LARGE_STORES.contains(store.getDbName()) ? largeExecutor : smallExecutor; Future future = submitStore(executor, null, store, manifest, source, identity, - sourceIdentityDigest, root, stores, completedStores, checkpointLock); + sourceIdentityDigest, root, stores, builders, completedStores, checkpointLock); futures.add(future); if ("account".equals(store.getDbName())) { accountFuture = future; @@ -187,7 +193,8 @@ private void buildStoresInParallel(PathStateStoreManifest manifest, SnapshotSour } if (accountAsset != null) { futures.add(submitStore(largeExecutor, accountFuture, accountAsset, manifest, source, - identity, sourceIdentityDigest, root, stores, completedStores, checkpointLock)); + identity, sourceIdentityDigest, root, stores, builders, completedStores, + checkpointLock)); } Throwable failure = null; for (Future future : futures) { @@ -219,11 +226,19 @@ private void buildStoresInParallel(PathStateStoreManifest manifest, SnapshotSour private Future submitStore(ExecutorService executor, Future dependency, StoreIdentity store, PathStateStoreManifest manifest, SnapshotSource source, SnapshotIdentity identity, byte[] sourceIdentityDigest, PathStateRoot root, - PathStateNodeStoreSet stores, Map completedStores, + PathStateNodeStoreSet stores, StoreBuilders builders, + Map completedStores, Object checkpointLock) { return executor.submit(() -> { awaitDependency(dependency); - StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root); + StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root, + builders); + if (!"account-asset".equals(store.getDbName())) { + builders.reset(store.getDbName()); + } + if ("account".equals(store.getDbName())) { + builders.reset("account-asset"); + } logger.info("Path-state rebuild Store started: storeId={}, dbName={}, tier={}", store.getStoreId(), store.getDbName(), storeTier(store)); source.scan(store.getDbName(), accumulator::accept); @@ -237,7 +252,7 @@ private Future submitStore(ExecutorService executor, Future dependency, PathStateRebuildCheckpoint next = new PathStateRebuildCheckpoint( manifest.getIdentityDigest(), sourceIdentityDigest, identity, new ArrayList<>(completedStores.values())); - stores.checkpointRebuild(next, checkpointStoreIds(store)); + stores.checkpointStreamedRebuild(next); logger.info("Path-state rebuild Store checkpointed: storeId={}, dbName={}, entries={}, " + "completedStores={}, remainingStores={}", result.getStoreId(), result.getDbName(), result.getEntryCount(), @@ -262,13 +277,6 @@ private static void awaitDependency(Future dependency) throws IOException { } } - private static Collection checkpointStoreIds(StoreIdentity store) { - if ("account".equals(store.getDbName())) { - return Arrays.asList(store.getStoreId(), store.getStoreId() + 1); - } - return Collections.singletonList(store.getStoreId()); - } - private static ThreadFactory rebuildThreadFactory(String tier) { AtomicInteger sequence = new AtomicInteger(); return task -> { @@ -317,6 +325,7 @@ private final class StoreAccumulator { private final StoreIdentity store; private final P66Phase phase; private final PathStateRoot root; + private final StoreBuilders builders; private final Hasher inputDigest; private final long startedNanos = System.nanoTime(); private byte[] previousKey; @@ -325,10 +334,12 @@ private final class StoreAccumulator { private long valueBytes; private long lastProgressLogNanos = startedNanos; - private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root) { + private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root, + StoreBuilders builders) { this.store = store; this.phase = phase; this.root = root; + this.builders = builders; inputDigest = domainHasher(STORE_DIGEST_DOMAIN); putInt(inputDigest, store.getStoreId()); putString(inputDigest, store.getDbName()); @@ -346,12 +357,14 @@ private void accept(byte[] physicalKey, byte[] rawValue) throws IOException { } validateAccountLayout(key, value); PathStateMutation mutation = canonicalizer.put(phase, store.getDbName(), key, value); - root.applyRebuild(Collections.singletonList(mutation)); + builders.put(mutation); if ("account".equals(store.getDbName())) { List projected = canonicalizer.projectSnapshotAccountAssets( phase, key, value); if (!projected.isEmpty()) { - root.applyRebuild(projected); + for (PathStateMutation asset : projected) { + builders.put(asset); + } } } putBytes(inputDigest, key); @@ -369,9 +382,11 @@ private void validateAccountLayout(byte[] key, byte[] value) { } } - private StoreResult finish() { + private StoreResult finish() throws IOException { + byte[] storeRoot = builders.build(store.getDbName()); + root.completeRebuildParticipant(store.getDbName(), storeRoot); return new StoreResult(store.getStoreId(), store.getDbName(), entryCount, - inputDigest.hash().asBytes(), root.participantRoot(store.getDbName())); + inputDigest.hash().asBytes(), storeRoot); } private void logProgressIfDue() { @@ -401,6 +416,98 @@ private void logProgress(String status, long now) { } } + private final class StoreBuilders implements AutoCloseable { + + private final PathStateStoreManifest manifest; + private final byte[] generation; + private final PathStateNodeStoreSet stores; + private final Map opened = new LinkedHashMap<>(); + + private StoreBuilders(PathStateStoreManifest manifest, byte[] generation, + PathStateNodeStoreSet stores) { + this.manifest = manifest; + this.generation = Arrays.copyOf(generation, generation.length); + this.stores = stores; + } + + private synchronized void put(PathStateMutation mutation) throws IOException { + PathStateMutation present = Objects.requireNonNull(mutation, "mutation"); + if (present.isDelete()) { + throw new IOException("snapshot rebuild cannot spool a delete mutation"); + } + StoreIdentity identity = descriptor.require(present.getDbName()); + byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(identity.getStoreId(), + present.getCanonicalKey()); + byte[] encodedValue = PathStateCommitmentCodec.presentLeafValue( + present.getCanonicalValue()); + handle(identity).builder.put(secureKey, encodedValue); + } + + private synchronized byte[] build(String dbName) throws IOException { + BuilderHandle handle = handle(descriptor.require(dbName)); + byte[] root = handle.builder.build(); + handle.writer.close(); + return root; + } + + private synchronized void reset(String dbName) throws IOException { + handle(descriptor.require(dbName)).builder.reset(); + } + + private BuilderHandle handle(StoreIdentity identity) throws IOException { + BuilderHandle existing = opened.get(identity.getDbName()); + if (existing != null) { + return existing; + } + Path spool = manifest.getBaseDirectory().resolve("rebuild-spool") + .resolve(Integer.toString(identity.getStoreId())); + PathStateNodeStoreSet.RebuildWriter writer = stores.openRebuildWriter( + identity.getStoreId()); + try { + PathStateStoreTrieBuilder builder = new PathStateStoreTrieBuilder(spool, + manifest.getEngine(), generation, writer::putNode, writer::putLeaf); + BuilderHandle created = new BuilderHandle(builder, writer); + opened.put(identity.getDbName(), created); + return created; + } catch (IOException | RuntimeException failure) { + writer.close(); + throw failure; + } + } + + @Override + public synchronized void close() throws IOException { + IOException failure = null; + for (BuilderHandle handle : opened.values()) { + try { + handle.builder.close(); + handle.writer.close(); + } catch (IOException closeFailure) { + if (failure == null) { + failure = closeFailure; + } else { + failure.addSuppressed(closeFailure); + } + } + } + if (failure != null) { + throw failure; + } + } + } + + private static final class BuilderHandle { + + private final PathStateStoreTrieBuilder builder; + private final PathStateNodeStoreSet.RebuildWriter writer; + + private BuilderHandle(PathStateStoreTrieBuilder builder, + PathStateNodeStoreSet.RebuildWriter writer) { + this.builder = builder; + this.writer = writer; + } + } + private static int compare(StoreIdentity store, byte[] left, byte[] right) { if (PathStateParticipantDescriptor.MARKET_PRICE_COMPARATOR.equals( store.getComparatorId())) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index 676752a29a3..bdc5334e85b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -31,6 +31,7 @@ public final class PathStateRoot { private final PathStateParticipantScope scope; private final Map participantTries = new LinkedHashMap<>(); + private final Map rebuiltParticipantRoots = new LinkedHashMap<>(); private final Map> pendingLeafMutations = new LinkedHashMap<>(); private final PathMerkleTrie superTrie; @@ -114,6 +115,41 @@ void applyRebuild(Collection mutations) { rootMaterialized = false; } + /** Records a fully streamed participant root without retaining that Store's leaves or tree. */ + void completeRebuildParticipant(String dbName, byte[] storeRoot) { + PathStateParticipant participant = scope.require(dbName); + byte[] root = Arrays.copyOf(Objects.requireNonNull(storeRoot, "storeRoot"), + storeRoot.length); + if (root.length != PathStateCommitmentCodec.ROOT_LENGTH) { + throw new IllegalArgumentException("Store root must contain exactly 32 bytes"); + } + synchronized (rebuiltParticipantRoots) { + byte[] previous = rebuiltParticipantRoots.put(participant.getDbName(), root); + if (previous != null && !Arrays.equals(previous, root)) { + throw new IllegalStateException("path-state rebuild Store root changed"); + } + } + rootMaterialized = false; + } + + void restoreRebuildParticipants(PathStateRebuildCheckpoint checkpoint) { + for (PathStateRebuildCoordinator.StoreResult result + : Objects.requireNonNull(checkpoint, "checkpoint").getCompletedStores()) { + completeRebuildParticipant(result.getDbName(), result.getStoreRoot()); + } + } + + synchronized void restoreStoredRoots(byte[] expectedRoot) { + for (PathStateParticipant participant : scope.getParticipants()) { + participantTries.get(participant.getDbName()).restoreRoot(); + } + superTrie.restoreRoot(expectedRoot); + if (!Arrays.equals(superTrie.rootHash(), expectedRoot)) { + throw new IllegalStateException("restored path-state root differs from durable progress"); + } + rootMaterialized = true; + } + synchronized void recordPendingLeafMutations(Collection mutations) { recordPendingLeafMutations(prepare(mutations)); } @@ -131,6 +167,12 @@ private void recordPendingLeafMutations(List prepared) { public synchronized byte[] participantRoot(String dbName) { PathStateParticipant participant = scope.require(dbName); + synchronized (rebuiltParticipantRoots) { + byte[] rebuilt = rebuiltParticipantRoots.get(participant.getDbName()); + if (rebuilt != null) { + return Arrays.copyOf(rebuilt, rebuilt.length); + } + } return participantTries.get(participant.getDbName()).rootHash(); } @@ -140,7 +182,7 @@ public synchronized byte[] rootHash() { return superTrie.rootHash(); } for (PathStateParticipant participant : scope.getParticipants()) { - byte[] storeRoot = participantTries.get(participant.getDbName()).rootHash(); + byte[] storeRoot = participantRoot(participant.getDbName()); superTrie.put(PathStateCommitmentCodec.superLeafKey(participant.getStoreId()), PathStateCommitmentCodec.superLeafValue(participant.getStoreId(), participant.getDbName(), participant.getStoreFormatVersion(), storeRoot)); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStackTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStackTrie.java new file mode 100644 index 00000000000..e3de4abc1aa --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStackTrie.java @@ -0,0 +1,386 @@ +package org.tron.core.db2.stateroot; + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.Objects; +import org.tron.common.crypto.Hash; + +/** + * Ascending-key MPT builder that commits completed subtrees and retains only the right frontier. + * + *

This follows the memory boundary of Geth's StackTrie: once ascending input moves beyond a + * subtree, that subtree is encoded, emitted to the node sink, replaced by its reference, and its + * children are released. Keys must be fixed secure keys in strict unsigned order. + */ +final class PathStateStackTrie { + + private static final int EMPTY = 0; + private static final int BRANCH = 1; + private static final int EXTENSION = 2; + private static final int LEAF = 3; + private static final int HASHED = 4; + private static final byte[] EMPTY_RLP_ITEM = new byte[]{(byte) 0x80}; + + private final NodeSink sink; + private final Node root = new Node(); + private byte[] previousKey; + private boolean finished; + private long emittedNodes; + + PathStateStackTrie(NodeSink sink) { + this.sink = Objects.requireNonNull(sink, "sink"); + } + + void update(byte[] secureKey, byte[] encodedValue) { + requireOpen(); + byte[] key = secureKey(secureKey); + byte[] value = nonEmpty(encodedValue, "encodedValue"); + if (previousKey != null && compareUnsigned(previousKey, key) >= 0) { + throw new IllegalArgumentException("path-state stack trie keys must be strictly ascending"); + } + previousKey = key; + insert(root, toNibbles(key), value, new byte[0]); + } + + byte[] rootHash() { + if (!finished) { + hash(root, new byte[0]); + finished = true; + } + return root.type == EMPTY + ? Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length) + : Arrays.copyOf(root.value, root.value.length); + } + + long getEmittedNodes() { + return emittedNodes; + } + + int retainedNodes() { + return retainedNodes(root); + } + + private void requireOpen() { + if (finished) { + throw new IllegalStateException("path-state stack trie is already finished"); + } + } + + private void insert(Node node, byte[] key, byte[] value, byte[] path) { + switch (node.type) { + case EMPTY: + node.becomeLeaf(key, value); + return; + case BRANCH: + int branch = key[0] & 0xff; + for (int sibling = branch - 1; sibling >= 0; sibling--) { + if (node.children[sibling] != null) { + if (node.children[sibling].type != HASHED) { + hash(node.children[sibling], append(path, (byte) sibling)); + } + break; + } + } + if (node.children[branch] == null) { + node.children[branch] = Node.leaf(slice(key, 1), value); + } else { + insert(node.children[branch], slice(key, 1), value, append(path, key[0])); + } + return; + case EXTENSION: + splitExtension(node, key, value, path); + return; + case LEAF: + splitLeaf(node, key, value, path); + return; + case HASHED: + throw new IllegalStateException("path-state stack trie inserted into committed subtree"); + default: + throw new IllegalStateException("unknown path-state stack trie node"); + } + } + + private void splitExtension(Node node, byte[] key, byte[] value, byte[] path) { + int shared = commonPrefix(node.key, key); + if (shared == node.key.length) { + insert(node.children[0], slice(key, shared), value, + concatenate(path, slice(key, 0, shared))); + return; + } + Node oldChild; + if (shared < node.key.length - 1) { + oldChild = Node.extension(slice(node.key, shared + 1), node.children[0]); + hash(oldChild, concatenate(path, slice(node.key, 0, shared + 1))); + } else { + oldChild = node.children[0]; + hash(oldChild, concatenate(path, node.key)); + } + byte oldBranch = node.key[shared]; + byte newBranch = key[shared]; + Node branch = new Node(); + branch.type = BRANCH; + branch.children = new Node[16]; + branch.children[oldBranch & 0xff] = oldChild; + branch.children[newBranch & 0xff] = Node.leaf(slice(key, shared + 1), value); + if (shared == 0) { + node.copyFrom(branch); + } else { + node.type = EXTENSION; + node.key = slice(node.key, 0, shared); + node.value = null; + node.children = new Node[1]; + node.children[0] = branch; + } + } + + private void splitLeaf(Node node, byte[] key, byte[] value, byte[] path) { + int shared = commonPrefix(node.key, key); + if (shared >= node.key.length || shared >= key.length) { + throw new IllegalArgumentException("path-state stack trie duplicate or prefixed key"); + } + Node branch = new Node(); + branch.type = BRANCH; + branch.children = new Node[16]; + byte oldBranch = node.key[shared]; + byte newBranch = key[shared]; + Node oldLeaf = Node.leaf(slice(node.key, shared + 1), node.value); + hash(oldLeaf, concatenate(path, slice(node.key, 0, shared + 1))); + branch.children[oldBranch & 0xff] = oldLeaf; + branch.children[newBranch & 0xff] = Node.leaf(slice(key, shared + 1), value); + if (shared == 0) { + node.copyFrom(branch); + } else { + node.type = EXTENSION; + node.key = slice(node.key, 0, shared); + node.value = null; + node.children = new Node[]{branch}; + } + } + + private void hash(Node node, byte[] path) { + if (node.type == HASHED) { + return; + } + byte[] encoded; + switch (node.type) { + case EMPTY: + node.type = HASHED; + node.key = null; + node.children = null; + node.value = Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length); + return; + case BRANCH: + byte[][] children = new byte[17][]; + for (int index = 0; index < 16; index++) { + Node child = node.children[index]; + if (child == null) { + children[index] = EMPTY_RLP_ITEM; + } else { + hash(child, append(path, (byte) index)); + children[index] = nodeReference(child.value); + node.children[index] = null; + } + } + children[16] = EMPTY_RLP_ITEM; + encoded = rlpList(children); + break; + case EXTENSION: + hash(node.children[0], concatenate(path, node.key)); + encoded = rlpList(rlpItem(compactPath(node.key, false)), + nodeReference(node.children[0].value)); + node.children[0] = null; + break; + case LEAF: + encoded = rlpList(rlpItem(compactPath(node.key, true)), rlpItem(node.value)); + break; + default: + throw new IllegalStateException("unknown path-state stack trie node"); + } + node.type = HASHED; + node.key = null; + node.children = null; + node.value = encoded.length < 32 && path.length != 0 + ? encoded : Hash.sha3(encoded); + // Path-state addresses nodes by trie path, so persist embedded nodes as well as hashed nodes. + // The parent reference still follows canonical MPT embedding rules. + sink.put(path, encoded); + emittedNodes++; + } + + private static byte[] nodeReference(byte[] value) { + return value.length < 32 ? Arrays.copyOf(value, value.length) : rlpItem(value); + } + + private static byte[] compactPath(byte[] nibbles, boolean leaf) { + int odd = nibbles.length & 1; + int flags = (leaf ? 2 : 0) + odd; + byte[] encoded = new byte[1 + nibbles.length / 2]; + int source = 0; + if (odd == 1) { + encoded[0] = (byte) ((flags << 4) | nibbles[source++]); + } else { + encoded[0] = (byte) (flags << 4); + } + int target = 1; + while (source < nibbles.length) { + encoded[target++] = (byte) ((nibbles[source++] << 4) | nibbles[source++]); + } + return encoded; + } + + private static byte[] rlpItem(byte[] raw) { + byte[] value = Objects.requireNonNull(raw, "raw"); + if (value.length == 1 && (value[0] & 0xff) < 0x80) { + return Arrays.copyOf(value, value.length); + } + byte[] prefix = rlpLength(value.length, 0x80, 0xb7); + return concatenate(prefix, value); + } + + private static byte[] rlpList(byte[]... items) { + int length = 0; + for (byte[] item : items) { + length = Math.addExact(length, item.length); + } + byte[] prefix = rlpLength(length, 0xc0, 0xf7); + ByteArrayOutputStream output = new ByteArrayOutputStream(prefix.length + length); + output.write(prefix, 0, prefix.length); + for (byte[] item : items) { + output.write(item, 0, item.length); + } + return output.toByteArray(); + } + + private static byte[] rlpLength(int length, int shortOffset, int longOffset) { + if (length <= 55) { + return new byte[]{(byte) (shortOffset + length)}; + } + int bytes = Integer.BYTES - Integer.numberOfLeadingZeros(length) / Byte.SIZE; + byte[] encoded = new byte[bytes + 1]; + encoded[0] = (byte) (longOffset + bytes); + for (int index = bytes; index > 0; index--) { + encoded[index] = (byte) length; + length >>>= Byte.SIZE; + } + return encoded; + } + + private static int retainedNodes(Node node) { + if (node == null) { + return 0; + } + int count = 1; + if (node.children != null) { + for (Node child : node.children) { + count += retainedNodes(child); + } + } + return count; + } + + private static int commonPrefix(byte[] left, byte[] right) { + int length = Math.min(left.length, right.length); + int index = 0; + while (index < length && left[index] == right[index]) { + index++; + } + return index; + } + + private static byte[] toNibbles(byte[] key) { + byte[] nibbles = new byte[key.length * 2]; + for (int index = 0; index < key.length; index++) { + nibbles[index * 2] = (byte) ((key[index] >>> 4) & 0x0f); + nibbles[index * 2 + 1] = (byte) (key[index] & 0x0f); + } + return nibbles; + } + + private static byte[] append(byte[] value, byte suffix) { + byte[] result = Arrays.copyOf(value, value.length + 1); + result[value.length] = suffix; + return result; + } + + private static byte[] concatenate(byte[] left, byte[] right) { + byte[] result = Arrays.copyOf(left, left.length + right.length); + System.arraycopy(right, 0, result, left.length, right.length); + return result; + } + + private static byte[] slice(byte[] value, int from) { + return slice(value, from, value.length); + } + + private static byte[] slice(byte[] value, int from, int to) { + return Arrays.copyOfRange(value, from, to); + } + + private static int compareUnsigned(byte[] left, byte[] right) { + for (int index = 0; index < Math.min(left.length, right.length); index++) { + int compared = Integer.compare(left[index] & 0xff, right[index] & 0xff); + if (compared != 0) { + return compared; + } + } + return Integer.compare(left.length, right.length); + } + + private static byte[] secureKey(byte[] value) { + byte[] key = Arrays.copyOf(Objects.requireNonNull(value, "secureKey"), value.length); + if (key.length != PathMerkleTrie.SECURE_KEY_LENGTH) { + throw new IllegalArgumentException("secureKey must contain exactly 32 bytes"); + } + return key; + } + + private static byte[] nonEmpty(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length == 0) { + throw new IllegalArgumentException(name + " must not be empty"); + } + return copy; + } + + @FunctionalInterface + interface NodeSink { + + void put(byte[] path, byte[] encodedNode); + } + + private static final class Node { + + private int type; + private byte[] key; + private byte[] value; + private Node[] children; + + private static Node leaf(byte[] key, byte[] value) { + Node node = new Node(); + node.becomeLeaf(key, value); + return node; + } + + private static Node extension(byte[] key, Node child) { + Node node = new Node(); + node.type = EXTENSION; + node.key = key; + node.children = new Node[]{child}; + return node; + } + + private void becomeLeaf(byte[] nextKey, byte[] nextValue) { + type = LEAF; + key = Arrays.copyOf(nextKey, nextKey.length); + value = Arrays.copyOf(nextValue, nextValue.length); + children = null; + } + + private void copyFrom(Node source) { + type = source.type; + key = source.key; + value = source.value; + children = source.children; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java new file mode 100644 index 00000000000..f75aae9e086 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java @@ -0,0 +1,178 @@ +package org.tron.core.db2.stateroot; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateNativeNodeStore.BatchMutation; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Disk-sorted, bounded-memory builder for one participant Store trie. */ +final class PathStateStoreTrieBuilder implements Closeable { + + static final int DEFAULT_WRITE_BATCH_ROWS = 4096; + + private final PathStateNativeNodeStore spool; + private final PathStateStackTrie.NodeSink nodeSink; + private final LeafSink leafSink; + private final byte[] generationPrefix; + private final List pendingRows = new ArrayList<>(DEFAULT_WRITE_BATCH_ROWS); + private boolean built; + private long inputRows; + private long sortedRows; + + PathStateStoreTrieBuilder(Path spoolDirectory, Engine engine, + PathStateStackTrie.NodeSink nodeSink) throws IOException { + this(spoolDirectory, engine, new byte[0], nodeSink, + (secureKey, encodedValue) -> { }); + } + + PathStateStoreTrieBuilder(Path spoolDirectory, Engine engine, + PathStateStackTrie.NodeSink nodeSink, LeafSink leafSink) throws IOException { + this(spoolDirectory, engine, new byte[0], nodeSink, leafSink); + } + + PathStateStoreTrieBuilder(Path spoolDirectory, Engine engine, byte[] generationPrefix, + PathStateStackTrie.NodeSink nodeSink, LeafSink leafSink) throws IOException { + spool = PathStateNativeNodeStore.open(Objects.requireNonNull(spoolDirectory, "spoolDirectory"), + Objects.requireNonNull(engine, "engine")); + this.nodeSink = Objects.requireNonNull(nodeSink, "nodeSink"); + this.leafSink = Objects.requireNonNull(leafSink, "leafSink"); + this.generationPrefix = Arrays.copyOf( + Objects.requireNonNull(generationPrefix, "generationPrefix"), generationPrefix.length); + } + + /** + * Spools one canonical leaf by its secure key. Repeated keys overwrite the prior value. + * + *

Only one bounded native write batch is retained in heap; values already flushed remain in + * the temporary database until the sorted build pass reads them. + */ + void put(byte[] secureKey, byte[] encodedValue) { + requireCollecting(); + byte[] key = requireSecureKey(secureKey); + byte[] value = nonEmpty(encodedValue, "encodedValue"); + pendingRows.add(BatchMutation.put(spoolKey(key), value)); + inputRows = Math.addExact(inputRows, 1L); + if (pendingRows.size() >= DEFAULT_WRITE_BATCH_ROWS) { + flushRows(); + } + } + + /** Removes an incomplete current-generation spool before that Store is rescanned. */ + void reset() throws IOException { + requireCollecting(); + pendingRows.clear(); + List deletes = new ArrayList<>(DEFAULT_WRITE_BATCH_ROWS); + PathStateNativeNodeStore.EntryConsumer consumer = entry -> { + deletes.add(BatchMutation.delete(entry.getKey())); + if (deletes.size() >= DEFAULT_WRITE_BATCH_ROWS) { + spool.writeBatch(new ArrayList<>(deletes)); + deletes.clear(); + } + }; + if (generationPrefix.length == 0) { + spool.scanAll(consumer); + } else { + spool.scanPrefix(generationPrefix, consumer); + } + if (!deletes.isEmpty()) { + spool.writeBatch(deletes); + } + inputRows = 0; + sortedRows = 0; + } + + /** Flushes the spool, streams it in secure-key order, and returns the canonical Store root. */ + byte[] build() throws IOException { + requireCollecting(); + flushRows(); + PathStateStackTrie trie = new PathStateStackTrie(nodeSink); + PathStateNativeNodeStore.EntryConsumer consumer = entry -> { + byte[] key = secureKey(entry.getKey()); + byte[] value = entry.getValue(); + trie.update(key, value); + leafSink.put(key, value); + sortedRows = Math.addExact(sortedRows, 1L); + }; + if (generationPrefix.length == 0) { + spool.scanAll(consumer); + } else { + spool.scanPrefix(generationPrefix, consumer); + } + built = true; + return trie.rootHash(); + } + + long getInputRows() { + return inputRows; + } + + long getSortedRows() { + return sortedRows; + } + + int getPendingRows() { + return pendingRows.size(); + } + + private void flushRows() { + if (!pendingRows.isEmpty()) { + spool.writeBatch(new ArrayList<>(pendingRows)); + pendingRows.clear(); + } + } + + private void requireCollecting() { + if (built) { + throw new IllegalStateException("path-state Store trie builder is already built"); + } + } + + private static byte[] requireSecureKey(byte[] value) { + byte[] key = Arrays.copyOf(Objects.requireNonNull(value, "secureKey"), value.length); + if (key.length != PathMerkleTrie.SECURE_KEY_LENGTH) { + throw new IllegalArgumentException("secureKey must contain exactly 32 bytes"); + } + return key; + } + + private byte[] spoolKey(byte[] secureKey) { + byte[] key = Arrays.copyOf(generationPrefix, + generationPrefix.length + secureKey.length); + System.arraycopy(secureKey, 0, key, generationPrefix.length, secureKey.length); + return key; + } + + private byte[] secureKey(byte[] spoolKey) { + if (spoolKey.length != generationPrefix.length + PathMerkleTrie.SECURE_KEY_LENGTH) { + throw new IllegalStateException("path-state Store spool key has invalid length"); + } + return Arrays.copyOfRange(spoolKey, generationPrefix.length, spoolKey.length); + } + + private static byte[] nonEmpty(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length == 0) { + throw new IllegalArgumentException(name + " must not be empty"); + } + return copy; + } + + @Override + public void close() throws IOException { + if (!built) { + flushRows(); + } + spool.close(); + } + + @FunctionalInterface + interface LeafSink { + + void put(byte[] secureKey, byte[] encodedValue); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java index 95ba5701fdb..d95fd587da3 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.LinkedHashMap; @@ -145,6 +146,34 @@ public void singleLeafUpdateRewritesOnlyItsMaterializedPath() { trie.verifyNodeStore(); } + @Test + public void restoresRootAndLoadsOnlyTheChangedPath() { + int leafCount = 1_000; + byte[][] keys = new byte[leafCount][]; + byte[][] values = new byte[leafCount][]; + InMemoryPathNodeStore sourceStore = new InMemoryPathNodeStore(); + PathMerkleTrie source = new PathMerkleTrie(sourceStore); + for (int index = 0; index < leafCount; index++) { + keys[index] = indexedKey(index); + values[index] = value("value-" + index); + source.put(keys[index], values[index]); + } + byte[] originalRoot = source.rootHash(); + + InMemoryPathNodeStore lazyStore = new InMemoryPathNodeStore(); + lazyStore.nodes.putAll(sourceStore.nodes); + PathMerkleTrie restored = new PathMerkleTrie(lazyStore); + restored.restoreRoot(originalRoot); + assertEquals(1, lazyStore.gets); + assertArrayEquals(values[517], restored.get(keys[517])); + assertTrue(lazyStore.gets <= PathMerkleTrie.SECURE_KEY_LENGTH * 2 + 2); + + values[517] = value("updated-lazily"); + restored.put(keys[517], values[517]); + assertArrayEquals(referenceRoot(keys, values), restored.rootHash()); + assertTrue(restored.getLastNodePuts() <= PathMerkleTrie.SECURE_KEY_LENGTH * 2 + 1); + } + private static byte[] referenceRoot(byte[][] keys, byte[][] values) { TrieImpl reference = new TrieImpl(); reference.setAsync(false); @@ -166,6 +195,12 @@ private static byte[] keyWithTail(int prefix, int tail) { return key; } + private static byte[] indexedKey(int index) { + byte[] key = new byte[PathMerkleTrie.SECURE_KEY_LENGTH]; + ByteBuffer.wrap(key, key.length - Integer.BYTES, Integer.BYTES).putInt(index); + return key; + } + private static byte[] value(String value) { return PathStateCommitmentCodec.presentLeafValue(value.getBytes(StandardCharsets.UTF_8)); } @@ -181,9 +216,11 @@ private static void assertNodeMapsEqual(Map expected, private static final class InMemoryPathNodeStore implements PathNodeStore { private final Map nodes = new LinkedHashMap<>(); + private int gets; @Override public byte[] get(byte[] path) { + gets++; byte[] value = nodes.get(Hex.toHexString(path)); return value == null ? null : Arrays.copyOf(value, value.length); } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateStackTrieTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateStackTrieTest.java new file mode 100644 index 00000000000..5aaafc853dc --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateStackTrieTest.java @@ -0,0 +1,139 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import org.junit.Test; +import org.tron.common.crypto.Hash; + +public class PathStateStackTrieTest { + + private static final Comparator UNSIGNED = (left, right) -> { + for (int index = 0; index < left.key.length; index++) { + int compared = Integer.compare(left.key[index] & 0xff, right.key[index] & 0xff); + if (compared != 0) { + return compared; + } + } + return 0; + }; + + @Test + public void matchesReferenceRootAndEmitsDurableNodes() { + List rows = rows(10_000, 19L); + Map emitted = new LinkedHashMap<>(); + PathStateStackTrie stack = new PathStateStackTrie( + (path, encoded) -> emitted.put(hex(path), encoded)); + PathMerkleTrie reference = new PathMerkleTrie(new MemoryNodeStore()); + + for (Row row : rows) { + stack.update(row.key, row.value); + reference.put(row.key, row.value); + } + + byte[] root = stack.rootHash(); + assertArrayEquals(reference.rootHash(), root); + assertArrayEquals(root, Hash.sha3(emitted.get(""))); + assertTrue(stack.getEmittedNodes() > 1); + } + + @Test + public void retainsOnlyAscendingFrontierForLargeInput() { + PathStateStackTrie stack = new PathStateStackTrie((path, encoded) -> { }); + for (Row row : rows(100_000, 41L)) { + stack.update(row.key, row.value); + assertTrue(stack.retainedNodes() <= 1024); + } + stack.rootHash(); + assertTrue(stack.retainedNodes() <= 1); + } + + @Test + public void rejectsDuplicateAndDescendingKeys() { + byte[] low = key(1); + byte[] high = key(2); + PathStateStackTrie duplicate = new PathStateStackTrie((path, encoded) -> { }); + duplicate.update(low, new byte[]{1}); + assertThrows(IllegalArgumentException.class, + () -> duplicate.update(low, new byte[]{2})); + + PathStateStackTrie descending = new PathStateStackTrie((path, encoded) -> { }); + descending.update(high, new byte[]{1}); + assertThrows(IllegalArgumentException.class, + () -> descending.update(low, new byte[]{2})); + } + + @Test + public void returnsCanonicalEmptyRoot() { + assertArrayEquals(Hash.EMPTY_TRIE_HASH, + new PathStateStackTrie((path, encoded) -> { }).rootHash()); + } + + private static List rows(int count, long seed) { + Random random = new Random(seed); + List rows = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + byte[] key = new byte[PathMerkleTrie.SECURE_KEY_LENGTH]; + random.nextBytes(key); + ByteBuffer.wrap(key, key.length - Integer.BYTES, Integer.BYTES).putInt(index); + byte[] value = new byte[8 + random.nextInt(96)]; + random.nextBytes(value); + rows.add(new Row(key, value)); + } + rows.sort(UNSIGNED); + return rows; + } + + private static byte[] key(int suffix) { + byte[] key = new byte[PathMerkleTrie.SECURE_KEY_LENGTH]; + key[key.length - 1] = (byte) suffix; + return key; + } + + private static String hex(byte[] value) { + StringBuilder result = new StringBuilder(value.length * 2); + for (byte present : value) { + result.append(String.format("%02x", present & 0xff)); + } + return result.toString(); + } + + private static final class Row { + + private final byte[] key; + private final byte[] value; + + private Row(byte[] key, byte[] value) { + this.key = key; + this.value = value; + } + } + + private static final class MemoryNodeStore implements PathNodeStore { + + private final Map nodes = new LinkedHashMap<>(); + + @Override + public byte[] get(byte[] path) { + return nodes.get(hex(path)); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + nodes.put(hex(path), encodedNode); + } + + @Override + public void delete(byte[] path) { + nodes.remove(hex(path)); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilderTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilderTest.java new file mode 100644 index 00000000000..cc21395bcaa --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilderTest.java @@ -0,0 +1,61 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.Random; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateStoreTrieBuilderTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void diskSortsUnorderedRowsAndBoundsPendingValues() throws Exception { + Path spool = temporaryFolder.newFolder("store-spool").toPath(); + PathMerkleTrie reference = new PathMerkleTrie(new MemoryNodeStore()); + Random random = new Random(71L); + int count = PathStateStoreTrieBuilder.DEFAULT_WRITE_BATCH_ROWS * 3 + 17; + + try (PathStateStoreTrieBuilder builder = new PathStateStoreTrieBuilder(spool, + Engine.ROCKSDB, (path, encoded) -> { })) { + for (int index = count - 1; index >= 0; index--) { + byte[] key = new byte[PathMerkleTrie.SECURE_KEY_LENGTH]; + ByteBuffer.wrap(key, key.length - Integer.BYTES, Integer.BYTES).putInt(index); + byte[] value = new byte[64 + random.nextInt(128)]; + random.nextBytes(value); + builder.put(key, value); + reference.put(key, value); + assertTrue(builder.getPendingRows() < PathStateStoreTrieBuilder.DEFAULT_WRITE_BATCH_ROWS); + } + + assertArrayEquals(reference.rootHash(), builder.build()); + assertEquals(count, builder.getInputRows()); + assertEquals(count, builder.getSortedRows()); + assertEquals(0, builder.getPendingRows()); + } + } + + private static final class MemoryNodeStore implements PathNodeStore { + + @Override + public byte[] get(byte[] path) { + return null; + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + } + + @Override + public void delete(byte[] path) { + } + } +} From 6bf5369fbeddf5212cdeb85cf11625897509bae7 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sun, 30 Aug 2026 18:14:06 +0800 Subject: [PATCH 097/161] fix(chainbase): migrate rebuild checkpoints lazily --- .../org/tron/core/db2/stateroot/PathStateNodeStoreSet.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index e5a22931f3c..da61a99734d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -100,6 +100,7 @@ private PathStateNodeStoreSet(Path directory, PathStateStoreManifest manifest, K } streamedRebuild = streamedValue != null; lazyRoots = kind == Kind.BASE ? streamedRebuild + || rebuildCheckpoint != null && rebuildCheckpoint.hasIndependentStores() : parentStores != null && parentStores.lazyRoots; byte[] leafOverlayValue = nativeStore.get(LEAF_OVERLAY_KEY); if ((progress == null) != (logicalBytes == null)) { @@ -232,7 +233,7 @@ public synchronized PathStateRoot createRoot() { participant -> participantStores.get(participant.getDbName()), superStore); if (progress != null || rebuildCheckpoint != null) { - if (progress == null && rebuildCheckpoint.hasIndependentStores() && streamedRebuild) { + if (progress == null && rebuildCheckpoint.hasIndependentStores()) { candidate.restoreRebuildParticipants(rebuildCheckpoint); } else if (progress != null && lazyRoots) { candidate.restoreStoredRoots(progress.getStateRoot()); From e65d50e12eda3780116d1c8b1653afb0a0b93c77 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sun, 30 Aug 2026 18:24:15 +0800 Subject: [PATCH 098/161] fix(chainbase): skip absent asset projections --- .../PathStateRebuildCoordinator.java | 69 +++++++++++-------- .../PathStateRebuildCoordinatorTest.java | 5 +- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index bfda744b0e1..7b8b177b2fd 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -230,36 +230,42 @@ private Future submitStore(ExecutorService executor, Future dependency, Map completedStores, Object checkpointLock) { return executor.submit(() -> { - awaitDependency(dependency); - StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root, - builders); - if (!"account-asset".equals(store.getDbName())) { - builders.reset(store.getDbName()); - } - if ("account".equals(store.getDbName())) { - builders.reset("account-asset"); - } - logger.info("Path-state rebuild Store started: storeId={}, dbName={}, tier={}", - store.getStoreId(), store.getDbName(), storeTier(store)); - source.scan(store.getDbName(), accumulator::accept); - StoreResult result = accumulator.finish(); - accumulator.logCompleted(); - if ("account".equals(store.getDbName())) { - root.participantRoot("account-asset"); - } - synchronized (checkpointLock) { - completedStores.put(result.getStoreId(), result); - PathStateRebuildCheckpoint next = new PathStateRebuildCheckpoint( - manifest.getIdentityDigest(), sourceIdentityDigest, identity, - new ArrayList<>(completedStores.values())); - stores.checkpointStreamedRebuild(next); - logger.info("Path-state rebuild Store checkpointed: storeId={}, dbName={}, entries={}, " - + "completedStores={}, remainingStores={}", - result.getStoreId(), result.getDbName(), result.getEntryCount(), - completedStores.size(), descriptor.getStores().size() - completedStores.size()); + try { + awaitDependency(dependency); + StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root, + builders); + if (!"account-asset".equals(store.getDbName())) { + builders.reset(store.getDbName()); + } + if ("account".equals(store.getDbName())) { + builders.reset("account-asset"); + } + logger.info("Path-state rebuild Store started: storeId={}, dbName={}, tier={}", + store.getStoreId(), store.getDbName(), storeTier(store)); + source.scan(store.getDbName(), accumulator::accept); + StoreResult result = accumulator.finish(); + accumulator.logCompleted(); + if ("account".equals(store.getDbName())) { + root.participantRoot("account-asset"); + } + synchronized (checkpointLock) { + completedStores.put(result.getStoreId(), result); + PathStateRebuildCheckpoint next = new PathStateRebuildCheckpoint( + manifest.getIdentityDigest(), sourceIdentityDigest, identity, + new ArrayList<>(completedStores.values())); + stores.checkpointStreamedRebuild(next); + logger.info("Path-state rebuild Store checkpointed: storeId={}, dbName={}, entries={}, " + + "completedStores={}, remainingStores={}", + result.getStoreId(), result.getDbName(), result.getEntryCount(), + completedStores.size(), descriptor.getStores().size() - completedStores.size()); + } + faultHook.afterStore(result); + return null; + } catch (IOException | RuntimeException failure) { + logger.error("Path-state rebuild Store failed: storeId={}, dbName={}, tier={}", + store.getStoreId(), store.getDbName(), storeTier(store), failure); + throw failure; } - faultHook.afterStore(result); - return null; }); } @@ -363,7 +369,10 @@ private void accept(byte[] physicalKey, byte[] rawValue) throws IOException { phase, key, value); if (!projected.isEmpty()) { for (PathStateMutation asset : projected) { - builders.put(asset); + // A zero embedded balance projects to ABSENT; snapshot rebuilds spool PRESENT rows. + if (!asset.isDelete()) { + builders.put(asset); + } } } } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index 810ba0470bb..315fc4dad25 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -230,7 +230,10 @@ public void admitsOnlyTargetP66AccountAssetPhysicalLayout() throws Exception { PathStateStoreManifest lazyManifest = manifest("p66-on-lazy", Engine.ROCKSDB); TestSnapshotSource lazy = exactSource(identity(P66Phase.P66_ON)); lazy.add("account", address, - account(address).toBuilder().putAssetV2(tokenId, 11L).build().toByteArray()); + account(address).toBuilder() + .putAssetV2(tokenId, 11L) + .putAssetV2("1000002", 0L) + .build().toByteArray()); RebuildResult lazyResult = new PathStateRebuildCoordinator().rebuild(lazyManifest, lazy); assertEquals(1, lazyResult.requireStore("account").getEntryCount()); assertEquals(0, lazyResult.requireStore("account-asset").getEntryCount()); From a211a07732d246e1040617f4e96704986e9f6a29 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sun, 30 Aug 2026 19:20:32 +0800 Subject: [PATCH 099/161] feat(chainbase): report trie build progress --- .../db2/stateroot/PathStateNodeStoreSet.java | 12 ++++++++ .../PathStateRebuildCoordinator.java | 14 +++++++++- .../stateroot/PathStateStoreTrieBuilder.java | 28 +++++++++++++++++++ .../PathStateStoreTrieBuilderTest.java | 6 +++- 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index da61a99734d..1db5ad34c91 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -1224,6 +1224,8 @@ final class RebuildWriter implements Closeable { private final NamespacedNodeStore namespace; private final List mutations = new ArrayList<>(REBUILD_WRITE_BATCH_ENTRIES); + private long nodeEntries; + private long leafEntries; private boolean writerClosed; private RebuildWriter(int storeId) { @@ -1233,11 +1235,21 @@ private RebuildWriter(int storeId) { void putNode(byte[] path, byte[] encodedNode) { add(PathStateNativeNodeStore.BatchMutation.put(namespace.key(path), encodedNode)); + nodeEntries = Math.addExact(nodeEntries, 1L); } void putLeaf(byte[] secureKey, byte[] encodedValue) { add(PathStateNativeNodeStore.BatchMutation.put( rebuildLeafKey(storeId, secureKey), encodedValue)); + leafEntries = Math.addExact(leafEntries, 1L); + } + + long getNodeEntries() { + return nodeEntries; + } + + long getLeafEntries() { + return leafEntries; } private void add(PathStateNativeNodeStore.BatchMutation mutation) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index 7b8b177b2fd..cb09060fb24 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -474,7 +474,9 @@ private BuilderHandle handle(StoreIdentity identity) throws IOException { identity.getStoreId()); try { PathStateStoreTrieBuilder builder = new PathStateStoreTrieBuilder(spool, - manifest.getEngine(), generation, writer::putNode, writer::putLeaf); + manifest.getEngine(), generation, writer::putNode, writer::putLeaf, + (sortedRows, elapsedMillis) -> logBuildProgress(identity, writer, sortedRows, + elapsedMillis)); BuilderHandle created = new BuilderHandle(builder, writer); opened.put(identity.getDbName(), created); return created; @@ -484,6 +486,16 @@ private BuilderHandle handle(StoreIdentity identity) throws IOException { } } + private void logBuildProgress(StoreIdentity identity, + PathStateNodeStoreSet.RebuildWriter writer, long sortedRows, long elapsedMillis) { + long rowsPerSecond = elapsedMillis == 0 ? sortedRows + : (long) (sortedRows * 1000.0d / elapsedMillis); + logger.info("Path-state rebuild Store building: storeId={}, dbName={}, tier={}, " + + "sortedRows={}, nodeEntries={}, leafEntries={}, elapsedMs={}, rowsPerSecond={}", + identity.getStoreId(), identity.getDbName(), storeTier(identity), sortedRows, + writer.getNodeEntries(), writer.getLeafEntries(), elapsedMillis, rowsPerSecond); + } + @Override public synchronized void close() throws IOException { IOException failure = null; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java index f75aae9e086..273e9e6d661 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java @@ -14,10 +14,12 @@ final class PathStateStoreTrieBuilder implements Closeable { static final int DEFAULT_WRITE_BATCH_ROWS = 4096; + private static final long BUILD_PROGRESS_ROWS = 1L << 20; private final PathStateNativeNodeStore spool; private final PathStateStackTrie.NodeSink nodeSink; private final LeafSink leafSink; + private final BuildProgress buildProgress; private final byte[] generationPrefix; private final List pendingRows = new ArrayList<>(DEFAULT_WRITE_BATCH_ROWS); private boolean built; @@ -37,10 +39,18 @@ final class PathStateStoreTrieBuilder implements Closeable { PathStateStoreTrieBuilder(Path spoolDirectory, Engine engine, byte[] generationPrefix, PathStateStackTrie.NodeSink nodeSink, LeafSink leafSink) throws IOException { + this(spoolDirectory, engine, generationPrefix, nodeSink, leafSink, + (sortedRows, elapsedMillis) -> { }); + } + + PathStateStoreTrieBuilder(Path spoolDirectory, Engine engine, byte[] generationPrefix, + PathStateStackTrie.NodeSink nodeSink, LeafSink leafSink, + BuildProgress buildProgress) throws IOException { spool = PathStateNativeNodeStore.open(Objects.requireNonNull(spoolDirectory, "spoolDirectory"), Objects.requireNonNull(engine, "engine")); this.nodeSink = Objects.requireNonNull(nodeSink, "nodeSink"); this.leafSink = Objects.requireNonNull(leafSink, "leafSink"); + this.buildProgress = Objects.requireNonNull(buildProgress, "buildProgress"); this.generationPrefix = Arrays.copyOf( Objects.requireNonNull(generationPrefix, "generationPrefix"), generationPrefix.length); } @@ -91,22 +101,34 @@ byte[] build() throws IOException { requireCollecting(); flushRows(); PathStateStackTrie trie = new PathStateStackTrie(nodeSink); + long startedNanos = System.nanoTime(); PathStateNativeNodeStore.EntryConsumer consumer = entry -> { byte[] key = secureKey(entry.getKey()); byte[] value = entry.getValue(); trie.update(key, value); leafSink.put(key, value); sortedRows = Math.addExact(sortedRows, 1L); + if (sortedRows % BUILD_PROGRESS_ROWS == 0) { + buildProgress.report(sortedRows, elapsedMillis(startedNanos)); + } }; if (generationPrefix.length == 0) { spool.scanAll(consumer); } else { spool.scanPrefix(generationPrefix, consumer); } + if (sortedRows % BUILD_PROGRESS_ROWS != 0) { + buildProgress.report(sortedRows, elapsedMillis(startedNanos)); + } built = true; return trie.rootHash(); } + private static long elapsedMillis(long startedNanos) { + return java.util.concurrent.TimeUnit.NANOSECONDS.toMillis( + System.nanoTime() - startedNanos); + } + long getInputRows() { return inputRows; } @@ -175,4 +197,10 @@ interface LeafSink { void put(byte[] secureKey, byte[] encodedValue); } + + @FunctionalInterface + interface BuildProgress { + + void report(long sortedRows, long elapsedMillis); + } } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilderTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilderTest.java index cc21395bcaa..e8043b92fe2 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilderTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilderTest.java @@ -7,6 +7,7 @@ import java.nio.ByteBuffer; import java.nio.file.Path; import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -23,9 +24,11 @@ public void diskSortsUnorderedRowsAndBoundsPendingValues() throws Exception { PathMerkleTrie reference = new PathMerkleTrie(new MemoryNodeStore()); Random random = new Random(71L); int count = PathStateStoreTrieBuilder.DEFAULT_WRITE_BATCH_ROWS * 3 + 17; + AtomicLong reportedRows = new AtomicLong(); try (PathStateStoreTrieBuilder builder = new PathStateStoreTrieBuilder(spool, - Engine.ROCKSDB, (path, encoded) -> { })) { + Engine.ROCKSDB, new byte[0], (path, encoded) -> { }, + (key, value) -> { }, (sortedRows, elapsedMillis) -> reportedRows.set(sortedRows))) { for (int index = count - 1; index >= 0; index--) { byte[] key = new byte[PathMerkleTrie.SECURE_KEY_LENGTH]; ByteBuffer.wrap(key, key.length - Integer.BYTES, Integer.BYTES).putInt(index); @@ -39,6 +42,7 @@ public void diskSortsUnorderedRowsAndBoundsPendingValues() throws Exception { assertArrayEquals(reference.rootHash(), builder.build()); assertEquals(count, builder.getInputRows()); assertEquals(count, builder.getSortedRows()); + assertEquals(count, reportedRows.get()); assertEquals(0, builder.getPendingRows()); } } From 48434d4a94971e8591a894fdf5ab4ef853553c4c Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sun, 30 Aug 2026 21:07:21 +0800 Subject: [PATCH 100/161] fix(chainbase): retire rebuild spools --- .../PathStateRebuildCoordinator.java | 57 +++++++++++++++++++ .../PathStateRebuildCoordinatorTest.java | 4 ++ 2 files changed, 61 insertions(+) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index cb09060fb24..412375122ec 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -5,11 +5,14 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -24,6 +27,7 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tron.core.capsule.utils.MarketUtils; @@ -126,6 +130,7 @@ private RebuildResult rebuildInternal(PathStateStoreManifest manifest, SnapshotS checkpoint.getSourceIdentityDigest())) { throw new IOException("path-state rebuild checkpoint source identity mismatch"); } + retireCompletedSpools(admittedManifest, completedStores.values()); try (StoreBuilders builders = new StoreBuilders(admittedManifest, sourceIdentityDigest, stores)) { buildStoresInParallel(admittedManifest, admittedSource, identity, sourceIdentityDigest, @@ -259,6 +264,9 @@ private Future submitStore(ExecutorService executor, Future dependency, result.getStoreId(), result.getDbName(), result.getEntryCount(), completedStores.size(), descriptor.getStores().size() - completedStores.size()); } + builders.retire(store); + logger.info("Path-state rebuild Store spool retired: storeId={}, dbName={}", + result.getStoreId(), result.getDbName()); faultHook.afterStore(result); return null; } catch (IOException | RuntimeException failure) { @@ -463,6 +471,15 @@ private synchronized void reset(String dbName) throws IOException { handle(descriptor.require(dbName)).builder.reset(); } + private synchronized void retire(StoreIdentity identity) throws IOException { + BuilderHandle handle = opened.remove(identity.getDbName()); + if (handle != null) { + handle.builder.close(); + handle.writer.close(); + } + deleteSpoolDirectory(manifest, identity); + } + private BuilderHandle handle(StoreIdentity identity) throws IOException { BuilderHandle existing = opened.get(identity.getDbName()); if (existing != null) { @@ -517,6 +534,46 @@ public synchronized void close() throws IOException { } } + private void retireCompletedSpools(PathStateStoreManifest manifest, + Collection completedStores) throws IOException { + for (StoreResult completed : completedStores) { + StoreIdentity identity = descriptor.require(completed.getDbName()); + if (identity.getStoreId() != completed.getStoreId()) { + throw new IOException("path-state completed Store identity mismatch"); + } + deleteSpoolDirectory(manifest, identity); + logger.info("Path-state rebuild completed Store spool absent: storeId={}, dbName={}", + identity.getStoreId(), identity.getDbName()); + } + } + + private static void deleteSpoolDirectory(PathStateStoreManifest manifest, + StoreIdentity identity) throws IOException { + Path spoolRoot = manifest.getBaseDirectory().resolve("rebuild-spool"); + Path directory = spoolRoot.resolve(Integer.toString(identity.getStoreId())); + if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + return; + } + if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS) + || Files.isSymbolicLink(directory)) { + throw new IOException("path-state rebuild spool is not a direct directory: " + directory); + } + List entries = new ArrayList<>(); + try (Stream paths = Files.walk(directory)) { + paths.forEach(entries::add); + } + for (Path entry : entries) { + if (Files.isSymbolicLink(entry)) { + throw new IOException("path-state rebuild spool contains a symbolic link: " + entry); + } + } + entries.sort(Comparator.reverseOrder()); + for (Path entry : entries) { + Files.deleteIfExists(entry); + } + PathStateMetadataFile.syncDirectory(spoolRoot); + } + private static final class BuilderHandle { private final PathStateStoreTrieBuilder builder; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index 315fc4dad25..4db07dbb690 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -71,6 +71,8 @@ public void rebuildsAndPublishesExactSnapshotAcrossNativeEngines() throws Except assertArrayEquals(oracle.stateRoot, result.getMetadata().getStateRoot()); for (StoreResult store : result.getStores()) { assertArrayEquals(oracle.storeRoots.get(store.getDbName()), store.getStoreRoot()); + assertFalse(Files.exists(manifest.getBaseDirectory().resolve("rebuild-spool") + .resolve(Integer.toString(store.getStoreId())))); } PathStateStoreManifest reopenedManifest = PathStateStoreManifest.validateExisting( @@ -295,6 +297,8 @@ public void resumesCompletedStoresWhileKeepingGenerationInvisible() throws Excep assertFalse(Files.exists(manifest.getBaseDirectory() .resolve(PathStateCurrentStore.METADATA_FILE))); assertNull(PathStateNodeStoreSet.loadProgress(manifest.getBaseDirectory(), manifest)); + assertFalse(Files.exists(manifest.getBaseDirectory().resolve("rebuild-spool/4"))); + assertTrue(Files.exists(manifest.getBaseDirectory().resolve("rebuild-spool/5"))); TestSnapshotSource resumed = exactSource(identity()); resumed.add("account", lazyAddress, lazyAccount); From 2edd4d7e705f537aefc4357d118d2856a73a0e3e Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 31 Aug 2026 08:08:03 +0800 Subject: [PATCH 101/161] fix(chainbase): recover failed store rebuilds --- .../PathStateRebuildCoordinator.java | 50 ++++++++++++++++--- .../stateroot/PathStateStoreTrieBuilder.java | 24 --------- .../PathStateRebuildCoordinatorTest.java | 13 ++--- 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index 412375122ec..15529096dad 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -27,6 +27,7 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -173,6 +174,7 @@ private void buildStoresInParallel(PathStateStoreManifest manifest, SnapshotSour ExecutorService smallExecutor = Executors.newFixedThreadPool(SMALL_STORE_WORKERS, rebuildThreadFactory("small")); List> futures = new ArrayList<>(); + AtomicReference abort = new AtomicReference<>(); Future accountFuture = null; StoreIdentity accountAsset = null; try { @@ -190,7 +192,7 @@ private void buildStoresInParallel(PathStateStoreManifest manifest, SnapshotSour ExecutorService executor = LARGE_STORES.contains(store.getDbName()) ? largeExecutor : smallExecutor; Future future = submitStore(executor, null, store, manifest, source, identity, - sourceIdentityDigest, root, stores, builders, completedStores, checkpointLock); + sourceIdentityDigest, root, stores, builders, completedStores, checkpointLock, abort); futures.add(future); if ("account".equals(store.getDbName())) { accountFuture = future; @@ -199,7 +201,7 @@ private void buildStoresInParallel(PathStateStoreManifest manifest, SnapshotSour if (accountAsset != null) { futures.add(submitStore(largeExecutor, accountFuture, accountAsset, manifest, source, identity, sourceIdentityDigest, root, stores, builders, completedStores, - checkpointLock)); + checkpointLock, abort)); } Throwable failure = null; for (Future future : futures) { @@ -209,11 +211,16 @@ private void buildStoresInParallel(PathStateStoreManifest manifest, SnapshotSour Thread.currentThread().interrupt(); failure = appendFailure(failure, new IOException("path-state rebuild interrupted", interrupted)); + cancelOutstanding(futures); + break; } catch (ExecutionException failed) { - Throwable cause = failed.getCause(); + Throwable firstFailure = abort.get(); + Throwable cause = firstFailure == null ? failed.getCause() : firstFailure; failure = appendFailure(failure, cause instanceof IOException || cause instanceof RuntimeException ? cause : new IOException("path-state Store rebuild failed", cause)); + cancelOutstanding(futures); + break; } } if (failure != null) { @@ -233,12 +240,14 @@ private Future submitStore(ExecutorService executor, Future dependency, SnapshotIdentity identity, byte[] sourceIdentityDigest, PathStateRoot root, PathStateNodeStoreSet stores, StoreBuilders builders, Map completedStores, - Object checkpointLock) { + Object checkpointLock, AtomicReference abort) { return executor.submit(() -> { try { + requireNotAborted(abort); awaitDependency(dependency); + requireNotAborted(abort); StoreAccumulator accumulator = new StoreAccumulator(store, identity.getPhase(), root, - builders); + builders, abort); if (!"account-asset".equals(store.getDbName())) { builders.reset(store.getDbName()); } @@ -270,6 +279,7 @@ private Future submitStore(ExecutorService executor, Future dependency, faultHook.afterStore(result); return null; } catch (IOException | RuntimeException failure) { + abort.compareAndSet(null, failure); logger.error("Path-state rebuild Store failed: storeId={}, dbName={}, tier={}", store.getStoreId(), store.getDbName(), storeTier(store), failure); throw failure; @@ -277,6 +287,21 @@ private Future submitStore(ExecutorService executor, Future dependency, }); } + private static void cancelOutstanding(List> futures) { + for (Future future : futures) { + if (!future.isDone()) { + future.cancel(true); + } + } + } + + private static void requireNotAborted(AtomicReference abort) throws IOException { + Throwable failure = abort.get(); + if (failure != null || Thread.currentThread().isInterrupted()) { + throw new IOException("path-state Store rebuild aborted after peer failure", failure); + } + } + private static void awaitDependency(Future dependency) throws IOException { if (dependency == null) { return; @@ -340,6 +365,7 @@ private final class StoreAccumulator { private final P66Phase phase; private final PathStateRoot root; private final StoreBuilders builders; + private final AtomicReference abort; private final Hasher inputDigest; private final long startedNanos = System.nanoTime(); private byte[] previousKey; @@ -349,11 +375,12 @@ private final class StoreAccumulator { private long lastProgressLogNanos = startedNanos; private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root, - StoreBuilders builders) { + StoreBuilders builders, AtomicReference abort) { this.store = store; this.phase = phase; this.root = root; this.builders = builders; + this.abort = abort; inputDigest = domainHasher(STORE_DIGEST_DOMAIN); putInt(inputDigest, store.getStoreId()); putString(inputDigest, store.getDbName()); @@ -362,6 +389,7 @@ private StoreAccumulator(StoreIdentity store, P66Phase phase, PathStateRoot root } private void accept(byte[] physicalKey, byte[] rawValue) throws IOException { + requireNotAborted(abort); byte[] key = copy(physicalKey, "physicalKey"); byte[] value = Arrays.copyOf(Objects.requireNonNull(rawValue, "rawValue"), rawValue.length); @@ -468,7 +496,15 @@ private synchronized byte[] build(String dbName) throws IOException { } private synchronized void reset(String dbName) throws IOException { - handle(descriptor.require(dbName)).builder.reset(); + StoreIdentity identity = descriptor.require(dbName); + BuilderHandle handle = opened.remove(identity.getDbName()); + if (handle != null) { + handle.builder.close(); + handle.writer.close(); + } + deleteSpoolDirectory(manifest, identity); + logger.info("Path-state rebuild incomplete Store spool reset: storeId={}, dbName={}", + identity.getStoreId(), identity.getDbName()); } private synchronized void retire(StoreIdentity identity) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java index 273e9e6d661..6435a115968 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateStoreTrieBuilder.java @@ -72,30 +72,6 @@ void put(byte[] secureKey, byte[] encodedValue) { } } - /** Removes an incomplete current-generation spool before that Store is rescanned. */ - void reset() throws IOException { - requireCollecting(); - pendingRows.clear(); - List deletes = new ArrayList<>(DEFAULT_WRITE_BATCH_ROWS); - PathStateNativeNodeStore.EntryConsumer consumer = entry -> { - deletes.add(BatchMutation.delete(entry.getKey())); - if (deletes.size() >= DEFAULT_WRITE_BATCH_ROWS) { - spool.writeBatch(new ArrayList<>(deletes)); - deletes.clear(); - } - }; - if (generationPrefix.length == 0) { - spool.scanAll(consumer); - } else { - spool.scanPrefix(generationPrefix, consumer); - } - if (!deletes.isEmpty()) { - spool.writeBatch(deletes); - } - inputRows = 0; - sortedRows = 0; - } - /** Flushes the spool, streams it in secure-key order, and returns the canonical Store root. */ byte[] build() throws IOException { requireCollecting(); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java index 4db07dbb690..0218687c472 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinatorTest.java @@ -306,9 +306,9 @@ public void resumesCompletedStoresWhileKeepingGenerationInvisible() throws Excep RebuildResult result = new PathStateRebuildCoordinator().rebuild(manifest, resumed); assertEquals(27, result.getStores().size()); - assertEquals(0, resumed.getScanCount("abi")); - assertEquals(0, resumed.getScanCount("accountid-index")); - assertEquals(0, resumed.getScanCount("account-index")); + assertTrue(resumed.getScanCount("abi") <= 1); + assertTrue(resumed.getScanCount("accountid-index") <= 1); + assertTrue(resumed.getScanCount("account-index") <= 1); assertEquals(0, resumed.getScanCount("account")); assertEquals(1, resumed.getScanCount("account-asset")); assertTrue(resumed.getScanCount("proposal") == 0 @@ -330,7 +330,7 @@ public void resumesCompletedStoresWhileKeepingGenerationInvisible() throws Excep } @Test - public void resumesNonPrefixCheckpointWithoutPersistingFailedStoreResidue() throws Exception { + public void resumesCheckpointWithoutPersistingFailedStoreResidue() throws Exception { PathStateStoreManifest manifest = manifest("resume-non-prefix", Engine.ROCKSDB); TestSnapshotSource failedSource = exactSource(identity()); byte[] accountKey = address(7); @@ -341,13 +341,14 @@ public void resumesNonPrefixCheckpointWithoutPersistingFailedStoreResidue() thro assertThrows(IllegalArgumentException.class, () -> new PathStateRebuildCoordinator().rebuild(manifest, failedSource)); assertFalse(new PathStateCurrentStore(manifest).isInitialized()); + assertEquals(0, failedSource.getScanCount("delegation")); + assertEquals(0, failedSource.getScanCount("storage-row")); + assertEquals(0, failedSource.getScanCount("account-asset")); try (PathStateNodeStoreSet reopened = PathStateNodeStoreSet.openBase(manifest)) { PathStateRebuildCheckpoint checkpoint = reopened.getRebuildCheckpoint(); assertTrue(checkpoint.hasIndependentStores()); assertTrue(checkpoint.getCompletedStores().stream() .noneMatch(store -> "account".equals(store.getDbName()))); - assertTrue(checkpoint.getCompletedStores().stream() - .anyMatch(store -> store.getStoreId() > 4)); } RebuildResult resumed = new PathStateRebuildCoordinator().rebuild(manifest, From 07f7b200d9e3ae33ceb70455de99955d2568c998 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 31 Aug 2026 22:54:39 +0800 Subject: [PATCH 102/161] feat(chainbase): add physical path state runtime Connect the exact 27 participant plus super-store format to Manager startup, block-final updates, recovery, and bounded short reorg handling. Add fresh-format admission, resumable physical ingestion, global publication intents, reverse journals, and integration/fault coverage. --- .../stateroot/PathStateCommitmentCodec.java | 12 +- .../core/db2/stateroot/PathStateHead.java | 22 + .../db2/stateroot/PathStateMetadataFile.java | 8 + .../core/db2/stateroot/PathStateMutation.java | 42 +- .../PathStateNativeSnapshotSource.java | 33 +- .../PathStatePhysicalGlobalIntent.java | 182 ++ .../PathStatePhysicalIngestCheckpoint.java | 64 + .../PathStatePhysicalReverseJournal.java | 267 +++ .../PathStatePhysicalRuntimeAdmission.java | 56 + .../PathStatePhysicalSnapshotHead.java | 146 ++ .../PathStatePhysicalStoreManifest.java | 124 ++ .../stateroot/PathStatePhysicalStoreSet.java | 1521 +++++++++++++++++ .../PathStateRebuildCoordinator.java | 6 + .../core/db2/stateroot/PathStateRoot.java | 4 +- .../db2/stateroot/PathStateSnapshotHead.java | 12 +- .../main/java/org/tron/core/db/Manager.java | 83 +- ...athStateManagerStartupIntegrationTest.java | 212 ++- .../PathStateNativeNodeStoreTest.java | 870 ++++++++++ .../PathStateNativeSnapshotSourceTest.java | 31 + .../PathStateRuntimeAdmissionTest.java | 36 + 20 files changed, 3630 insertions(+), 101 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateHead.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalGlobalIntent.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalIngestCheckpoint.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalReverseJournal.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalRuntimeAdmission.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreManifest.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java index 26b6460addb..d839e0af41d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java @@ -32,10 +32,10 @@ public final class PathStateCommitmentCodec { private PathStateCommitmentCodec() { } - /** Returns the secure per-Store trie key for one canonical present value. */ - public static byte[] storeLeafKey(int stableStoreId, byte[] canonicalKey) { + /** Returns the secure per-Store trie key for one exact physical raw key. */ + public static byte[] storeLeafKey(int stableStoreId, byte[] physicalRawKey) { requireStoreId(stableStoreId); - byte[] key = copy(canonicalKey, "canonicalKey"); + byte[] key = copy(physicalRawKey, "physicalRawKey"); ByteBuffer material = ByteBuffer.allocate(Short.BYTES + STORE_LEAF_KEY_DOMAIN.length + Short.BYTES + Integer.BYTES + Integer.BYTES + key.length); putDomain(material, STORE_LEAF_KEY_DOMAIN); @@ -47,9 +47,9 @@ public static byte[] storeLeafKey(int stableStoreId, byte[] canonicalKey) { } /** Encodes PRESENT(empty) distinctly from PRESENT(0x00); ABSENT has no leaf. */ - public static byte[] presentLeafValue(byte[] canonicalValue) { - byte[] value = Arrays.copyOf(Objects.requireNonNull(canonicalValue, "canonicalValue"), - canonicalValue.length); + public static byte[] presentLeafValue(byte[] physicalRawValue) { + byte[] value = Arrays.copyOf(Objects.requireNonNull(physicalRawValue, "physicalRawValue"), + physicalRawValue.length); return rlpList(new byte[]{PRESENT_TAG}, value); } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateHead.java new file mode 100644 index 00000000000..625594764de --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateHead.java @@ -0,0 +1,22 @@ +package org.tron.core.db2.stateroot; + +import java.io.Closeable; +import java.io.IOException; + +/** Runtime-owned current path-state authority used by Manager lifecycle integration. */ +public interface PathStateHead extends Closeable { + + PathStateRootMetadata advance(PathStateBlockTransition transition) throws IOException; + + PathStateRootMetadata rewindTo(long blockNumber, byte[] blockHash) throws IOException; + + PathStateRootMetadata flushBaseThrough(long blockNumber, byte[] blockHash) throws IOException; + + /** Computes the child state root without publishing or adopting it. */ + byte[] preview(PathStateBlockTransition transition) throws IOException; + + PathStateRootMetadata getHead() throws IOException; + + @Override + void close() throws IOException; +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java index c13ad9d0284..bc86e3d83cb 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMetadataFile.java @@ -72,6 +72,14 @@ static void replaceCurrent(Path path, PathStateRootMetadata metadata) throws IOE replaceCurrent(path, metadata, temporary -> { }); } + static void replaceCurrentBytes(Path path, byte[] encoded) throws IOException { + byte[] value = Arrays.copyOf(Objects.requireNonNull(encoded, "encoded"), encoded.length); + if (value.length == 0) { + throw new IllegalArgumentException("path-state CURRENT record must not be empty"); + } + publish(Objects.requireNonNull(path, "path"), value, true, temporary -> { }); + } + static void replaceCurrent(Path path, PathStateRootMetadata metadata, FaultHook faultHook) throws IOException { publish(Objects.requireNonNull(path, "path"), diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java index 50d6a0b4cce..1c5b557e64a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java @@ -7,39 +7,53 @@ public final class PathStateMutation { private final String dbName; - private final byte[] canonicalKey; - private final byte[] canonicalValue; + private final byte[] physicalKey; + private final byte[] physicalValue; - private PathStateMutation(String dbName, byte[] canonicalKey, byte[] canonicalValue) { + private PathStateMutation(String dbName, byte[] physicalKey, byte[] physicalValue) { this.dbName = Objects.requireNonNull(dbName, "dbName"); - this.canonicalKey = copy(canonicalKey, "canonicalKey"); - this.canonicalValue = canonicalValue == null ? null - : Arrays.copyOf(canonicalValue, canonicalValue.length); + this.physicalKey = copy(physicalKey, "physicalKey"); + this.physicalValue = physicalValue == null ? null + : Arrays.copyOf(physicalValue, physicalValue.length); } - public static PathStateMutation put(String dbName, byte[] canonicalKey, byte[] canonicalValue) { - return new PathStateMutation(dbName, canonicalKey, - Objects.requireNonNull(canonicalValue, "canonicalValue")); + public static PathStateMutation put(String dbName, byte[] physicalKey, byte[] physicalValue) { + return new PathStateMutation(dbName, physicalKey, + Objects.requireNonNull(physicalValue, "physicalValue")); } - public static PathStateMutation delete(String dbName, byte[] canonicalKey) { - return new PathStateMutation(dbName, canonicalKey, null); + public static PathStateMutation delete(String dbName, byte[] physicalKey) { + return new PathStateMutation(dbName, physicalKey, null); } public String getDbName() { return dbName; } + /** Exact physical key bytes supplied by the Chainbase mutation/source boundary. */ + public byte[] getPhysicalKey() { + return Arrays.copyOf(physicalKey, physicalKey.length); + } + + /** Exact physical value bytes, or {@code null} for an absent/delete mutation. */ + public byte[] getPhysicalValue() { + return physicalValue == null ? null : Arrays.copyOf(physicalValue, physicalValue.length); + } + + /** @deprecated Use {@link #getPhysicalKey()}; this alias is retained for old-format callers. */ + @Deprecated public byte[] getCanonicalKey() { - return Arrays.copyOf(canonicalKey, canonicalKey.length); + return getPhysicalKey(); } public boolean isDelete() { - return canonicalValue == null; + return physicalValue == null; } + /** @deprecated Use {@link #getPhysicalValue()}; this alias is retained for old-format callers. */ + @Deprecated public byte[] getCanonicalValue() { - return canonicalValue == null ? null : Arrays.copyOf(canonicalValue, canonicalValue.length); + return getPhysicalValue(); } private static byte[] copy(byte[] value, String name) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java index 88d71e63f68..12bb4df016c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSource.java @@ -164,6 +164,12 @@ public byte[] sourceIdentityDigest() { @Override public void scan(String dbName, EntryConsumer consumer) throws IOException { + scanAfter(dbName, null, consumer); + } + + @Override + public void scanAfter(String dbName, byte[] exclusivePhysicalCursor, EntryConsumer consumer) + throws IOException { ensureOpen(); Objects.requireNonNull(consumer, "consumer"); PathStateParticipantDescriptor.StoreIdentity participant = descriptor.require(dbName); @@ -173,6 +179,9 @@ public void scan(String dbName, EntryConsumer consumer) throws IOException { } if (PathStateParticipantDescriptor.MARKET_PRICE_COMPARATOR.equals( participant.getComparatorId())) { + if (exclusivePhysicalCursor != null) { + throw new IOException("market-price snapshot scan does not support physical cursor resume"); + } List> entries = new ArrayList<>(); scanLexical(snapshot, (key, value) -> { if (entries.size() >= marketEntryLimit) { @@ -186,42 +195,50 @@ public void scan(String dbName, EntryConsumer consumer) throws IOException { } return; } - scanLexical(snapshot, consumer); + scanLexical(snapshot, consumer, exclusivePhysicalCursor); } private void scanLexical(StoreSnapshot snapshot, EntryConsumer consumer) throws IOException { - byte[] lower = new byte[0]; - byte[] previous = null; + scanLexical(snapshot, consumer, null); + } + + private void scanLexical(StoreSnapshot snapshot, EntryConsumer consumer, byte[] cursor) + throws IOException { + byte[] lower = cursor == null ? new byte[0] : Arrays.copyOf(cursor, cursor.length); + byte[] previous = cursor == null ? null : Arrays.copyOf(cursor, cursor.length); while (true) { List> page; try { - page = snapshot.range(lower, null, pageSize); + page = snapshot.range(lower, null, pageSize + 1); } catch (UnsupportedOperationException unsupported) { throw new IOException("pinned Store does not support range scan: " + snapshot.getDbName(), unsupported); } - if (page == null || page.size() > pageSize) { + if (page == null || page.size() > pageSize + 1) { throw new IOException("pinned Store returned an invalid range page: " + snapshot.getDbName()); } for (Map.Entry entry : page) { byte[] key = copy(entry.getKey(), "snapshot key"); byte[] value = copy(entry.getValue(), "snapshot value"); - if (previous != null && compareUnsigned(previous, key) >= 0) { + if (previous != null && compareUnsigned(previous, key) > 0) { throw new IOException("pinned Store range is not strictly lexical: " + snapshot.getDbName()); } + if (previous != null && Arrays.equals(previous, key)) { + continue; + } consumer.accept(key, value); previous = key; } - if (page.size() < pageSize) { + if (page.size() < pageSize + 1) { return; } if (previous == null) { throw new IOException("pinned Store returned a full empty range page: " + snapshot.getDbName()); } - lower = Arrays.copyOf(previous, previous.length + 1); + lower = Arrays.copyOf(previous, previous.length); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalGlobalIntent.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalGlobalIntent.java new file mode 100644 index 00000000000..4391d91ecb9 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalGlobalIntent.java @@ -0,0 +1,182 @@ +package org.tron.core.db2.stateroot; + +import com.google.common.hash.Hashing; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** Exact 27-participant plus super target used by TASK-018 INTENT and CURRENT. */ +final class PathStatePhysicalGlobalIntent { + + static final int DIGEST_LENGTH = 32; + static final int MAX_ENCODED_LENGTH = 32 * 1024; + private static final int PARTICIPANT_COUNT = 27; + + private static final int MAGIC = 0x50534749; // PSGI + private static final short VERSION = 1; + private static final int ENTRY_LENGTH = Integer.BYTES + 3 * DIGEST_LENGTH; + private static final int FIXED_LENGTH_WITHOUT_METADATA = Integer.BYTES + Short.BYTES + + DIGEST_LENGTH + Integer.BYTES + Integer.BYTES + 2 * DIGEST_LENGTH + DIGEST_LENGTH; + + private final byte[] formatDigest; + private final PathStateRootMetadata metadata; + private final List participants; + private final byte[] superGeneration; + private final byte[] superRoot; + + PathStatePhysicalGlobalIntent(byte[] formatDigest, PathStateRootMetadata metadata, + List participants, byte[] superGeneration, byte[] superRoot) { + this.formatDigest = digest(formatDigest, "formatDigest"); + this.metadata = Objects.requireNonNull(metadata, "metadata"); + if (!Arrays.equals(this.formatDigest, metadata.getFormatDigest())) { + throw new IllegalArgumentException("physical global intent metadata format differs"); + } + List supplied = new ArrayList<>( + Objects.requireNonNull(participants, "participants")); + if (supplied.size() != PARTICIPANT_COUNT) { + throw new IllegalArgumentException("physical global intent must contain exact-27 participants"); + } + int previousStoreId = 0; + for (ParticipantTarget target : supplied) { + ParticipantTarget present = Objects.requireNonNull(target, "participant target"); + if (present.storeId <= previousStoreId) { + throw new IllegalArgumentException( + "physical global intent Store IDs must be strictly ascending"); + } + previousStoreId = present.storeId; + } + this.participants = Collections.unmodifiableList(supplied); + this.superGeneration = digest(superGeneration, "superGeneration"); + this.superRoot = digest(superRoot, "superRoot"); + if (!Arrays.equals(this.superRoot, metadata.getStateRoot())) { + throw new IllegalArgumentException("physical global intent metadata root differs"); + } + } + + byte[] encode() { + byte[] encodedMetadata = metadata.encode(); + int payloadLength = FIXED_LENGTH_WITHOUT_METADATA - DIGEST_LENGTH + + encodedMetadata.length + participants.size() * ENTRY_LENGTH; + ByteBuffer payload = ByteBuffer.allocate(payloadLength); + payload.putInt(MAGIC).putShort(VERSION).put(formatDigest) + .putInt(encodedMetadata.length).put(encodedMetadata).putInt(participants.size()); + for (ParticipantTarget participant : participants) { + payload.putInt(participant.storeId).put(participant.generation) + .put(participant.flatDigest).put(participant.storeRoot); + } + payload.put(superGeneration).put(superRoot); + byte[] body = payload.array(); + return ByteBuffer.allocate(body.length + DIGEST_LENGTH).put(body) + .put(Hashing.sha256().hashBytes(body).asBytes()).array(); + } + + static PathStatePhysicalGlobalIntent decode(byte[] encoded) { + byte[] supplied = Arrays.copyOf(Objects.requireNonNull(encoded, "encoded"), encoded.length); + if (supplied.length < FIXED_LENGTH_WITHOUT_METADATA || supplied.length > MAX_ENCODED_LENGTH) { + throw new IllegalArgumentException("physical global intent length is invalid"); + } + int payloadLength = supplied.length - DIGEST_LENGTH; + byte[] body = Arrays.copyOf(supplied, payloadLength); + byte[] checksum = Arrays.copyOfRange(supplied, payloadLength, supplied.length); + if (!Arrays.equals(checksum, Hashing.sha256().hashBytes(body).asBytes())) { + throw new IllegalArgumentException("physical global intent checksum differs"); + } + ByteBuffer input = ByteBuffer.wrap(body); + if (input.getInt() != MAGIC || input.getShort() != VERSION) { + throw new IllegalArgumentException("physical global intent format is unsupported"); + } + byte[] formatDigest = readDigest(input); + int metadataLength = input.getInt(); + if (metadataLength <= 0 || metadataLength > input.remaining() - Integer.BYTES + - 2 * DIGEST_LENGTH) { + throw new IllegalArgumentException("physical global intent metadata length is invalid"); + } + byte[] encodedMetadata = new byte[metadataLength]; + input.get(encodedMetadata); + PathStateRootMetadata metadata = PathStateRootMetadata.decode(encodedMetadata); + int count = input.getInt(); + if (count != PARTICIPANT_COUNT + || body.length != FIXED_LENGTH_WITHOUT_METADATA - DIGEST_LENGTH + + metadataLength + count * ENTRY_LENGTH) { + throw new IllegalArgumentException("physical global intent participant count is invalid"); + } + List participants = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + participants.add(new ParticipantTarget(input.getInt(), readDigest(input), + readDigest(input), readDigest(input))); + } + return new PathStatePhysicalGlobalIntent(formatDigest, metadata, participants, + readDigest(input), readDigest(input)); + } + + byte[] getFormatDigest() { + return Arrays.copyOf(formatDigest, formatDigest.length); + } + + List getParticipants() { + return participants; + } + + PathStateRootMetadata getMetadata() { + return PathStateRootMetadata.decode(metadata.encode()); + } + + byte[] getSuperGeneration() { + return Arrays.copyOf(superGeneration, superGeneration.length); + } + + byte[] getSuperRoot() { + return Arrays.copyOf(superRoot, superRoot.length); + } + + private static byte[] readDigest(ByteBuffer input) { + byte[] value = new byte[DIGEST_LENGTH]; + input.get(value); + return value; + } + + private static byte[] digest(byte[] value, String name) { + byte[] supplied = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (supplied.length != DIGEST_LENGTH) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return supplied; + } + + static final class ParticipantTarget { + + private final int storeId; + private final byte[] generation; + private final byte[] flatDigest; + private final byte[] storeRoot; + + ParticipantTarget(int storeId, byte[] generation, byte[] flatDigest, byte[] storeRoot) { + if (storeId <= 0) { + throw new IllegalArgumentException("storeId must be positive"); + } + this.storeId = storeId; + this.generation = digest(generation, "generation"); + this.flatDigest = digest(flatDigest, "flatDigest"); + this.storeRoot = digest(storeRoot, "storeRoot"); + } + + int getStoreId() { + return storeId; + } + + byte[] getGeneration() { + return Arrays.copyOf(generation, generation.length); + } + + byte[] getFlatDigest() { + return Arrays.copyOf(flatDigest, flatDigest.length); + } + + byte[] getStoreRoot() { + return Arrays.copyOf(storeRoot, storeRoot.length); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalIngestCheckpoint.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalIngestCheckpoint.java new file mode 100644 index 00000000000..a9e383dc2c6 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalIngestCheckpoint.java @@ -0,0 +1,64 @@ +package org.tron.core.db2.stateroot; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Objects; + +/** Durable per-Store F-ingest cursor bound to one pinned physical source identity. */ +final class PathStatePhysicalIngestCheckpoint { + + private static final int VERSION = 1; + private final byte[] sourceIdentity; + private final byte[] cursor; + private final long rows; + private final long bytes; + + PathStatePhysicalIngestCheckpoint(byte[] sourceIdentity, byte[] cursor, long rows, long bytes) { + this.sourceIdentity = copy32(sourceIdentity, "sourceIdentity"); + this.cursor = Arrays.copyOf(Objects.requireNonNull(cursor, "cursor"), cursor.length); + if (rows < 0 || bytes < 0) { + throw new IllegalArgumentException("checkpoint rows and bytes must not be negative"); + } + this.rows = rows; + this.bytes = bytes; + } + + byte[] encode() { + return ByteBuffer.allocate(Integer.BYTES + sourceIdentity.length + Integer.BYTES + cursor.length + + Long.BYTES * 2).putInt(VERSION).put(sourceIdentity).putInt(cursor.length).put(cursor) + .putLong(rows).putLong(bytes).array(); + } + + static PathStatePhysicalIngestCheckpoint decode(byte[] encoded) { + ByteBuffer input = ByteBuffer.wrap(Objects.requireNonNull(encoded, "encoded")); + if (input.remaining() < Integer.BYTES + 32 + Integer.BYTES + Long.BYTES * 2 + || input.getInt() != VERSION) { + throw new IllegalArgumentException("physical ingest checkpoint is invalid"); + } + byte[] identity = new byte[32]; + input.get(identity); + int cursorLength = input.getInt(); + if (cursorLength < 0 || input.remaining() != cursorLength + Long.BYTES * 2) { + throw new IllegalArgumentException("physical ingest checkpoint cursor is invalid"); + } + byte[] cursor = new byte[cursorLength]; + input.get(cursor); + return new PathStatePhysicalIngestCheckpoint(identity, cursor, input.getLong(), input.getLong()); + } + + byte[] getSourceIdentity() { return Arrays.copyOf(sourceIdentity, sourceIdentity.length); } + + byte[] getCursor() { return Arrays.copyOf(cursor, cursor.length); } + + long getRows() { return rows; } + + long getBytes() { return bytes; } + + private static byte[] copy32(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length != 32) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return copy; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalReverseJournal.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalReverseJournal.java new file mode 100644 index 00000000000..578d5b9300c --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalReverseJournal.java @@ -0,0 +1,267 @@ +package org.tron.core.db2.stateroot; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** Immutable reverse delta for one committed physical 27+1 child. */ +final class PathStatePhysicalReverseJournal { + + static final int MAX_ENCODED_LENGTH = 256 * 1024 * 1024; + private static final int MAGIC = 0x5053524a; // PSRJ + private static final short VERSION = 1; + private static final int CHECKSUM_LENGTH = 32; + private static final int MAX_STORES = 27; + private static final int MAX_MUTATIONS = 1_000_000; + private static final int MAX_VALUE_LENGTH = 16 * 1024 * 1024; + private static final int MAX_PATH_LENGTH = 64; + + private final byte[] childTarget; + private final byte[] parentTarget; + private final List stores; + private final List superNodes; + + PathStatePhysicalReverseJournal(byte[] childTarget, byte[] parentTarget, + List stores, List superNodes) { + this.childTarget = target(childTarget, "childTarget"); + this.parentTarget = target(parentTarget, "parentTarget"); + PathStatePhysicalGlobalIntent child = PathStatePhysicalGlobalIntent.decode(this.childTarget); + PathStatePhysicalGlobalIntent parent = PathStatePhysicalGlobalIntent.decode(this.parentTarget); + if (child.getMetadata().getBlockNumber() != parent.getMetadata().getBlockNumber() + 1 + || !Arrays.equals(child.getMetadata().getParentHash(), + parent.getMetadata().getBlockHash())) { + throw new IllegalArgumentException("physical reverse journal is not a direct child"); + } + List supplied = new ArrayList<>(Objects.requireNonNull(stores, "stores")); + if (supplied.size() > MAX_STORES) { + throw new IllegalArgumentException("physical reverse journal has too many Stores"); + } + int previousStoreId = 0; + for (StoreReverse store : supplied) { + if (store.storeId <= previousStoreId) { + throw new IllegalArgumentException("physical reverse Store IDs are not ascending"); + } + previousStoreId = store.storeId; + } + this.stores = Collections.unmodifiableList(supplied); + this.superNodes = immutableEntries(superNodes, false); + } + + byte[] encode() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + writeBytes(output, childTarget); + writeBytes(output, parentTarget); + output.writeInt(stores.size()); + for (StoreReverse store : stores) { + output.writeInt(store.storeId); + writeEntries(output, store.flatEntries); + writeEntries(output, store.nodeEntries); + } + writeEntries(output, superNodes); + output.flush(); + byte[] body = bytes.toByteArray(); + if (body.length > MAX_ENCODED_LENGTH - CHECKSUM_LENGTH) { + throw new IllegalArgumentException("physical reverse journal exceeds byte limit"); + } + return ByteBuffer.allocate(body.length + CHECKSUM_LENGTH).put(body) + .put(Hashing.sha256().hashBytes(body).asBytes()).array(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory reverse journal encoding failed", impossible); + } + } + + static PathStatePhysicalReverseJournal decode(byte[] encoded) { + byte[] supplied = Arrays.copyOf(Objects.requireNonNull(encoded, "encoded"), encoded.length); + if (supplied.length <= CHECKSUM_LENGTH || supplied.length > MAX_ENCODED_LENGTH) { + throw new IllegalArgumentException("physical reverse journal length is invalid"); + } + byte[] body = Arrays.copyOf(supplied, supplied.length - CHECKSUM_LENGTH); + byte[] checksum = Arrays.copyOfRange(supplied, body.length, supplied.length); + if (!Arrays.equals(checksum, Hashing.sha256().hashBytes(body).asBytes())) { + throw new IllegalArgumentException("physical reverse journal checksum differs"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(body))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION) { + throw new IllegalArgumentException("physical reverse journal format is unsupported"); + } + byte[] child = readBytes(input, PathStatePhysicalGlobalIntent.MAX_ENCODED_LENGTH); + byte[] parent = readBytes(input, PathStatePhysicalGlobalIntent.MAX_ENCODED_LENGTH); + int storeCount = input.readInt(); + if (storeCount < 0 || storeCount > MAX_STORES) { + throw new IllegalArgumentException("physical reverse journal Store count is invalid"); + } + List stores = new ArrayList<>(); + for (int index = 0; index < storeCount; index++) { + stores.add(new StoreReverse(input.readInt(), readEntries(input, true), + readEntries(input, false))); + } + List superNodes = readEntries(input, false); + if (input.available() != 0) { + throw new IllegalArgumentException("physical reverse journal has trailing bytes"); + } + return new PathStatePhysicalReverseJournal(child, parent, stores, superNodes); + } catch (IOException truncated) { + throw new IllegalArgumentException("physical reverse journal is truncated", truncated); + } + } + + byte[] getChildTarget() { + return Arrays.copyOf(childTarget, childTarget.length); + } + + byte[] getParentTarget() { + return Arrays.copyOf(parentTarget, parentTarget.length); + } + + List getStores() { + return stores; + } + + List getSuperNodes() { + return superNodes; + } + + static final class StoreReverse { + + private final int storeId; + private final List flatEntries; + private final List nodeEntries; + + StoreReverse(int storeId, List flatEntries, List nodeEntries) { + if (storeId <= 0) { + throw new IllegalArgumentException("physical reverse Store ID must be positive"); + } + this.storeId = storeId; + this.flatEntries = immutableEntries(flatEntries, true); + this.nodeEntries = immutableEntries(nodeEntries, false); + } + + int getStoreId() { + return storeId; + } + + List getFlatEntries() { + return flatEntries; + } + + List getNodeEntries() { + return nodeEntries; + } + } + + static final class Entry { + + private final byte[] key; + private final byte[] oldValue; + + Entry(byte[] key, byte[] oldValue) { + this.key = Arrays.copyOf(Objects.requireNonNull(key, "key"), key.length); + this.oldValue = oldValue == null ? null : Arrays.copyOf(oldValue, oldValue.length); + } + + byte[] getKey() { + return Arrays.copyOf(key, key.length); + } + + byte[] getOldValue() { + return oldValue == null ? null : Arrays.copyOf(oldValue, oldValue.length); + } + } + + private static List immutableEntries(List entries, boolean flat) { + List supplied = new ArrayList<>(Objects.requireNonNull(entries, "entries")); + if (supplied.size() > MAX_MUTATIONS) { + throw new IllegalArgumentException("physical reverse mutation count exceeds limit"); + } + List copies = new ArrayList<>(supplied.size()); + for (Entry entry : supplied) { + Entry present = Objects.requireNonNull(entry, "entry"); + int expected = flat ? PathStateCommitmentCodec.ROOT_LENGTH : -1; + if (flat && present.key.length != expected) { + throw new IllegalArgumentException("physical reverse flat key length is invalid"); + } + if (!flat && present.key.length > MAX_PATH_LENGTH) { + throw new IllegalArgumentException("physical reverse node path length is invalid"); + } + if (present.oldValue != null + && (present.oldValue.length == 0 || present.oldValue.length > MAX_VALUE_LENGTH)) { + throw new IllegalArgumentException("physical reverse value length is invalid"); + } + copies.add(new Entry(present.key, present.oldValue)); + } + return Collections.unmodifiableList(copies); + } + + private static void writeEntries(DataOutputStream output, List entries) + throws IOException { + output.writeInt(entries.size()); + for (Entry entry : entries) { + writeBytes(output, entry.key); + if (entry.oldValue == null) { + output.writeInt(-1); + } else { + writeBytes(output, entry.oldValue); + } + } + } + + private static List readEntries(DataInputStream input, boolean flat) + throws IOException { + int count = input.readInt(); + if (count < 0 || count > MAX_MUTATIONS) { + throw new IllegalArgumentException("physical reverse mutation count is invalid"); + } + List entries = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + byte[] key = readBytes(input, flat ? PathStateCommitmentCodec.ROOT_LENGTH + : MAX_PATH_LENGTH); + int valueLength = input.readInt(); + byte[] value = null; + if (valueLength != -1) { + if (valueLength <= 0 || valueLength > MAX_VALUE_LENGTH) { + throw new IllegalArgumentException("physical reverse value length is invalid"); + } + value = new byte[valueLength]; + input.readFully(value); + } + entries.add(new Entry(key, value)); + } + return entries; + } + + private static void writeBytes(DataOutputStream output, byte[] value) throws IOException { + output.writeInt(value.length); + output.write(value); + } + + private static byte[] readBytes(DataInputStream input, int maxLength) throws IOException { + int length = input.readInt(); + if (length < 0 || length > maxLength) { + throw new IllegalArgumentException("physical reverse field length is invalid"); + } + byte[] value = new byte[length]; + input.readFully(value); + return value; + } + + private static byte[] target(byte[] encoded, String name) { + byte[] value = Arrays.copyOf(Objects.requireNonNull(encoded, name), encoded.length); + if (value.length == 0 || value.length > PathStatePhysicalGlobalIntent.MAX_ENCODED_LENGTH) { + throw new IllegalArgumentException(name + " length is invalid"); + } + return value; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalRuntimeAdmission.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalRuntimeAdmission.java new file mode 100644 index 00000000000..1d40ab16c7e --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalRuntimeAdmission.java @@ -0,0 +1,56 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Non-creating startup gate for the fresh physical 27+1 format. */ +public final class PathStatePhysicalRuntimeAdmission { + + private PathStatePhysicalRuntimeAdmission() { + } + + public static Result inspect(boolean enabled, Path directory, Engine engine) throws IOException { + if (!enabled) { + return new Result(Status.DISABLED, null); + } + Path root = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); + Engine selected = Objects.requireNonNull(engine, "engine"); + if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { + return new Result(Status.REBUILD_REQUIRED, null); + } + PathStatePhysicalStoreManifest manifest = + PathStatePhysicalStoreManifest.validateExisting(root, selected); + Status status = Files.isRegularFile(root.resolve(PathStatePhysicalStoreSet.CURRENT_FILE), + LinkOption.NOFOLLOW_LINKS) ? Status.CURRENT_CANDIDATE : Status.REBUILD_REQUIRED; + return new Result(status, manifest); + } + + public enum Status { + DISABLED, + REBUILD_REQUIRED, + CURRENT_CANDIDATE + } + + public static final class Result { + + private final Status status; + private final PathStatePhysicalStoreManifest manifest; + + private Result(Status status, PathStatePhysicalStoreManifest manifest) { + this.status = status; + this.manifest = manifest; + } + + public Status getStatus() { + return status; + } + + public PathStatePhysicalStoreManifest getManifest() { + return manifest; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java new file mode 100644 index 00000000000..0b2476e2b31 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java @@ -0,0 +1,146 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Runtime owner for a block-bound physical 27+1 CURRENT. */ +public final class PathStatePhysicalSnapshotHead implements PathStateHead { + + private final PathStatePhysicalStoreSet stores; + private final PathStateLayerLimits limits; + private PathStateRootMetadata head; + private boolean failed; + private boolean closed; + + private PathStatePhysicalSnapshotHead(PathStatePhysicalStoreSet stores, + PathStateRootMetadata head, PathStateLayerLimits limits) { + this.stores = stores; + this.head = head; + this.limits = limits; + } + + /** Opens and verifies the exact 28-database target before exposing its block identity. */ + public static PathStatePhysicalSnapshotHead open(Path directory, Engine engine) + throws IOException { + return open(directory, engine, PathStateLayerLimits.defaults()); + } + + /** Opens with explicit bounded reverse-journal count and logical-byte limits. */ + public static PathStatePhysicalSnapshotHead open(Path directory, Engine engine, + PathStateLayerLimits limits) throws IOException { + PathStatePhysicalStoreSet opened = PathStatePhysicalStoreSet.openExisting(directory, + new PathStateCanonicalizer().participantScope(), engine); + try { + opened.recoverPublication(); + PathStateLayerLimits admitted = Objects.requireNonNull(limits, "limits"); + opened.verifyReverseJournals(admitted); + return new PathStatePhysicalSnapshotHead(opened, opened.currentMetadata(), admitted); + } catch (IOException | RuntimeException failure) { + try { + opened.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } + } + + @Override + public synchronized PathStateRootMetadata advance(PathStateBlockTransition transition) + throws IOException { + requireHealthy(); + PathStateRootMetadata previous = head; + try { + PathStateRootMetadata committed = stores.applyAndPublish(transition, limits, stage -> { }); + if (!same(committed, stores.currentMetadata())) { + failed = true; + throw new IOException("physical path-state committed CURRENT identity mismatch"); + } + head = committed; + return PathStateRootMetadata.decode(committed.encode()); + } catch (IOException | RuntimeException failure) { + try { + if (!same(previous, stores.currentMetadata())) { + this.failed = true; + } + } catch (IOException | RuntimeException verificationFailure) { + this.failed = true; + failure.addSuppressed(verificationFailure); + } + throw failure; + } + } + + @Override + public synchronized PathStateRootMetadata rewindTo(long blockNumber, byte[] blockHash) + throws IOException { + requireHealthy(); + PathStateRootMetadata previous = head; + try { + PathStateRootMetadata rewound = stores.rewindTo(blockNumber, blockHash, limits); + if (!same(rewound, stores.currentMetadata())) { + failed = true; + throw new IOException("physical path-state rewound CURRENT identity mismatch"); + } + head = rewound; + return PathStateRootMetadata.decode(rewound.encode()); + } catch (IOException | RuntimeException failure) { + try { + if (!same(previous, stores.currentMetadata())) { + this.failed = true; + } + } catch (IOException | RuntimeException verificationFailure) { + this.failed = true; + failure.addSuppressed(verificationFailure); + } + throw failure; + } + } + + @Override + public synchronized PathStateRootMetadata flushBaseThrough(long blockNumber, byte[] blockHash) + throws IOException { + requireHealthy(); + return head; + } + + @Override + public synchronized byte[] preview(PathStateBlockTransition transition) throws IOException { + requireHealthy(); + return stores.previewTransition(Objects.requireNonNull(transition, "transition")) + .getStateRoot(); + } + + @Override + public synchronized PathStateRootMetadata getHead() throws IOException { + requireHealthy(); + return PathStateRootMetadata.decode(head.encode()); + } + + @Override + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + stores.close(); + } + } + + private void requireOpen() throws IOException { + if (closed) { + throw new IOException("physical path-state head is closed"); + } + } + + private void requireHealthy() throws IOException { + requireOpen(); + if (failed) { + throw new IOException("physical path-state head failed closed"); + } + } + + private static boolean same(PathStateRootMetadata left, PathStateRootMetadata right) { + return java.util.Arrays.equals(left.encode(), right.encode()); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreManifest.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreManifest.java new file mode 100644 index 00000000000..169c33a19d6 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreManifest.java @@ -0,0 +1,124 @@ +package org.tron.core.db2.stateroot; + +import com.google.common.hash.Hashing; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Immutable format identity for the TASK-018 physical 27+1 layout. */ +public final class PathStatePhysicalStoreManifest { + + private static final String FILE = "MANIFEST"; + private static final int MAGIC = 0x50535046; // PSPF + private static final short VERSION = 1; + private static final int HEADER_LENGTH = Integer.BYTES + Short.BYTES + Short.BYTES + + Short.BYTES + Integer.BYTES; + + private final Path directory; + private final Engine engine; + private final byte[] identityDigest; + + private PathStatePhysicalStoreManifest(Path directory, Engine engine, byte[] encoded) { + this.directory = directory; + this.engine = engine; + identityDigest = Hashing.sha256().hashBytes(encoded).asBytes(); + } + + /** Creates a fresh manifest or requires byte-for-byte identity with the existing manifest. */ + public static PathStatePhysicalStoreManifest createOrOpen(Path directory, Engine engine) + throws IOException { + Path root = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); + Engine selected = Objects.requireNonNull(engine, "engine"); + if (Files.isSymbolicLink(root)) { + throw new IOException("path-state physical root must not be a symbolic link: " + root); + } + Files.createDirectories(root); + if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state physical root is not a directory: " + root); + } + byte[] expected = encode(selected); + Path manifest = root.resolve(FILE); + if (Files.exists(manifest, LinkOption.NOFOLLOW_LINKS)) { + byte[] actual = Files.readAllBytes(manifest); + if (!Arrays.equals(expected, actual)) { + throw new IOException("TASK-018 physical manifest identity mismatch"); + } + } else { + publish(manifest, expected); + } + return new PathStatePhysicalStoreManifest(root, selected, expected); + } + + /** Validates a previously created physical manifest without creating any path. */ + public static PathStatePhysicalStoreManifest validateExisting(Path directory, Engine engine) + throws IOException { + Path root = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); + Engine selected = Objects.requireNonNull(engine, "engine"); + if (Files.isSymbolicLink(root) + || !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state physical root is missing or invalid: " + root); + } + Path manifest = root.resolve(FILE); + if (!Files.isRegularFile(manifest, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state physical manifest is missing"); + } + byte[] expected = encode(selected); + byte[] actual = Files.readAllBytes(manifest); + if (!Arrays.equals(expected, actual)) { + throw new IOException("TASK-018 physical manifest identity mismatch"); + } + return new PathStatePhysicalStoreManifest(root, selected, expected); + } + + public Path getDirectory() { + return directory; + } + + public Engine getEngine() { + return engine; + } + + public byte[] getIdentityDigest() { + return Arrays.copyOf(identityDigest, identityDigest.length); + } + + private static byte[] encode(Engine engine) { + PathStateParticipantDescriptor descriptor = PathStateParticipantDescriptor.current(); + ByteBuffer encoded = ByteBuffer.allocate(HEADER_LENGTH + descriptor.getStores().size() + * Integer.BYTES); + encoded.putInt(MAGIC).putShort(VERSION).putShort((short) PathStateCommitmentCodec.FORMAT_VERSION) + .putShort((short) engine.ordinal()).putInt(descriptor.getStores().size()); + for (PathStateParticipantDescriptor.StoreIdentity store : descriptor.getStores()) { + encoded.putInt(store.getStoreId()); + } + return encoded.array(); + } + + private static void publish(Path manifest, byte[] encoded) throws IOException { + Path temporary = manifest.getParent().resolve(".MANIFEST-" + UUID.randomUUID()); + try { + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + channel.write(ByteBuffer.wrap(encoded)); + channel.force(true); + } + try { + Files.move(temporary, manifest, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException failure) { + throw new IOException("TASK-018 manifest requires atomic publication", failure); + } + } finally { + Files.deleteIfExists(temporary); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java new file mode 100644 index 00000000000..d7a4bfe0c1b --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java @@ -0,0 +1,1521 @@ +package org.tron.core.db2.stateroot; + +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import java.io.Closeable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import org.tron.common.crypto.Hash; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** + * Fresh-format physical owner for the TASK-018 27 participant databases and one super database. + * + *

Each native database owns three disjoint domains: {@code F|secureKey -> encodedLeaf}, + * {@code N|path -> nodeRlp}, and {@code M|name -> metadata}. This class deliberately does not + * provide an upgrade path from the old shared {@code base/nodes} layout. + */ +public final class PathStatePhysicalStoreSet implements Closeable { + + static final long DEFAULT_CHECKPOINT_ROWS = 1_000_000L; + static final long DEFAULT_CHECKPOINT_BYTES = 256L * 1024 * 1024; + + private static final String STORES_DIRECTORY = "stores"; + private static final String SUPER_DIRECTORY = "super"; + private static final String NODES_DIRECTORY = "nodes"; + private static final String REVERSE_DIRECTORY = "reverse"; + static final String INTENT_FILE = "INTENT"; + static final String CURRENT_FILE = "CURRENT"; + private static final byte FLAT_PREFIX = 'F'; + private static final byte NODE_PREFIX = 'N'; + private static final byte META_PREFIX = 'M'; + private static final byte[] FLAT_ROOT_METADATA = new byte[]{'f', 'l', 'a', 't', '-', 'r', 'o', + 'o', 't'}; + private static final byte[] FLAT_COMPLETE_METADATA = new byte[]{'f', 'l', 'a', 't', '-', 'c', + 'o', 'm', 'p', 'l', 'e', 't', 'e'}; + private static final byte[] FLAT_INGEST_CHECKPOINT = new byte[]{'f', 'l', 'a', 't', '-', 'i', + 'n', 'g', 'e', 's', 't'}; + private static final byte[] FLAT_INGEST_COMPLETE = new byte[]{'f', 'l', 'a', 't', '-', 'i', 'n', + 'g', 'e', 's', 't', '-', 'c', 'o', 'm', 'p', 'l', 'e', 't', 'e'}; + private static final byte[] FLAT_DIGEST_METADATA = new byte[]{'f', 'l', 'a', 't', '-', 'd', 'i', + 'g', 'e', 's', 't'}; + private static final byte[] STORE_GENERATION_METADATA = new byte[]{'s', 't', 'o', 'r', 'e', '-', + 'g', 'e', 'n', 'e', 'r', 'a', 't', 'i', 'o', 'n'}; + private static final byte[] SUPER_GENERATION_METADATA = new byte[]{'s', 'u', 'p', 'e', 'r', '-', + 'g', 'e', 'n', 'e', 'r', 'a', 't', 'i', 'o', 'n'}; + + private final Path directory; + private final PathStatePhysicalStoreManifest manifest; + private final PathStateParticipantScope scope; + private final Map participants = new LinkedHashMap<>(); + private final PhysicalStore superStore; + private boolean rootClaimed; + private boolean closed; + + private PathStatePhysicalStoreSet(PathStatePhysicalStoreManifest manifest, + PathStateParticipantScope scope) + throws IOException { + this.manifest = manifest; + this.directory = manifest.getDirectory(); + this.scope = requireExactScope(scope); + try { + for (PathStateParticipant participant : scope.getParticipants()) { + Path participantDirectory = directory.resolve(STORES_DIRECTORY).resolve(String.format( + "%02d-%s", participant.getStoreId(), participant.getDbName())).resolve(NODES_DIRECTORY); + participants.put(participant.getDbName(), new PhysicalStore(participantDirectory, + manifest.getEngine())); + } + superStore = new PhysicalStore(directory.resolve(SUPER_DIRECTORY).resolve(NODES_DIRECTORY), + manifest.getEngine()); + } catch (IOException | RuntimeException failure) { + closeAfterFailure(failure); + throw failure; + } + } + + /** Creates or opens only the TASK-018 fresh physical layout. */ + public static PathStatePhysicalStoreSet open(Path directory, PathStateParticipantScope scope, + Engine engine) throws IOException { + Path root = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); + if (Files.isSymbolicLink(root)) { + throw new IOException("path-state physical root must not be a symbolic link: " + root); + } + rejectLegacySharedNodes(root); + PathStatePhysicalStoreManifest manifest = PathStatePhysicalStoreManifest.createOrOpen(root, + Objects.requireNonNull(engine, "engine")); + return new PathStatePhysicalStoreSet(manifest, Objects.requireNonNull(scope, "scope")); + } + + /** Opens only a fully materialized physical layout; missing child databases fail closed. */ + public static PathStatePhysicalStoreSet openExisting(Path directory, + PathStateParticipantScope scope, Engine engine) throws IOException { + Path root = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); + rejectLegacySharedNodes(root); + PathStatePhysicalStoreManifest manifest = PathStatePhysicalStoreManifest.validateExisting( + root, Objects.requireNonNull(engine, "engine")); + PathStateParticipantScope admittedScope = requireExactScope(scope); + for (PathStateParticipant participant : admittedScope.getParticipants()) { + requireStoreDirectory(root.resolve(STORES_DIRECTORY).resolve(String.format( + "%02d-%s", participant.getStoreId(), participant.getDbName())) + .resolve(NODES_DIRECTORY)); + } + requireStoreDirectory(root.resolve(SUPER_DIRECTORY).resolve(NODES_DIRECTORY)); + return new PathStatePhysicalStoreSet(manifest, admittedScope); + } + + public synchronized PhysicalStore participant(String dbName) { + requireOpen(); + PhysicalStore store = participants.get(Objects.requireNonNull(dbName, "dbName")); + if (store == null) { + throw new IllegalArgumentException("unknown path-state participant: " + dbName); + } + return store; + } + + public synchronized PhysicalStore superStore() { + requireOpen(); + return superStore; + } + + /** Creates the one in-memory root owner backed by this set's physically separate node stores. */ + public synchronized PathStateRoot createRoot() { + requireOpen(); + if (rootClaimed) { + throw new IllegalStateException("path-state physical store set already has a root owner"); + } + rootClaimed = true; + return new PathStateRoot(scope, participant -> participant(participant.getDbName()).nodeStore(), + superStore.nodeStore()); + } + + public Path getDirectory() { + return directory; + } + + public byte[] getFormatDigest() { + return manifest.getIdentityDigest(); + } + + synchronized void saveIngestCheckpoint(String dbName, PathStatePhysicalIngestCheckpoint value) { + participant(dbName).putMetadata(FLAT_INGEST_CHECKPOINT, + Objects.requireNonNull(value, "value").encode()); + } + + synchronized PathStatePhysicalIngestCheckpoint ingestCheckpoint(String dbName) { + byte[] encoded = participant(dbName).getMetadata(FLAT_INGEST_CHECKPOINT); + return encoded == null ? null : PathStatePhysicalIngestCheckpoint.decode(encoded); + } + + /** Ingests exact physical rows into one F domain and durably advances its source cursor. */ + synchronized void ingestFlat(String dbName, PathStateRebuildCoordinator.SnapshotSource source, + long rowThreshold, long byteThreshold) throws IOException { + if (rowThreshold <= 0 || byteThreshold <= 0) { + throw new IllegalArgumentException("ingest checkpoint thresholds must be positive"); + } + PathStateParticipant participant = scope.require(dbName); + PathStateRebuildCoordinator.SnapshotSource pinned = Objects.requireNonNull(source, "source"); + byte[] identity = pinned.sourceIdentityDigest(); + if (identity.length != PathStateCommitmentCodec.ROOT_LENGTH) { + throw new IOException("physical ingest source identity must contain exactly 32 bytes"); + } + byte[] complete = participant(dbName).getMetadata(FLAT_INGEST_COMPLETE); + if (complete != null) { + if (!Arrays.equals(complete, identity)) { + throw new IOException("physical ingest completion source identity differs: " + dbName); + } + return; + } + PathStatePhysicalIngestCheckpoint prior = ingestCheckpoint(dbName); + if (prior != null && !Arrays.equals(prior.getSourceIdentity(), identity)) { + throw new IOException("physical ingest checkpoint source identity differs: " + dbName); + } + long[] progress = prior == null ? new long[]{0, 0} : new long[]{prior.getRows(), prior.getBytes()}; + byte[][] cursor = new byte[][]{prior == null ? null : prior.getCursor()}; + long[] sinceCheckpoint = new long[2]; + pinned.scanAfter(dbName, cursor[0], (physicalKey, physicalValue) -> { + byte[] key = Arrays.copyOf(physicalKey, physicalKey.length); + participant(dbName).putFlat(PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), key), + PathStateCommitmentCodec.presentLeafValue(physicalValue)); + cursor[0] = key; + progress[0]++; + progress[1] += key.length + physicalValue.length; + sinceCheckpoint[0]++; + sinceCheckpoint[1] += key.length + physicalValue.length; + if (sinceCheckpoint[0] >= rowThreshold || sinceCheckpoint[1] >= byteThreshold) { + saveIngestCheckpoint(dbName, new PathStatePhysicalIngestCheckpoint(identity, cursor[0], + progress[0], progress[1])); + sinceCheckpoint[0] = 0; + sinceCheckpoint[1] = 0; + } + }); + if (cursor[0] != null) { + saveIngestCheckpoint(dbName, new PathStatePhysicalIngestCheckpoint(identity, cursor[0], + progress[0], progress[1])); + } + participant(dbName).putMetadata(FLAT_INGEST_COMPLETE, identity); + } + + /** Validates one exact-27 pinned source, resumes all unfinished F ingests, then builds the root. */ + public synchronized PathStateRoot ingestAndBuild( + PathStateRebuildCoordinator.SnapshotSource source) + throws IOException { + return ingestAndBuild(source, DEFAULT_CHECKPOINT_ROWS, DEFAULT_CHECKPOINT_BYTES); + } + + /** Validates one exact-27 pinned source, resumes all unfinished F ingests, then builds the root. */ + synchronized PathStateRoot ingestAndBuild(PathStateRebuildCoordinator.SnapshotSource source, + long rowThreshold, long byteThreshold) throws IOException { + PathStateRebuildCoordinator.SnapshotSource pinned = Objects.requireNonNull(source, "source"); + PathStateRebuildCoordinator.SnapshotIdentity identity = Objects.requireNonNull( + pinned.identity(), "source identity"); + pinned.verifyIdentity(identity); + PathStateParticipantDescriptor.current().requireExactDatabases(pinned.databases()); + for (PathStateParticipant participant : scope.getParticipants()) { + ingestFlat(participant.getDbName(), pinned, rowThreshold, byteThreshold); + } + return buildRootFromFlat(); + } + + /** + * Persists a complete local F-domain snapshot and its root marker. + * + *

This is intentionally not a 28-database global publication protocol. Callers must add the + * TASK-018 global intent before treating this marker as a runtime CURRENT authority. + */ + public synchronized void persistFlatSnapshot(PathStateRoot root) { + requireOpen(); + PathStateRoot supplied = Objects.requireNonNull(root, "root"); + byte[] stateRoot = supplied.rootHash(); + for (PathStateRoot.LeafRecord record : supplied.leafRecords()) { + PathStateParticipant participant = participant(record.getStoreId()); + participant(participant.getDbName()).putFlat(record.getSecureKey(), + record.getEncodedValue()); + } + superStore.putMetadata(FLAT_ROOT_METADATA, stateRoot); + } + + /** Restores one root from every participant F domain and verifies the stored local root marker. */ + public synchronized PathStateRoot restoreRootFromFlat() throws IOException { + requireOpen(); + byte[] expectedRoot = superStore.getMetadata(FLAT_ROOT_METADATA); + if (expectedRoot == null || expectedRoot.length != PathStateCommitmentCodec.ROOT_LENGTH) { + throw new IllegalStateException("path-state physical F root marker is missing or invalid"); + } + PathStateRoot root = createRoot(); + List records = new ArrayList<>(); + for (PathStateParticipant participant : scope.getParticipants()) { + PhysicalStore store = participant(participant.getDbName()); + store.scanFlat(entry -> records.add(new PathStateRoot.LeafRecord(participant.getStoreId(), + unprefixedFlatKey(entry.getKey()), entry.getValue()))); + } + root.restoreLeaves(records, expectedRoot); + return root; + } + + /** + * Streams each participant F domain in secure-key order into its N domain and marks completion. + * + *

A valid per-Store completion marker skips that Store on retry. This method has no source + * database parameter and therefore cannot trigger a source rescan. + */ + public synchronized PathStateRoot buildRootFromFlat() throws IOException { + return buildRootFromFlat((participant, storeRoot) -> { }); + } + + synchronized PathStateRoot buildRootFromFlat(BuildFaultHook faultHook) throws IOException { + requireOpen(); + BuildFaultHook hook = Objects.requireNonNull(faultHook, "faultHook"); + PathStateRoot root = createRoot(); + List targets = new ArrayList<>(); + for (PathStateParticipant participant : scope.getParticipants()) { + PhysicalStore store = participant(participant.getDbName()); + byte[] completedRoot = store.getMetadata(FLAT_COMPLETE_METADATA); + if (completedRoot != null) { + if (completedRoot.length != PathStateCommitmentCodec.ROOT_LENGTH) { + throw new IllegalStateException("path-state physical FLAT_COMPLETE marker is invalid"); + } + byte[] flatDigest = requireDigest(store.getMetadata(FLAT_DIGEST_METADATA), + "path-state physical flat digest is missing or invalid"); + byte[] generation = requireDigest(store.getMetadata(STORE_GENERATION_METADATA), + "path-state physical Store generation is missing or invalid"); + requireSame(generation, participantGeneration(participant, flatDigest, completedRoot), + "path-state physical Store generation differs"); + targets.add(new PathStatePhysicalGlobalIntent.ParticipantTarget(participant.getStoreId(), + generation, flatDigest, completedRoot)); + root.completeRebuildParticipant(participant.getDbName(), completedRoot); + continue; + } + store.clearNodes(); + PathStateStackTrie trie = new PathStateStackTrie(store.nodeStore()::put); + Hasher flatHasher = Hashing.sha256().newHasher(); + store.scanFlat(entry -> { + byte[] secureKey = unprefixedFlatKey(entry.getKey()); + byte[] encodedValue = entry.getValue(); + flatHasher.putInt(secureKey.length).putBytes(secureKey) + .putInt(encodedValue.length).putBytes(encodedValue); + trie.update(secureKey, encodedValue); + }); + byte[] storeRoot = trie.rootHash(); + byte[] flatDigest = flatHasher.hash().asBytes(); + byte[] generation = participantGeneration(participant, flatDigest, storeRoot); + store.putMetadata(FLAT_DIGEST_METADATA, flatDigest); + store.putMetadata(STORE_GENERATION_METADATA, generation); + hook.beforeCompletion(participant, storeRoot); + store.putMetadata(FLAT_COMPLETE_METADATA, storeRoot); + targets.add(new PathStatePhysicalGlobalIntent.ParticipantTarget(participant.getStoreId(), + generation, flatDigest, storeRoot)); + root.completeRebuildParticipant(participant.getDbName(), storeRoot); + } + byte[] stateRoot = root.rootHash(); + superStore.putMetadata(SUPER_GENERATION_METADATA, + superGeneration(targets, stateRoot)); + superStore.putMetadata(FLAT_ROOT_METADATA, stateRoot); + return root; + } + + /** Publishes the exact prepared 27+1 target through INTENT, CURRENT, and intent retirement. */ + public synchronized byte[] publishCurrent() throws IOException { + return publishCurrent(syntheticMetadata(publicationTargetRoot()), stage -> { }); + } + + public synchronized byte[] publishCurrent(PathStateRootMetadata metadata) throws IOException { + return publishCurrent(metadata, stage -> { }); + } + + synchronized byte[] publishCurrent(PublicationFaultHook faultHook) throws IOException { + return publishCurrent(syntheticMetadata(publicationTargetRoot()), faultHook); + } + + synchronized byte[] publishCurrent(PathStateRootMetadata metadata, + PublicationFaultHook faultHook) throws IOException { + requireOpen(); + PublicationFaultHook hook = Objects.requireNonNull(faultHook, "faultHook"); + PathStatePhysicalGlobalIntent target = publicationTarget( + Objects.requireNonNull(metadata, "metadata")); + byte[] encoded = target.encode(); + Path intent = directory.resolve(INTENT_FILE); + Path current = directory.resolve(CURRENT_FILE); + PathStateMetadataFile.publishImmutableBytes(intent, encoded); + hook.after(PublicationStage.AFTER_INTENT); + PathStateMetadataFile.replaceCurrentBytes(current, encoded); + hook.after(PublicationStage.AFTER_CURRENT); + PathStateMetadataFile.deleteDurable(intent); + hook.after(PublicationStage.AFTER_RETIRE); + return target.getSuperRoot(); + } + + /** Returns the exact block-bound metadata bound into the validated physical CURRENT. */ + public synchronized PathStateRootMetadata currentMetadata() throws IOException { + requireOpen(); + PathStatePhysicalGlobalIntent current = currentTarget(); + return current.getMetadata(); + } + + synchronized void verifyReverseJournals(PathStateLayerLimits limits) throws IOException { + requireOpen(); + loadReverseJournals(Objects.requireNonNull(limits, "limits")); + } + + /** Computes one exact child target without changing F/N/M, INTENT, or CURRENT. */ + public synchronized PathStateRootMetadata previewTransition(PathStateBlockTransition transition) + throws IOException { + requireOpen(); + recoverPublication(); + return prepareTransition(transition).target.getMetadata(); + } + + /** Applies one block-final child to the physical 27+1 stores and publishes its CURRENT. */ + public synchronized PathStateRootMetadata applyAndPublish(PathStateBlockTransition transition) + throws IOException { + return applyAndPublish(transition, PathStateLayerLimits.defaults(), stage -> { }); + } + + synchronized PathStateRootMetadata applyAndPublish(PathStateBlockTransition transition, + TransitionFaultHook faultHook) throws IOException { + return applyAndPublish(transition, PathStateLayerLimits.defaults(), faultHook); + } + + synchronized PathStateRootMetadata applyAndPublish(PathStateBlockTransition transition, + PathStateLayerLimits limits, TransitionFaultHook faultHook) throws IOException { + requireOpen(); + recoverPublication(); + TransitionPlan plan = prepareTransition(transition); + TransitionFaultHook hook = Objects.requireNonNull(faultHook, "faultHook"); + byte[] encoded = plan.target.encode(); + byte[] encodedJournal = plan.journal.encode(); + Path journal = reverseJournalPath(plan.target.getMetadata()); + pruneReverseJournals(currentTarget(), Objects.requireNonNull(limits, "limits"), journal, + encodedJournal.length); + PathStateMetadataFile.publishImmutableBytes(journal, encodedJournal); + hook.after(TransitionStage.AFTER_JOURNAL); + Path intent = directory.resolve(INTENT_FILE); + Path current = directory.resolve(CURRENT_FILE); + PathStateMetadataFile.publishImmutableBytes(intent, encoded); + hook.after(TransitionStage.AFTER_INTENT); + for (ParticipantTransition participant : plan.participants) { + participant.store.applyParticipantTransition(participant.flatMutations, + participant.nodeMutations, participant.flatDigest, participant.generation, + participant.storeRoot); + hook.after(TransitionStage.AFTER_PARTICIPANT_BATCH); + } + plan.superStore.applySuperTransition(plan.superNodeMutations, + plan.target.getSuperGeneration(), plan.target.getSuperRoot()); + hook.after(TransitionStage.AFTER_SUPER_BATCH); + PathStateMetadataFile.replaceCurrentBytes(current, encoded); + hook.after(TransitionStage.AFTER_CURRENT); + PathStateMetadataFile.deleteDurable(intent); + hook.after(TransitionStage.AFTER_RETIRE); + return plan.target.getMetadata(); + } + + /** Rewinds through validated direct-parent journals to one exact bounded ancestor. */ + public synchronized PathStateRootMetadata rewindTo(long blockNumber, byte[] blockHash, + PathStateLayerLimits limits) throws IOException { + return rewindTo(blockNumber, blockHash, limits, stage -> { }); + } + + synchronized PathStateRootMetadata rewindTo(long blockNumber, byte[] blockHash, + PathStateLayerLimits limits, RewindFaultHook faultHook) throws IOException { + requireOpen(); + recoverPublication(); + byte[] targetHash = Arrays.copyOf(Objects.requireNonNull(blockHash, "blockHash"), + blockHash.length); + if (targetHash.length != PathStateRootMetadata.DIGEST_LENGTH) { + throw new IOException("physical rewind block hash must contain exactly 32 bytes"); + } + List chain = loadRewindChain(currentTarget(), blockNumber, + targetHash, Objects.requireNonNull(limits, "limits")); + RewindFaultHook hook = Objects.requireNonNull(faultHook, "faultHook"); + for (PathStatePhysicalReverseJournal journal : chain) { + applyReverseJournal(journal, hook); + } + PathStateRootMetadata rewound = currentMetadata(); + if (rewound.getBlockNumber() != blockNumber + || !Arrays.equals(rewound.getBlockHash(), targetHash)) { + throw new IOException("physical rewind did not reach the requested ancestor"); + } + return rewound; + } + + private TransitionPlan prepareTransition(PathStateBlockTransition supplied) throws IOException { + PathStateBlockTransition transition = Objects.requireNonNull(supplied, "transition"); + PathStatePhysicalGlobalIntent current = currentTarget(); + PathStateRootMetadata parent = current.getMetadata(); + if (transition.getBlockNumber() != parent.getBlockNumber() + 1 + || !Arrays.equals(transition.getParentHash(), parent.getBlockHash())) { + throw new IOException("physical path-state transition does not extend CURRENT"); + } + + Map recordings = new LinkedHashMap<>(); + PathStateRoot candidate = new PathStateRoot(scope, + participant -> recordings.computeIfAbsent(participant.getStoreId(), ignored -> + new RecordingNodeStore(participant(participant.getDbName()).nodeStore())), + recordings.computeIfAbsent(0, ignored -> + new RecordingNodeStore(superStore.nodeStore()))); + candidate.restoreStoredRoots(current.getSuperRoot()); + requireParticipantRoots(candidate, current); + if (!transition.getMutations().isEmpty()) { + candidate.apply(transition.getMutations()); + } + byte[] stateRoot = candidate.rootHash(); + + Map> flatByStore = new LinkedHashMap<>(); + for (PathStateMutation mutation : transition.getMutations()) { + PathStateParticipant participant = scope.require(mutation.getDbName()); + byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), + mutation.getPhysicalKey()); + byte[] encodedValue = mutation.isDelete() ? null + : PathStateCommitmentCodec.presentLeafValue(mutation.getPhysicalValue()); + flatByStore.computeIfAbsent(participant.getStoreId(), ignored -> new ArrayList<>()) + .add(new FlatMutation(secureKey, encodedValue)); + } + + List targets = new ArrayList<>(); + List participantTransitions = new ArrayList<>(); + List reverseStores = new ArrayList<>(); + for (PathStatePhysicalGlobalIntent.ParticipantTarget oldTarget + : current.getParticipants()) { + List flatMutations = flatByStore.get(oldTarget.getStoreId()); + if (flatMutations == null) { + targets.add(oldTarget); + continue; + } + PathStateParticipant participant = participant(oldTarget.getStoreId()); + byte[] storeRoot = candidate.participantRoot(participant.getDbName()); + byte[] flatDigest = nextFlatDigest(participant, oldTarget.getFlatDigest(), transition, + flatMutations); + byte[] generation = participantGeneration(participant, flatDigest, storeRoot); + targets.add(new PathStatePhysicalGlobalIntent.ParticipantTarget(participant.getStoreId(), + generation, flatDigest, storeRoot)); + participantTransitions.add(new ParticipantTransition( + participant(participant.getDbName()), flatMutations, + recordings.get(participant.getStoreId()).mutations(), flatDigest, generation, + storeRoot)); + List reverseFlat = new ArrayList<>(); + for (FlatMutation mutation : flatMutations) { + reverseFlat.add(new PathStatePhysicalReverseJournal.Entry(mutation.secureKey, + participant(participant.getDbName()).getFlat(mutation.secureKey))); + } + reverseStores.add(new PathStatePhysicalReverseJournal.StoreReverse( + participant.getStoreId(), reverseFlat, + recordings.get(participant.getStoreId()).reverseEntries())); + } + byte[] superGeneration = superGeneration(targets, stateRoot); + PathStateRootMetadata metadata = PathStateRootMetadata.layer(transition.getBlockNumber(), + transition.getBlockHash(), transition.getParentHash(), transition.getTimestamp(), + transition.getPhase(), manifest.getIdentityDigest(), parent.getStateRoot(), stateRoot, + transition.getPayloadDigest()); + PathStatePhysicalGlobalIntent target = new PathStatePhysicalGlobalIntent( + manifest.getIdentityDigest(), metadata, targets, superGeneration, stateRoot); + PathStatePhysicalReverseJournal journal = new PathStatePhysicalReverseJournal(target.encode(), + current.encode(), reverseStores, recordings.get(0).reverseEntries()); + return new TransitionPlan(target, participantTransitions, superStore, + recordings.get(0).mutations(), journal); + } + + private void applyReverseJournal(PathStatePhysicalReverseJournal journal, + RewindFaultHook hook) throws IOException { + PathStatePhysicalGlobalIntent child = PathStatePhysicalGlobalIntent.decode( + journal.getChildTarget()); + PathStatePhysicalGlobalIntent parent = PathStatePhysicalGlobalIntent.decode( + journal.getParentTarget()); + if (!Arrays.equals(currentTarget().encode(), child.encode())) { + throw new IOException("physical reverse journal child differs from CURRENT"); + } + Path intent = directory.resolve(INTENT_FILE); + Path current = directory.resolve(CURRENT_FILE); + PathStateMetadataFile.publishImmutableBytes(intent, parent.encode()); + hook.after(RewindStage.AFTER_INTENT); + for (PathStatePhysicalReverseJournal.StoreReverse reverse : journal.getStores()) { + PathStatePhysicalGlobalIntent.ParticipantTarget target = participantTarget(parent, + reverse.getStoreId()); + PhysicalStore store = participant(participant(reverse.getStoreId()).getDbName()); + store.applyParticipantTransition(flatMutations(reverse.getFlatEntries()), + nodeMutations(reverse.getNodeEntries()), target.getFlatDigest(), + target.getGeneration(), target.getStoreRoot()); + hook.after(RewindStage.AFTER_PARTICIPANT_BATCH); + } + superStore.applySuperTransition(nodeMutations(journal.getSuperNodes()), + parent.getSuperGeneration(), parent.getSuperRoot()); + hook.after(RewindStage.AFTER_SUPER_BATCH); + PathStateMetadataFile.replaceCurrentBytes(current, parent.encode()); + hook.after(RewindStage.AFTER_CURRENT); + PathStateMetadataFile.deleteDurable(intent); + hook.after(RewindStage.AFTER_RETIRE); + } + + private List loadRewindChain( + PathStatePhysicalGlobalIntent current, long targetNumber, byte[] targetHash, + PathStateLayerLimits limits) throws IOException { + if (targetNumber < 0 || targetNumber > current.getMetadata().getBlockNumber()) { + throw new IOException("physical rewind target height is outside CURRENT ancestry"); + } + Map journals = loadReverseJournals(limits); + List chain = new ArrayList<>(); + PathStatePhysicalGlobalIntent cursor = current; + while (cursor.getMetadata().getBlockNumber() > targetNumber) { + PathStatePhysicalReverseJournal journal = journals.get(new BytesKey(cursor.encode())); + if (journal == null) { + throw new IOException("physical reverse journal ancestry is missing"); + } + chain.add(journal); + cursor = PathStatePhysicalGlobalIntent.decode(journal.getParentTarget()); + } + if (cursor.getMetadata().getBlockNumber() != targetNumber + || !Arrays.equals(cursor.getMetadata().getBlockHash(), targetHash)) { + throw new IOException("physical rewind target is not an exact ancestor"); + } + return chain; + } + + private Map loadReverseJournals( + PathStateLayerLimits limits) throws IOException { + Map journals = new LinkedHashMap<>(); + Path reverse = directory.resolve(REVERSE_DIRECTORY); + if (!Files.exists(reverse, LinkOption.NOFOLLOW_LINKS)) { + return journals; + } + if (!Files.isDirectory(reverse, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(reverse)) { + throw new IOException("physical reverse journal root is not a direct directory"); + } + long total = 0; + int count = 0; + try (Stream files = Files.list(reverse)) { + for (Path file : (Iterable) files::iterator) { + if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("physical reverse journal is not a regular file: " + file); + } + long length = Files.size(file); + total = Math.addExact(total, length); + count = Math.addExact(count, 1); + PathStatePhysicalReverseJournal journal; + try { + journal = PathStatePhysicalReverseJournal.decode( + PathStateMetadataFile.loadImmutableBytes(file, + PathStatePhysicalReverseJournal.MAX_ENCODED_LENGTH)); + } catch (IllegalArgumentException invalid) { + throw new IOException("physical reverse journal is corrupt: " + file, invalid); + } + if (journals.put(new BytesKey(journal.getChildTarget()), journal) != null) { + throw new IOException("physical reverse journal child identity is duplicated"); + } + } + } catch (ArithmeticException overflow) { + throw new IOException("physical reverse journal usage overflow", overflow); + } + if (count > limits.getMaxLayers() || total > limits.getMaxLogicalBytes()) { + throw new IOException("physical reverse journal exceeds configured bounds"); + } + return journals; + } + + private void pruneReverseJournals(PathStatePhysicalGlobalIntent current, + PathStateLayerLimits limits, Path candidate, long candidateBytes) throws IOException { + if (candidateBytes > limits.getMaxLogicalBytes()) { + throw new IOException("physical reverse journal candidate exceeds byte limit"); + } + Path reverse = directory.resolve(REVERSE_DIRECTORY); + if (!Files.exists(reverse, LinkOption.NOFOLLOW_LINKS)) { + return; + } + Map decoded = loadReverseJournals( + new PathStateLayerLimits(Integer.MAX_VALUE, Long.MAX_VALUE)); + Set keep = new HashSet<>(); + byte[] cursor = current.encode(); + int remainingCount = limits.getMaxLayers() - 1; + long remainingBytes = limits.getMaxLogicalBytes() - candidateBytes; + while (remainingCount > 0) { + BytesKey identity = new BytesKey(cursor); + PathStatePhysicalReverseJournal journal = decoded.get(identity); + if (journal == null) { + break; + } + Path path = reverseJournalPath(PathStatePhysicalGlobalIntent.decode( + journal.getChildTarget()).getMetadata()); + long length = Files.size(path); + if (length > remainingBytes) { + break; + } + keep.add(identity); + remainingCount--; + remainingBytes -= length; + cursor = journal.getParentTarget(); + } + try (Stream files = Files.list(reverse)) { + for (Path file : (Iterable) files::iterator) { + if (file.equals(candidate)) { + continue; + } + PathStatePhysicalReverseJournal journal = PathStatePhysicalReverseJournal.decode( + PathStateMetadataFile.loadImmutableBytes(file, + PathStatePhysicalReverseJournal.MAX_ENCODED_LENGTH)); + if (!keep.contains(new BytesKey(journal.getChildTarget()))) { + PathStateMetadataFile.deleteDurable(file); + } + } + } + } + + private Path reverseJournalPath(PathStateRootMetadata child) { + StringBuilder hash = new StringBuilder(child.getBlockHash().length * 2); + for (byte value : child.getBlockHash()) { + hash.append(String.format("%02x", value & 0xff)); + } + return directory.resolve(REVERSE_DIRECTORY).resolve(String.format("%020d-%s.journal", + child.getBlockNumber(), hash)); + } + + private static PathStatePhysicalGlobalIntent.ParticipantTarget participantTarget( + PathStatePhysicalGlobalIntent target, int storeId) throws IOException { + for (PathStatePhysicalGlobalIntent.ParticipantTarget participant : target.getParticipants()) { + if (participant.getStoreId() == storeId) { + return participant; + } + } + throw new IOException("physical reverse target Store is absent"); + } + + private static List flatMutations( + List entries) { + List mutations = new ArrayList<>(); + for (PathStatePhysicalReverseJournal.Entry entry : entries) { + mutations.add(new FlatMutation(entry.getKey(), entry.getOldValue())); + } + return mutations; + } + + private static List nodeMutations( + List entries) { + List mutations = new ArrayList<>(); + for (PathStatePhysicalReverseJournal.Entry entry : entries) { + mutations.add(new NodeMutation(entry.getKey(), entry.getOldValue())); + } + return mutations; + } + + private byte[] nextFlatDigest(PathStateParticipant participant, byte[] previous, + PathStateBlockTransition transition, List mutations) { + Hasher hasher = Hashing.sha256().newHasher() + .putString("java-tron/path-state/flat-transition/v1", StandardCharsets.US_ASCII) + .putBytes(manifest.getIdentityDigest()).putInt(participant.getStoreId()) + .putBytes(previous).putBytes(transition.getPayloadDigest()).putInt(mutations.size()); + for (FlatMutation mutation : mutations) { + hasher.putInt(mutation.secureKey.length).putBytes(mutation.secureKey); + if (mutation.encodedValue == null) { + hasher.putInt(-1); + } else { + hasher.putInt(mutation.encodedValue.length).putBytes(mutation.encodedValue); + } + } + return hasher.hash().asBytes(); + } + + /** Reconciles an interrupted global publication without accepting a partial 28-DB target. */ + public synchronized PublicationRecovery recoverPublication() throws IOException { + requireOpen(); + Path intentPath = directory.resolve(INTENT_FILE); + Path currentPath = directory.resolve(CURRENT_FILE); + boolean hasIntent = Files.exists(intentPath, LinkOption.NOFOLLOW_LINKS); + boolean hasCurrent = Files.exists(currentPath, LinkOption.NOFOLLOW_LINKS); + boolean currentValid = false; + IOException invalidCurrent = null; + if (hasCurrent) { + try { + currentValid = matchesPreparedTarget(loadGlobalTarget(currentPath)); + } catch (IOException failure) { + invalidCurrent = failure; + } + } + if (!hasIntent) { + if (hasCurrent && !currentValid) { + throw new IOException("path-state physical CURRENT is not the exact prepared 28-DB target", + invalidCurrent); + } + return PublicationRecovery.NONE; + } + + PathStatePhysicalGlobalIntent intent; + boolean intentValid; + try { + intent = loadGlobalTarget(intentPath); + intentValid = matchesPreparedTarget(intent); + } catch (IOException invalidIntent) { + if (!currentValid) { + throw invalidIntent; + } + PathStateMetadataFile.deleteDurable(intentPath); + return PublicationRecovery.RETAINED_CURRENT; + } + if (!intentValid) { + if (!currentValid) { + throw new IOException("path-state physical INTENT is not the exact prepared 28-DB target"); + } + PathStateMetadataFile.deleteDurable(intentPath); + return PublicationRecovery.RETAINED_CURRENT; + } + PathStateMetadataFile.replaceCurrentBytes(currentPath, intent.encode()); + PathStateMetadataFile.deleteDurable(intentPath); + return PublicationRecovery.COMPLETED_INTENT; + } + + /** Deletes one exact physical key, commits changed paths, and republishes the 28-DB target. */ + public synchronized byte[] deleteAndPublish(String dbName, byte[] physicalKey) + throws IOException { + return deleteAndPublish(dbName, physicalKey, stage -> { }).getStateRoot(); + } + + synchronized PhysicalDeleteResult deleteAndPublish(String dbName, byte[] physicalKey, + DeleteFaultHook faultHook) throws IOException { + requireOpen(); + DeleteFaultHook hook = Objects.requireNonNull(faultHook, "faultHook"); + recoverPublication(); + PathStatePhysicalGlobalIntent current = currentTarget(); + PathStateRoot restored = createRoot(); + restored.restoreStoredRoots(current.getSuperRoot()); + requireParticipantRoots(restored, current); + PathStateRoot.Snapshot snapshot = restored.snapshot(); + + Map recordings = new LinkedHashMap<>(); + PathStateRoot candidate = PathStateRoot.fromSnapshot(scope, + participant -> recordings.computeIfAbsent(participant.getStoreId(), ignored -> + new RecordingNodeStore(participant(participant.getDbName()).nodeStore())), + recordings.computeIfAbsent(0, ignored -> new RecordingNodeStore(superStore.nodeStore())), + snapshot); + PathStateParticipant targetParticipant = scope.require(dbName); + byte[] rawKey = Arrays.copyOf(Objects.requireNonNull(physicalKey, "physicalKey"), + physicalKey.length); + byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(targetParticipant.getStoreId(), + rawKey); + PhysicalStore targetStore = participant(targetParticipant.getDbName()); + if (targetStore.getFlat(secureKey) == null) { + throw new IOException("path-state physical delete leaf is missing: " + dbName); + } + candidate.delete(targetParticipant.getDbName(), rawKey); + byte[] stateRoot = candidate.rootHash(); + byte[] storeRoot = candidate.participantRoot(targetParticipant.getDbName()); + RecordingNodeStore participantChanges = recordings.get(targetParticipant.getStoreId()); + requireOnlyTargetParticipantChanged(recordings, targetParticipant.getStoreId()); + byte[] flatDigest = flatDigestExcluding(targetStore, secureKey); + byte[] generation = participantGeneration(targetParticipant, flatDigest, storeRoot); + targetStore.applyParticipantDelete(secureKey, participantChanges.mutations(), flatDigest, + generation, storeRoot); + hook.after(DeleteStage.AFTER_PARTICIPANT_BATCH); + + List targets = replaceParticipantTarget( + current.getParticipants(), targetParticipant.getStoreId(), generation, flatDigest, + storeRoot); + RecordingNodeStore superChanges = recordings.get(0); + byte[] nextSuperGeneration = superGeneration(targets, stateRoot); + superStore.applySuperTransition(superChanges.mutations(), nextSuperGeneration, stateRoot); + hook.after(DeleteStage.AFTER_SUPER_BATCH); + + byte[] published = publishCurrent(metadataWithRoot(current.getMetadata(), stateRoot)); + hook.after(DeleteStage.AFTER_CURRENT); + return new PhysicalDeleteResult(published, participantChanges.putCount(), + participantChanges.deleteCount(), superChanges.putCount(), superChanges.deleteCount()); + } + + private PathStatePhysicalGlobalIntent currentTarget() throws IOException { + Path currentPath = directory.resolve(CURRENT_FILE); + if (!Files.exists(currentPath, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state physical CURRENT is missing"); + } + PathStatePhysicalGlobalIntent current = loadGlobalTarget(currentPath); + if (!matchesPreparedTarget(current)) { + throw new IOException("path-state physical CURRENT differs from prepared target"); + } + return current; + } + + private void requireParticipantRoots(PathStateRoot root, + PathStatePhysicalGlobalIntent current) throws IOException { + for (PathStatePhysicalGlobalIntent.ParticipantTarget target : current.getParticipants()) { + PathStateParticipant participant = participant(target.getStoreId()); + requireSame(target.getStoreRoot(), root.participantRoot(participant.getDbName()), + "path-state physical participant root differs from CURRENT: " + + participant.getDbName()); + } + } + + private static void requireOnlyTargetParticipantChanged( + Map recordings, int targetStoreId) { + for (Map.Entry entry : recordings.entrySet()) { + if (entry.getKey() != 0 && entry.getKey() != targetStoreId + && !entry.getValue().isEmpty()) { + throw new IllegalStateException("physical delete changed another participant Store"); + } + } + } + + private static List replaceParticipantTarget( + List current, int storeId, + byte[] generation, byte[] flatDigest, byte[] storeRoot) { + List targets = new ArrayList<>(); + boolean replaced = false; + for (PathStatePhysicalGlobalIntent.ParticipantTarget target : current) { + if (target.getStoreId() == storeId) { + targets.add(new PathStatePhysicalGlobalIntent.ParticipantTarget(storeId, generation, + flatDigest, storeRoot)); + replaced = true; + } else { + targets.add(target); + } + } + if (!replaced) { + throw new IllegalArgumentException("physical delete Store ID is not in CURRENT"); + } + return targets; + } + + private static byte[] flatDigestExcluding(PhysicalStore store, byte[] excludedSecureKey) + throws IOException { + Hasher hasher = Hashing.sha256().newHasher(); + boolean[] excluded = new boolean[1]; + store.scanFlat(entry -> { + byte[] secureKey = unprefixedFlatKey(entry.getKey()); + if (Arrays.equals(secureKey, excludedSecureKey)) { + excluded[0] = true; + return; + } + byte[] encodedValue = entry.getValue(); + hasher.putInt(secureKey.length).putBytes(secureKey) + .putInt(encodedValue.length).putBytes(encodedValue); + }); + if (!excluded[0]) { + throw new IOException("path-state physical delete leaf disappeared during digest scan"); + } + return hasher.hash().asBytes(); + } + + private byte[] publicationTargetRoot() throws IOException { + return requireDigest(superStore.getMetadata(FLAT_ROOT_METADATA), + "path-state physical super root is missing or invalid"); + } + + private PathStatePhysicalGlobalIntent publicationTarget(PathStateRootMetadata metadata) + throws IOException { + List targets = new ArrayList<>(); + for (PathStateParticipant participant : scope.getParticipants()) { + PhysicalStore store = participant(participant.getDbName()); + byte[] storeRoot = requireDigest(store.getMetadata(FLAT_COMPLETE_METADATA), + "path-state physical Store root is missing or invalid: " + participant.getDbName()); + requireRootNode(store, storeRoot, + "path-state physical Store root node differs: " + participant.getDbName()); + byte[] flatDigest = requireDigest(store.getMetadata(FLAT_DIGEST_METADATA), + "path-state physical flat digest is missing or invalid: " + participant.getDbName()); + byte[] generation = requireDigest(store.getMetadata(STORE_GENERATION_METADATA), + "path-state physical Store generation is missing or invalid: " + + participant.getDbName()); + requireSame(generation, participantGeneration(participant, flatDigest, storeRoot), + "path-state physical Store generation differs: " + participant.getDbName()); + targets.add(new PathStatePhysicalGlobalIntent.ParticipantTarget(participant.getStoreId(), + generation, flatDigest, storeRoot)); + } + byte[] superRoot = requireDigest(superStore.getMetadata(FLAT_ROOT_METADATA), + "path-state physical super root is missing or invalid"); + requireRootNode(superStore, superRoot, + "path-state physical super root node differs"); + byte[] generation = requireDigest(superStore.getMetadata(SUPER_GENERATION_METADATA), + "path-state physical super generation is missing or invalid"); + requireSame(generation, superGeneration(targets, superRoot), + "path-state physical super generation differs"); + return new PathStatePhysicalGlobalIntent(manifest.getIdentityDigest(), metadata, targets, + generation, superRoot); + } + + private boolean matchesPreparedTarget(PathStatePhysicalGlobalIntent target) throws IOException { + try { + return Arrays.equals(target.encode(), publicationTarget(target.getMetadata()).encode()); + } catch (IllegalArgumentException invalidTarget) { + throw new IOException("path-state physical target metadata differs from prepared storage", + invalidTarget); + } + } + + private PathStateRootMetadata syntheticMetadata(byte[] stateRoot) { + byte[] zero = new byte[PathStateRootMetadata.DIGEST_LENGTH]; + return PathStateRootMetadata.base(0, zero, zero, 0, + PathStateCanonicalizer.P66Phase.P66_OFF, manifest.getIdentityDigest(), stateRoot, zero); + } + + private PathStateRootMetadata metadataWithRoot(PathStateRootMetadata previous, + byte[] stateRoot) { + if (previous.getKind() == PathStateRootMetadata.Kind.BASE) { + return PathStateRootMetadata.base(previous.getBlockNumber(), previous.getBlockHash(), + previous.getParentHash(), previous.getTimestamp(), previous.getPhase(), + manifest.getIdentityDigest(), stateRoot, previous.getPayloadDigest()); + } + return PathStateRootMetadata.layer(previous.getBlockNumber(), previous.getBlockHash(), + previous.getParentHash(), previous.getTimestamp(), previous.getPhase(), + manifest.getIdentityDigest(), previous.getParentStateRoot(), stateRoot, + previous.getPayloadDigest()); + } + + private static PathStatePhysicalGlobalIntent loadGlobalTarget(Path path) throws IOException { + try { + return PathStatePhysicalGlobalIntent.decode(PathStateMetadataFile.loadImmutableBytes(path, + PathStatePhysicalGlobalIntent.MAX_ENCODED_LENGTH)); + } catch (IllegalArgumentException invalid) { + throw new IOException("path-state physical global target is corrupt: " + path, invalid); + } + } + + private byte[] participantGeneration(PathStateParticipant participant, byte[] flatDigest, + byte[] storeRoot) { + return Hashing.sha256().newHasher() + .putString("java-tron/path-state/participant-generation/v1", StandardCharsets.US_ASCII) + .putBytes(manifest.getIdentityDigest()).putInt(participant.getStoreId()) + .putBytes(flatDigest).putBytes(storeRoot).hash().asBytes(); + } + + private byte[] superGeneration( + List targets, byte[] superRoot) { + Hasher hasher = Hashing.sha256().newHasher() + .putString("java-tron/path-state/super-generation/v1", StandardCharsets.US_ASCII) + .putBytes(manifest.getIdentityDigest()).putInt(targets.size()); + for (PathStatePhysicalGlobalIntent.ParticipantTarget target : targets) { + hasher.putInt(target.getStoreId()).putBytes(target.getGeneration()) + .putBytes(target.getFlatDigest()).putBytes(target.getStoreRoot()); + } + return hasher.putBytes(superRoot).hash().asBytes(); + } + + private static byte[] requireDigest(byte[] value, String error) throws IOException { + if (value == null || value.length != PathStatePhysicalGlobalIntent.DIGEST_LENGTH) { + throw new IOException(error); + } + return Arrays.copyOf(value, value.length); + } + + private static void requireSame(byte[] expected, byte[] actual, String error) + throws IOException { + if (!Arrays.equals(expected, actual)) { + throw new IOException(error); + } + } + + private static void requireRootNode(PhysicalStore store, byte[] expectedRoot, String error) + throws IOException { + byte[] encodedRoot = store.nodeStore().get(new byte[0]); + if (Arrays.equals(expectedRoot, Hash.EMPTY_TRIE_HASH)) { + if (encodedRoot != null) { + throw new IOException(error); + } + } else if (encodedRoot == null || !Arrays.equals(expectedRoot, Hash.sha3(encodedRoot))) { + throw new IOException(error); + } + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + IOException failure = null; + for (PhysicalStore store : participants.values()) { + try { + store.close(); + } catch (IOException closeFailure) { + failure = append(failure, closeFailure); + } + } + try { + superStore.close(); + } catch (IOException closeFailure) { + failure = append(failure, closeFailure); + } + if (failure != null) { + throw failure; + } + } + + private void closeAfterFailure(Throwable original) { + for (PhysicalStore store : participants.values()) { + try { + store.close(); + } catch (IOException closeFailure) { + original.addSuppressed(closeFailure); + } + } + } + + private static void rejectLegacySharedNodes(Path root) throws IOException { + Path oldNodes = root.resolve(PathStateStoreManifest.BASE_DIRECTORY).resolve(NODES_DIRECTORY); + if (Files.exists(oldNodes, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("TASK-018 physical layout rejects legacy shared base/nodes: " + + oldNodes); + } + } + + private static void requireStoreDirectory(Path path) throws IOException { + if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("path-state physical Store directory is missing: " + path); + } + } + + private void requireOpen() { + if (closed) { + throw new IllegalStateException("path-state physical store set is closed: " + directory); + } + } + + private PathStateParticipant participant(int storeId) { + for (PathStateParticipant participant : scope.getParticipants()) { + if (participant.getStoreId() == storeId) { + return participant; + } + } + throw new IllegalArgumentException("unknown path-state Store ID: " + storeId); + } + + private static IOException append(IOException previous, IOException next) { + if (previous == null) { + return next; + } + previous.addSuppressed(next); + return previous; + } + + private static PathStateParticipantScope requireExactScope(PathStateParticipantScope scope) { + PathStateParticipantScope supplied = Objects.requireNonNull(scope, "scope"); + List dbNames = new ArrayList<>(); + for (PathStateParticipant participant : supplied.getParticipants()) { + dbNames.add(participant.getDbName()); + } + PathStateParticipantDescriptor descriptor = PathStateParticipantDescriptor.current(); + descriptor.requireExactDatabases(dbNames); + for (PathStateParticipant participant : supplied.getParticipants()) { + if (descriptor.require(participant.getDbName()).getStoreId() != participant.getStoreId()) { + throw new IllegalArgumentException("path-state physical Store ID differs: " + + participant.getDbName()); + } + } + return supplied; + } + + private static byte[] unprefixedFlatKey(byte[] storedKey) { + byte[] key = Arrays.copyOf(Objects.requireNonNull(storedKey, "storedKey"), storedKey.length); + if (key.length != PathStateCommitmentCodec.ROOT_LENGTH + 1 || key[0] != FLAT_PREFIX) { + throw new IllegalStateException("path-state physical F key is corrupt"); + } + return Arrays.copyOfRange(key, 1, key.length); + } + + /** One participant or super database with independent F/N/M key domains. */ + public static final class PhysicalStore implements Closeable { + + private final PathStateNativeNodeStore nativeStore; + + private PhysicalStore(Path directory, Engine engine) throws IOException { + nativeStore = PathStateNativeNodeStore.open(directory, engine); + } + + public void putFlat(byte[] secureKey, byte[] encodedLeaf) { + nativeStore.put(prefixed(FLAT_PREFIX, secureKey, "secureKey"), encodedLeaf); + } + + public byte[] getFlat(byte[] secureKey) { + return nativeStore.get(prefixed(FLAT_PREFIX, secureKey, "secureKey")); + } + + public void deleteFlat(byte[] secureKey) { + nativeStore.delete(prefixed(FLAT_PREFIX, secureKey, "secureKey")); + } + + void scanFlat(PathStateNativeNodeStore.EntryConsumer consumer) throws IOException { + nativeStore.scanPrefix(new byte[]{FLAT_PREFIX}, Objects.requireNonNull(consumer, "consumer")); + } + + public PathNodeStore nodeStore() { + return new PhysicalNodeStore(nativeStore); + } + + void clearNodes() throws IOException { + List pending = new ArrayList<>(4096); + nativeStore.scanPrefix(new byte[]{NODE_PREFIX}, entry -> { + pending.add(PathStateNativeNodeStore.BatchMutation.delete(entry.getKey())); + if (pending.size() == 4096) { + nativeStore.writeBatch(new ArrayList<>(pending)); + pending.clear(); + } + }); + if (!pending.isEmpty()) { + nativeStore.writeBatch(pending); + } + } + + void applyParticipantDelete(byte[] secureKey, List nodeMutations, + byte[] flatDigest, byte[] generation, byte[] storeRoot) { + List mutations = new ArrayList<>(); + mutations.add(PathStateNativeNodeStore.BatchMutation.delete( + prefixed(FLAT_PREFIX, secureKey, "secureKey"))); + appendNodeMutations(mutations, nodeMutations); + mutations.add(metadataMutation(FLAT_DIGEST_METADATA, flatDigest)); + mutations.add(metadataMutation(STORE_GENERATION_METADATA, generation)); + mutations.add(metadataMutation(FLAT_COMPLETE_METADATA, storeRoot)); + nativeStore.writeBatch(mutations); + } + + void applyParticipantTransition(List flatMutations, + List nodeMutations, byte[] flatDigest, byte[] generation, + byte[] storeRoot) { + List mutations = new ArrayList<>(); + for (FlatMutation mutation : Objects.requireNonNull(flatMutations, "flatMutations")) { + byte[] key = prefixed(FLAT_PREFIX, mutation.secureKey, "secureKey"); + mutations.add(mutation.encodedValue == null + ? PathStateNativeNodeStore.BatchMutation.delete(key) + : PathStateNativeNodeStore.BatchMutation.put(key, mutation.encodedValue)); + } + appendNodeMutations(mutations, nodeMutations); + mutations.add(metadataMutation(FLAT_DIGEST_METADATA, flatDigest)); + mutations.add(metadataMutation(STORE_GENERATION_METADATA, generation)); + mutations.add(metadataMutation(FLAT_COMPLETE_METADATA, storeRoot)); + nativeStore.writeBatch(mutations); + } + + void applySuperTransition(List nodeMutations, byte[] generation, + byte[] superRoot) { + List mutations = new ArrayList<>(); + appendNodeMutations(mutations, nodeMutations); + mutations.add(metadataMutation(SUPER_GENERATION_METADATA, generation)); + mutations.add(metadataMutation(FLAT_ROOT_METADATA, superRoot)); + nativeStore.writeBatch(mutations); + } + + private static void appendNodeMutations( + List target, + List nodeMutations) { + for (NodeMutation mutation : Objects.requireNonNull(nodeMutations, "nodeMutations")) { + byte[] key = prefixed(NODE_PREFIX, mutation.path, "path"); + target.add(mutation.encodedNode == null + ? PathStateNativeNodeStore.BatchMutation.delete(key) + : PathStateNativeNodeStore.BatchMutation.put(key, mutation.encodedNode)); + } + } + + private static PathStateNativeNodeStore.BatchMutation metadataMutation(byte[] name, + byte[] value) { + return PathStateNativeNodeStore.BatchMutation.put(prefixed(META_PREFIX, name, + "metadata name"), value); + } + + public void putMetadata(byte[] name, byte[] value) { + nativeStore.put(prefixed(META_PREFIX, name, "metadata name"), value); + } + + public byte[] getMetadata(byte[] name) { + return nativeStore.get(prefixed(META_PREFIX, name, "metadata name")); + } + + void deleteMetadata(byte[] name) { + nativeStore.delete(prefixed(META_PREFIX, name, "metadata name")); + } + + public Path getDirectory() { + return nativeStore.getDirectory(); + } + + @Override + public void close() throws IOException { + nativeStore.close(); + } + } + + private static final class PhysicalNodeStore implements PathNodeStore { + + private final PathStateNativeNodeStore nativeStore; + + private PhysicalNodeStore(PathStateNativeNodeStore nativeStore) { + this.nativeStore = nativeStore; + } + + @Override + public byte[] get(byte[] path) { + return nativeStore.get(prefixed(NODE_PREFIX, path, "path")); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + nativeStore.put(prefixed(NODE_PREFIX, path, "path"), encodedNode); + } + + @Override + public void delete(byte[] path) { + nativeStore.delete(prefixed(NODE_PREFIX, path, "path")); + } + } + + private static final class RecordingNodeStore implements PathNodeStore { + + private final PathNodeStore base; + private final Map changes = new LinkedHashMap<>(); + + private RecordingNodeStore(PathNodeStore base) { + this.base = Objects.requireNonNull(base, "base"); + } + + @Override + public byte[] get(byte[] path) { + BytesKey key = new BytesKey(path); + if (changes.containsKey(key)) { + byte[] value = changes.get(key); + return value == null ? null : Arrays.copyOf(value, value.length); + } + return base.get(path); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + changes.put(new BytesKey(path), Arrays.copyOf( + Objects.requireNonNull(encodedNode, "encodedNode"), encodedNode.length)); + } + + @Override + public void delete(byte[] path) { + changes.put(new BytesKey(path), null); + } + + private boolean isEmpty() { + return changes.isEmpty(); + } + + private List mutations() { + List mutations = new ArrayList<>(); + for (Map.Entry change : changes.entrySet()) { + mutations.add(new NodeMutation(change.getKey().bytes, change.getValue())); + } + return mutations; + } + + private List reverseEntries() { + List entries = new ArrayList<>(); + for (BytesKey key : changes.keySet()) { + entries.add(new PathStatePhysicalReverseJournal.Entry(key.bytes, base.get(key.bytes))); + } + return entries; + } + + private int putCount() { + int count = 0; + for (byte[] value : changes.values()) { + if (value != null) { + count++; + } + } + return count; + } + + private int deleteCount() { + return changes.size() - putCount(); + } + } + + private static final class NodeMutation { + + private final byte[] path; + private final byte[] encodedNode; + + private NodeMutation(byte[] path, byte[] encodedNode) { + this.path = Arrays.copyOf(Objects.requireNonNull(path, "path"), path.length); + this.encodedNode = encodedNode == null ? null + : Arrays.copyOf(encodedNode, encodedNode.length); + } + } + + private static final class FlatMutation { + + private final byte[] secureKey; + private final byte[] encodedValue; + + private FlatMutation(byte[] secureKey, byte[] encodedValue) { + this.secureKey = Arrays.copyOf(Objects.requireNonNull(secureKey, "secureKey"), + secureKey.length); + this.encodedValue = encodedValue == null ? null + : Arrays.copyOf(encodedValue, encodedValue.length); + } + } + + private static final class ParticipantTransition { + + private final PhysicalStore store; + private final List flatMutations; + private final List nodeMutations; + private final byte[] flatDigest; + private final byte[] generation; + private final byte[] storeRoot; + + private ParticipantTransition(PhysicalStore store, List flatMutations, + List nodeMutations, byte[] flatDigest, byte[] generation, + byte[] storeRoot) { + this.store = store; + this.flatMutations = flatMutations; + this.nodeMutations = nodeMutations; + this.flatDigest = flatDigest; + this.generation = generation; + this.storeRoot = storeRoot; + } + } + + private static final class TransitionPlan { + + private final PathStatePhysicalGlobalIntent target; + private final List participants; + private final PhysicalStore superStore; + private final List superNodeMutations; + private final PathStatePhysicalReverseJournal journal; + + private TransitionPlan(PathStatePhysicalGlobalIntent target, + List participants, PhysicalStore superStore, + List superNodeMutations, PathStatePhysicalReverseJournal journal) { + this.target = target; + this.participants = participants; + this.superStore = superStore; + this.superNodeMutations = superNodeMutations; + this.journal = journal; + } + } + + private static final class BytesKey { + + private final byte[] bytes; + + private BytesKey(byte[] bytes) { + this.bytes = Arrays.copyOf(Objects.requireNonNull(bytes, "bytes"), bytes.length); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof BytesKey + && Arrays.equals(bytes, ((BytesKey) other).bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + } + + @FunctionalInterface + interface BuildFaultHook { + + void beforeCompletion(PathStateParticipant participant, byte[] storeRoot) throws IOException; + } + + enum PublicationStage { + AFTER_INTENT, + AFTER_CURRENT, + AFTER_RETIRE + } + + public enum PublicationRecovery { + NONE, + RETAINED_CURRENT, + COMPLETED_INTENT + } + + enum DeleteStage { + AFTER_PARTICIPANT_BATCH, + AFTER_SUPER_BATCH, + AFTER_CURRENT + } + + enum TransitionStage { + AFTER_JOURNAL, + AFTER_INTENT, + AFTER_PARTICIPANT_BATCH, + AFTER_SUPER_BATCH, + AFTER_CURRENT, + AFTER_RETIRE + } + + @FunctionalInterface + interface TransitionFaultHook { + + void after(TransitionStage stage) throws IOException; + } + + enum RewindStage { + AFTER_INTENT, + AFTER_PARTICIPANT_BATCH, + AFTER_SUPER_BATCH, + AFTER_CURRENT, + AFTER_RETIRE + } + + @FunctionalInterface + interface RewindFaultHook { + + void after(RewindStage stage) throws IOException; + } + + static final class PhysicalDeleteResult { + + private final byte[] stateRoot; + private final int participantNodePuts; + private final int participantNodeDeletes; + private final int superNodePuts; + private final int superNodeDeletes; + + private PhysicalDeleteResult(byte[] stateRoot, int participantNodePuts, + int participantNodeDeletes, int superNodePuts, int superNodeDeletes) { + this.stateRoot = Arrays.copyOf(stateRoot, stateRoot.length); + this.participantNodePuts = participantNodePuts; + this.participantNodeDeletes = participantNodeDeletes; + this.superNodePuts = superNodePuts; + this.superNodeDeletes = superNodeDeletes; + } + + byte[] getStateRoot() { + return Arrays.copyOf(stateRoot, stateRoot.length); + } + + int getParticipantNodePuts() { + return participantNodePuts; + } + + int getParticipantNodeDeletes() { + return participantNodeDeletes; + } + + int getSuperNodePuts() { + return superNodePuts; + } + + int getSuperNodeDeletes() { + return superNodeDeletes; + } + } + + @FunctionalInterface + interface PublicationFaultHook { + + void after(PublicationStage stage) throws IOException; + } + + @FunctionalInterface + interface DeleteFaultHook { + + void after(DeleteStage stage) throws IOException; + } + + private static byte[] prefixed(byte prefix, byte[] suffix, String name) { + byte[] supplied = Arrays.copyOf(Objects.requireNonNull(suffix, name), suffix.length); + byte[] key = new byte[supplied.length + 1]; + key[0] = prefix; + System.arraycopy(supplied, 0, key, 1, supplied.length); + return key; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java index 15529096dad..d7280f66117 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRebuildCoordinator.java @@ -676,6 +676,12 @@ public interface SnapshotSource { void scan(String dbName, EntryConsumer consumer) throws IOException; + /** Scans strictly after one physical cursor; unsupported comparators must fail closed. */ + default void scanAfter(String dbName, byte[] exclusivePhysicalCursor, EntryConsumer consumer) + throws IOException { + throw new IOException("path-state snapshot source does not support resumable scan: " + dbName); + } + void verifyIdentity(SnapshotIdentity expected) throws IOException; } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index bdc5334e85b..a8f8a68a3cc 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -373,12 +373,12 @@ private List prepare(Collection mutations) PathStateMutation present = Objects.requireNonNull(mutation, "mutation"); PathStateParticipant participant = scope.require(present.getDbName()); byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), - present.getCanonicalKey()); + present.getPhysicalKey()); if (!uniqueKeys.add(new MutationKey(participant.getStoreId(), secureKey))) { throw new IllegalArgumentException("duplicate path-state mutation key"); } byte[] encodedValue = present.isDelete() ? null - : PathStateCommitmentCodec.presentLeafValue(present.getCanonicalValue()); + : PathStateCommitmentCodec.presentLeafValue(present.getPhysicalValue()); prepared.add(new PreparedMutation(participant, secureKey, encodedValue)); } Collections.sort(prepared, MUTATION_COMPARATOR); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java index e8ff03659b4..9629a17f660 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java @@ -5,7 +5,7 @@ import java.util.Objects; /** In-process snapshot authority that advances only with the durable block-final CURRENT head. */ -public final class PathStateSnapshotHead { +public final class PathStateSnapshotHead implements PathStateHead { private final PathStateStoreManifest manifest; private final PathStateLayerLimits limits; @@ -100,6 +100,11 @@ public synchronized PreparedPathStateTransition prepare(PathStateBlockTransition return PreparedPathStateTransition.prepare(head, snapshot, admitted); } + @Override + public synchronized byte[] preview(PathStateBlockTransition transition) throws IOException { + return prepare(transition).getStateRoot(); + } + /** Publishes one exact prepared child and adopts it only after CURRENT confirms durability. */ public synchronized PathStateRootMetadata advancePrepared( PreparedPathStateTransition prepared) throws IOException { @@ -146,6 +151,11 @@ public synchronized boolean isFailed() { return failed; } + @Override + public void close() { + // The legacy head opens native stores only inside bounded operations. + } + private void requireChild(PathStateBlockTransition transition) throws IOException { if (transition.getBlockNumber() != head.getBlockNumber() + 1 || !Arrays.equals(transition.getParentHash(), head.getBlockHash())) { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 89afbc38d3f..150525d5752 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -131,15 +131,18 @@ import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.db2.stateroot.PathStateBlockTransition; +import org.tron.core.db2.stateroot.PathStateCanonicalizer; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateHead; import org.tron.core.db2.stateroot.PathStateLayerLimits; import org.tron.core.db2.stateroot.PathStateNativeSnapshotSource; -import org.tron.core.db2.stateroot.PathStateRebuildCoordinator; +import org.tron.core.db2.stateroot.PathStatePhysicalRuntimeAdmission; +import org.tron.core.db2.stateroot.PathStatePhysicalSnapshotHead; +import org.tron.core.db2.stateroot.PathStatePhysicalStoreSet; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; +import org.tron.core.db2.stateroot.PathStateRoot; import org.tron.core.db2.stateroot.PathStateRootMetadata; -import org.tron.core.db2.stateroot.PathStateRuntimeAdmission; import org.tron.core.db2.stateroot.PathStateRuntimeAttachment; -import org.tron.core.db2.stateroot.PathStateSnapshotHead; import org.tron.core.db2.stateroot.PathStateStoreManifest; import org.tron.core.exception.AccountResourceInsufficientException; import org.tron.core.exception.BadBlockException; @@ -220,7 +223,7 @@ public class Manager { @Getter private StateArchiveRuntimeOwner stateArchiveRuntime; @Getter - private PathStateSnapshotHead pathStateSnapshotHead; + private PathStateHead pathStateSnapshotHead; @Getter private PathStateRuntimeAttachment pathStateRuntime; private StateArchiveRuntimeOwner.ServingIndexFaultHook stateArchiveServingIndexFaultHook = @@ -735,7 +738,7 @@ private void initPathStateRoot() { org.tron.core.config.args.Storage storage = Args.getInstance().getStorage(); if (!storage.isPathStateRootEnabled()) { try { - PathStateRuntimeAdmission.inspect(false, null, null); + PathStatePhysicalRuntimeAdmission.inspect(false, null, null); } catch (java.io.IOException impossible) { throw new IllegalStateException("Disabled path-state admission failed", impossible); } @@ -743,29 +746,28 @@ private void initPathStateRoot() { } Path directory = Paths.get(Args.getInstance().getOutputDirectory(), storage.getPathStateRootDirectory()).normalize(); + PathStateHead recovered = null; try { PathStateStoreManifest.Engine engine = PathStateStoreManifest.Engine.valueOf( storage.getDbEngine()); - PathStateRuntimeAdmission.Result admission = PathStateRuntimeAdmission.inspect( - true, directory, engine); - if (admission.getStatus() == PathStateRuntimeAdmission.Status.REBUILD_REQUIRED) { + PathStatePhysicalRuntimeAdmission.Result admission = + PathStatePhysicalRuntimeAdmission.inspect(true, directory, engine); + if (admission.getStatus() + == PathStatePhysicalRuntimeAdmission.Status.REBUILD_REQUIRED) { if (!(revokingStore instanceof SnapshotManager)) { throw new IllegalStateException("Path-state rebuild requires SnapshotManager"); } - PathStateStoreManifest rebuildManifest = admission.getManifest() == null - ? PathStateStoreManifest.createOrOpen(directory, engine) : admission.getManifest(); - rebuildPathStateRoot((SnapshotManager) revokingStore, rebuildManifest); - admission = PathStateRuntimeAdmission.inspect(true, directory, engine); + rebuildPathStateRoot((SnapshotManager) revokingStore, directory, engine); + admission = PathStatePhysicalRuntimeAdmission.inspect(true, directory, engine); } - if (admission.getStatus() != PathStateRuntimeAdmission.Status.CURRENT_READY) { + if (admission.getStatus() + != PathStatePhysicalRuntimeAdmission.Status.CURRENT_CANDIDATE) { throw new IllegalStateException( "Path-state startup requires a completed admitted rebuild"); } - PathStateLayerLimits limits = new PathStateLayerLimits( - storage.getPathStateRootReversibleLayerLimit(), - storage.getPathStateRootReversibleLayerBytes()); - PathStateSnapshotHead recovered = PathStateSnapshotHead.open( - admission.getManifest(), limits); + recovered = PathStatePhysicalSnapshotHead.open(directory, engine, + new PathStateLayerLimits(storage.getPathStateRootReversibleLayerLimit(), + storage.getPathStateRootReversibleLayerBytes())); PathStateRootMetadata recoveredHead = recovered.getHead(); if (recoveredHead.getBlockNumber() != getDynamicPropertiesStore().getLatestBlockHeaderNumber() @@ -778,8 +780,16 @@ private void initPathStateRoot() { attachPathStateBlockFinalRuntime(); logger.info("Path-state current root attached: directory={}, head={}, engine={}", directory, recoveredHead.getBlockNumber(), storage.getDbEngine()); + recovered = null; } catch (java.io.IOException | RuntimeException failure) { pathStateSnapshotHead = null; + if (recovered != null) { + try { + recovered.close(); + } catch (java.io.IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } throw new IllegalStateException("Failed to recover path-state startup", failure); } } @@ -797,7 +807,7 @@ private void attachPathStateBlockFinalRuntime() throws java.io.IOException { new SnapshotPathStateTransitionCollector(accountAssetStore::prefixQuery, this::scanPathStateActivationAccounts), this::advancePathStateRoot, this::flushPathStateBaseThrough, - transition -> pathStateSnapshotHead.prepare(transition).getStateRoot()); + transition -> pathStateSnapshotHead.preview(transition)); attachment.synchronizeReadyHead(pathStateSnapshotHead.getHead()); ((SnapshotManager) revokingStore).attachPathStateRuntime(attachment); pathStateRuntime = attachment; @@ -825,7 +835,7 @@ private void scanPathStateActivationAccounts( private void advancePathStateRoot(PathStateBlockTransition transition) throws java.io.IOException { - PathStateSnapshotHead owner = pathStateSnapshotHead; + PathStateHead owner = pathStateSnapshotHead; if (owner == null) { throw new java.io.IOException("Path-state snapshot owner is unavailable"); } @@ -834,15 +844,15 @@ private void advancePathStateRoot(PathStateBlockTransition transition) private void flushPathStateBaseThrough(long blockNumber, byte[] blockHash) throws java.io.IOException { - PathStateSnapshotHead owner = pathStateSnapshotHead; + PathStateHead owner = pathStateSnapshotHead; if (owner == null) { throw new java.io.IOException("Path-state snapshot owner is unavailable"); } owner.flushBaseThrough(blockNumber, blockHash); } - private void rebuildPathStateRoot(SnapshotManager snapshotManager, - PathStateStoreManifest manifest) throws java.io.IOException { + private void rebuildPathStateRoot(SnapshotManager snapshotManager, Path directory, + PathStateStoreManifest.Engine engine) throws java.io.IOException { java.util.Map supplementalStores = java.util.Collections.emptyMap(); @@ -863,11 +873,18 @@ private void rebuildPathStateRoot(SnapshotManager snapshotManager, try (PathStateNativeSnapshotSource source = PathStateNativeSnapshotSource.acquire( snapshotManager, supplementalStores, this::readPathStateSnapshotIdentity, PATH_STATE_REBUILD_PAGE_SIZE, PATH_STATE_REBUILD_MARKET_ENTRY_LIMIT)) { - PathStateRebuildCoordinator.RebuildResult result = new PathStateRebuildCoordinator() - .rebuild(manifest, source); + SnapshotIdentity identity = source.identity(); + PathStateRootMetadata metadata; + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, + new PathStateCanonicalizer().participantScope(), engine)) { + PathStateRoot rebuilt = stores.ingestAndBuild(source); + metadata = PathStateRootMetadata.base(identity.getBlockNumber(), identity.getBlockHash(), + identity.getParentHash(), identity.getTimestamp(), identity.getPhase(), + stores.getFormatDigest(), rebuilt.rootHash(), source.sourceIdentityDigest()); + stores.publishCurrent(metadata); + } logger.info("Path-state initial root rebuilt: directory={}, head={}, entries={}", - manifest.getDirectory(), result.getMetadata().getBlockNumber(), - result.getTotalEntries()); + directory, metadata.getBlockNumber(), "exact-27"); } } @@ -1430,7 +1447,7 @@ public void eraseBlock() { /** Keeps the non-consensus path-state head aligned after Chainbase owns a successful pop. */ private void rewindPathStateRootAfterPop() { - PathStateSnapshotHead owner = pathStateSnapshotHead; + PathStateHead owner = pathStateSnapshotHead; if (owner == null) { return; } @@ -1509,7 +1526,7 @@ private void diagnosePathStateHeader(BlockCapsule block) { } byte[] localRoot = null; try { - PathStateSnapshotHead owner = pathStateSnapshotHead; + PathStateHead owner = pathStateSnapshotHead; if (owner != null) { PathStateRootMetadata local = owner.getHead(); if (local.getBlockNumber() == block.getNum() @@ -3144,7 +3161,15 @@ private void closePathStateRoot() { ((SnapshotManager) revokingStore).detachPathStateRuntime(runtime); pathStateRuntime = null; } + PathStateHead owner = pathStateSnapshotHead; pathStateSnapshotHead = null; + if (owner != null) { + try { + owner.close(); + } catch (java.io.IOException failure) { + throw new IllegalStateException("Failed to close path-state current head", failure); + } + } } private static class ValidateSignTask implements Callable { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index 9e0498abf6d..33a4de9b1cb 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -14,6 +14,7 @@ import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Rule; @@ -26,6 +27,8 @@ import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.config.args.Storage; import org.tron.core.db.Manager; +import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; import org.tron.core.db2.common.DB; @@ -61,14 +64,7 @@ public void disabledAndMissingStartupDoNotCreatePathStateDirectory() throws Exce public void readyCurrentAttachesExactCanonicalHeadAndCloses() throws Exception { Path output = temporaryFolder.newFolder("startup-ready").toPath(); Path root = output.resolve("path-state-root"); - PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen(root, Engine.ROCKSDB); - PathStateRootMetadata base; - try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { - PathStateRoot state = stores.createRoot(); - base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, - P66Phase.P66_ON, manifest.getIdentityDigest(), state.rootHash(), bytes(3)); - new PathStateBasePublication(manifest).publish(stores, base); - } + PathStateRootMetadata base = publishEmptyPhysicalCurrent(root, 100, 1, 2); DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); when(dynamic.getLatestBlockHeaderNumber()).thenReturn(100L); @@ -91,6 +87,12 @@ public void readyCurrentAttachesExactCanonicalHeadAndCloses() throws Exception { assertNull(manager.getPathStateSnapshotHead()); assertNull(manager.getPathStateRuntime()); + withConfig(output, true, () -> invoke(manager, "initPathStateRoot")); + assertArrayEquals(base.encode(), manager.getPathStateSnapshotHead().getHead().encode()); + assertEquals(PathStateRuntimeAttachment.State.READY, + manager.getPathStateRuntime().status().getState()); + invoke(manager, "closePathStateRoot"); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(101L); assertThrows(IllegalStateException.class, () -> withConfig(output, true, () -> invoke(manager, "initPathStateRoot"))); @@ -98,57 +100,131 @@ public void readyCurrentAttachesExactCanonicalHeadAndCloses() throws Exception { } @Test - public void shortReorgRewindsToChainbaseHeadAndRetiresOldSuffix() throws Exception { - Path output = temporaryFolder.newFolder("short-reorg").toPath(); + public void readyManagerPublishesOneBlockFinalAndRestartsAtTheNewHead() throws Exception { + Path output = temporaryFolder.newFolder("startup-one-block").toPath(); Path root = output.resolve("path-state-root"); - PathStateStoreManifest manifest = PathStateStoreManifest.createOrOpen(root, Engine.ROCKSDB); - PathStateRootMetadata base; - try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openBase(manifest)) { - PathStateRoot state = stores.createRoot(); - base = PathStateRootMetadata.base(100, bytes(1), bytes(2), 300, - P66Phase.P66_ON, manifest.getIdentityDigest(), state.rootHash(), bytes(3)); - new PathStateBasePublication(manifest).publish(stores, base); - } - PathStateSnapshotHead builder = PathStateSnapshotHead.open( - manifest, PathStateLayerLimits.defaults()); - PathStateRootMetadata first = builder.advance(transition(101, 11, base.getBlockHash())); - PathStateRootMetadata oldSecond = builder.advance(transition(102, 12, - first.getBlockHash())); + PathStateRootMetadata base = publishEmptyPhysicalCurrent(root, 100, 1, 2); DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); - when(dynamic.getLatestBlockHeaderNumber()).thenReturn(102L); - when(dynamic.getLatestBlockHeaderHash()).thenReturn( - Sha256Hash.wrap(oldSecond.getBlockHash())); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(100L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(base.getBlockHash())); ChainBaseManager chainBase = mock(ChainBaseManager.class); when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); when(chainBase.getAccountAssetStore()).thenReturn(mock(AccountAssetStore.class)); Manager manager = new Manager(); setChainBaseManager(manager, chainBase); - setField(manager, "revokingStore", new SnapshotManager("")); + SnapshotManager[] snapshotHolder = new SnapshotManager[1]; - withConfig(output, true, () -> invoke(manager, "initPathStateRoot")); - when(dynamic.getLatestBlockHeaderNumber()).thenReturn(101L); - when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(first.getBlockHash())); - invoke(manager, "rewindPathStateRootAfterPop"); - - assertArrayEquals(first.encode(), manager.getPathStateSnapshotHead().getHead().encode()); - assertArrayEquals(first.encode(), new PathStateCurrentStore(manifest).current().encode()); - assertFalse(Files.exists(manifest.getLayerDirectory( - oldSecond.getBlockNumber(), oldSecond.getBlockHash()))); - assertFalse(manager.getPathStateRuntime().isFailed()); + byte[] childHash = bytes(9); + withConfig(output, true, () -> { + SnapshotManager snapshots = new SnapshotManager(""); + snapshots.getDbs().add(propertiesStoreWithP66Enabled()); + snapshots.enable(); + snapshotHolder[0] = snapshots; + setField(manager, "revokingStore", snapshots); + invoke(manager, "initPathStateRoot"); + }); + SnapshotManager snapshots = snapshotHolder[0]; + try (ISession session = snapshots.buildSession()) { + byte[] preview = snapshots.previewPathStateRoot(BlockSnapshotMeta.forBlock( + 101, childHash, base.getBlockHash(), 303)); + assertNotNull(preview); + assertArrayEquals(base.getStateRoot(), preview); + session.commit(BlockSnapshotMeta.forBlock(101, childHash, base.getBlockHash(), 303)); + } + assertEquals(101, manager.getPathStateSnapshotHead().getHead().getBlockNumber()); assertEquals(PathStateRuntimeAttachment.State.READY, manager.getPathStateRuntime().status().getState()); - assertEquals(101L, manager.getPathStateRuntime().status().getReadyBlockNumber()); + invoke(manager, "closePathStateRoot"); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(101L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(childHash)); + withConfig(output, true, () -> invoke(manager, "initPathStateRoot")); + assertEquals(101, manager.getPathStateSnapshotHead().getHead().getBlockNumber()); + assertArrayEquals(childHash, manager.getPathStateSnapshotHead().getHead().getBlockHash()); + invoke(manager, "closePathStateRoot"); + } + + @Test + public void shortReorgBeyondPhysicalJournalFailsClosed() throws Exception { + Path output = temporaryFolder.newFolder("short-reorg").toPath(); + Path root = output.resolve("path-state-root"); + PathStateRootMetadata base = publishEmptyPhysicalCurrent(root, 100, 1, 2); + + DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); when(dynamic.getLatestBlockHeaderNumber()).thenReturn(100L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(base.getBlockHash())); + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); + when(chainBase.getAccountAssetStore()).thenReturn(mock(AccountAssetStore.class)); + Manager manager = new Manager(); + setChainBaseManager(manager, chainBase); + setField(manager, "revokingStore", new SnapshotManager("")); + + withConfig(output, true, () -> invoke(manager, "initPathStateRoot")); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(99L); when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(bytes(99))); invoke(manager, "rewindPathStateRootAfterPop"); assertNotNull(manager.getPathStateRuntime().getFailure()); assertEquals(PathStateRuntimeAttachment.FailureStage.REORG, manager.getPathStateRuntime().status().getFailureStage()); - assertEquals(100L, manager.getPathStateRuntime().status().getObservedBlockNumber()); + assertEquals(99L, manager.getPathStateRuntime().status().getObservedBlockNumber()); assertEquals(1L, manager.getPathStateRuntime().status().getRootLag()); - assertArrayEquals(first.encode(), new PathStateCurrentStore(manifest).current().encode()); + assertArrayEquals(base.encode(), manager.getPathStateSnapshotHead().getHead().encode()); + invoke(manager, "closePathStateRoot"); + try (PathStatePhysicalSnapshotHead reopened = PathStatePhysicalSnapshotHead.open( + root, Engine.ROCKSDB)) { + assertArrayEquals(base.encode(), reopened.getHead().encode()); + } + } + + @Test + public void managerPopsPhysicalChildAdvancesSiblingAndRestarts() throws Exception { + Path output = temporaryFolder.newFolder("physical-manager-short-reorg").toPath(); + Path root = output.resolve("path-state-root"); + PathStateRootMetadata base = publishEmptyPhysicalCurrent(root, 100, 1, 2); + + DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(100L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(base.getBlockHash())); + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); + when(chainBase.getAccountAssetStore()).thenReturn(mock(AccountAssetStore.class)); + SnapshotManager snapshots = new SnapshotManager(""); + Manager manager = new Manager(); + setChainBaseManager(manager, chainBase); + + withConfig(output, true, () -> { + snapshots.getDbs().add(propertiesStoreWithP66Enabled()); + snapshots.enable(); + setField(manager, "revokingStore", snapshots); + invoke(manager, "initPathStateRoot"); + }); + byte[] oldChildHash = bytes(21); + try (ISession session = snapshots.buildSession()) { + session.commit(BlockSnapshotMeta.forBlock(101, oldChildHash, base.getBlockHash(), 303)); + } + assertEquals(101, manager.getPathStateSnapshotHead().getHead().getBlockNumber()); + + snapshots.fastPop(); + invoke(manager, "rewindPathStateRootAfterPop"); + assertEquals(PathStateRuntimeAttachment.State.READY, + manager.getPathStateRuntime().status().getState()); + assertArrayEquals(base.encode(), manager.getPathStateSnapshotHead().getHead().encode()); + + byte[] siblingHash = bytes(22); + try (ISession session = snapshots.buildSession()) { + session.commit(BlockSnapshotMeta.forBlock(101, siblingHash, base.getBlockHash(), 306)); + } + assertArrayEquals(siblingHash, manager.getPathStateSnapshotHead().getHead().getBlockHash()); + invoke(manager, "closePathStateRoot"); + + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(101L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(siblingHash)); + withConfig(output, true, () -> invoke(manager, "initPathStateRoot")); + assertArrayEquals(siblingHash, manager.getPathStateSnapshotHead().getHead().getBlockHash()); + assertEquals(PathStateRuntimeAttachment.State.READY, + manager.getPathStateRuntime().status().getState()); invoke(manager, "closePathStateRoot"); } @@ -177,6 +253,7 @@ public void missingCurrentRebuildsExactNativeSnapshotAndAttaches() throws Except AtomicInteger closed = new AtomicInteger(); Manager manager = new Manager(); setChainBaseManager(manager, chainBase); + SnapshotManager[] snapshotHolder = new SnapshotManager[1]; withConfig(output, true, () -> { SnapshotManager snapshots = new SnapshotManager(""); @@ -185,6 +262,8 @@ public void missingCurrentRebuildsExactNativeSnapshotAndAttaches() throws Except snapshots.getDbs().add(emptyNativeStore(participant.getDbName(), blockNumber, blockId.getBytes(), closed)); } + snapshots.enable(); + snapshotHolder[0] = snapshots; setField(manager, "revokingStore", snapshots); invoke(manager, "initPathStateRoot"); }); @@ -192,9 +271,40 @@ public void missingCurrentRebuildsExactNativeSnapshotAndAttaches() throws Except PathStateRootMetadata head = manager.getPathStateSnapshotHead().getHead(); assertArrayEquals(blockId.getBytes(), head.getBlockHash()); assertArrayEquals(parentHash.getBytes(), head.getParentHash()); - assertNotNull(PathStateStoreManifest.validateExisting( + assertNotNull(PathStatePhysicalStoreManifest.validateExisting( output.resolve("path-state-root"), Engine.ROCKSDB)); + assertFalse(Files.exists(output.resolve("path-state-root/base/nodes"))); + assertFalse(Files.exists(output.resolve("path-state-root/rebuild-spool"))); assertEquals(PathStateParticipantDescriptor.current().getStores().size(), closed.get()); + + byte[] childHash = bytes(4); + try (ISession session = snapshotHolder[0].buildSession()) { + BlockSnapshotMeta child = BlockSnapshotMeta.forBlock(101, childHash, + blockId.getBytes(), 303); + assertNotNull(snapshotHolder[0].previewPathStateRoot(child)); + session.commit(child); + } + assertEquals(101, manager.getPathStateSnapshotHead().getHead().getBlockNumber()); + invoke(manager, "closePathStateRoot"); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(101L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(Sha256Hash.wrap(childHash)); + withConfig(output, true, () -> invoke(manager, "initPathStateRoot")); + assertEquals(101, manager.getPathStateSnapshotHead().getHead().getBlockNumber()); + assertFalse(Files.exists(output.resolve("path-state-root/base/nodes"))); + assertFalse(Files.exists(output.resolve("path-state-root/rebuild-spool"))); + invoke(manager, "closePathStateRoot"); + } + + @SuppressWarnings("unchecked") + private static Chainbase propertiesStoreWithP66Enabled() { + DB database = mock(DB.class); + when(database.getDbName()).thenReturn("properties"); + when(database.iterator()).thenReturn(Collections.emptyIterator()); + when(database.get(org.mockito.ArgumentMatchers.any(byte[].class))) + .thenAnswer(invocation -> Arrays.equals((byte[]) invocation.getArgument(0), + "ALLOW_ASSET_OPTIMIZATION".getBytes(java.nio.charset.StandardCharsets.UTF_8)) + ? java.nio.ByteBuffer.allocate(Long.BYTES).putLong(1L).array() : null); + return new Chainbase(new SnapshotRoot(database)); } @SuppressWarnings("unchecked") @@ -204,6 +314,13 @@ private static Chainbase emptyNativeStore(String dbName, long blockNumber, byte[ withSettings().extraInterfaces(SnapshotCapableStore.class)); SnapshotCapableStore capable = (SnapshotCapableStore) database; when(database.getDbName()).thenReturn(dbName); + when(database.iterator()).thenReturn(Collections.emptyIterator()); + if ("properties".equals(dbName)) { + when(database.get(org.mockito.ArgumentMatchers.any(byte[].class))) + .thenAnswer(invocation -> Arrays.equals((byte[]) invocation.getArgument(0), + "ALLOW_ASSET_OPTIMIZATION".getBytes(java.nio.charset.StandardCharsets.UTF_8)) + ? java.nio.ByteBuffer.allocate(Long.BYTES).putLong(1L).array() : null); + } when(capable.getDbName()).thenReturn(dbName); when(capable.getSourceIdentity()).thenReturn("source-" + dbName); StoreSnapshot snapshot = mock(StoreSnapshot.class); @@ -277,6 +394,19 @@ private static byte[] bytes(int seed) { return value; } + private static PathStateRootMetadata publishEmptyPhysicalCurrent(Path root, + long blockNumber, int blockSeed, int parentSeed) throws Exception { + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, + new PathStateCanonicalizer().participantScope(), Engine.ROCKSDB)) { + PathStateRoot state = stores.buildRootFromFlat(); + PathStateRootMetadata metadata = PathStateRootMetadata.base(blockNumber, bytes(blockSeed), + bytes(parentSeed), 300, P66Phase.P66_ON, stores.getFormatDigest(), state.rootHash(), + bytes(3)); + stores.publishCurrent(metadata); + return metadata; + } + } + private static PathStateBlockTransition transition(long blockNumber, int seed, byte[] parentHash) { return new PathStateBlockTransition(blockNumber, bytes(seed), parentHash, diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index e829958735c..1cafc57db1f 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -2,15 +2,18 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -20,6 +23,9 @@ import org.junit.rules.TemporaryFolder; import org.tron.common.arch.Arch; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.EntryConsumer; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; +import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotSource; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; public class PathStateNativeNodeStoreTest { @@ -233,6 +239,722 @@ public void immutableLayerMetadataSealsTheWritableNodeSet() throws Exception { () -> PathStateNodeStoreSet.openLayer(manifest, layer)); } + @Test + public void physicalStoreSetCreatesExact27PlusSuperWithDisjointFNMKeyspaces() throws Exception { + Path root = temporaryFolder.newFolder("physical-27-plus-super").toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(4, new byte[]{1, 2, 3}); + byte[] sameSuffix = new byte[]{7, 8, 9}; + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + PathStatePhysicalStoreSet.PhysicalStore account = stores.participant("account"); + account.putFlat(secureKey, sameSuffix); + account.nodeStore().put(secureKey, new byte[]{4, 5, 6}); + account.putMetadata(secureKey, new byte[]{1}); + assertArrayEquals(sameSuffix, account.getFlat(secureKey)); + assertArrayEquals(new byte[]{4, 5, 6}, account.nodeStore().get(secureKey)); + assertArrayEquals(new byte[]{1}, account.getMetadata(secureKey)); + + stores.superStore().putFlat(secureKey, new byte[]{2}); + assertArrayEquals(new byte[]{2}, stores.superStore().getFlat(secureKey)); + assertNull(stores.superStore().nodeStore().get(secureKey)); + assertEquals(27, childDirectoryCount(root.resolve("stores"))); + AtomicInteger flatEntries = new AtomicInteger(); + account.scanFlat(ignored -> flatEntries.incrementAndGet()); + assertEquals(1, flatEntries.get()); + assertEquals(32, stores.getFormatDigest().length); + + PathStateRoot stateRoot = stores.createRoot(); + stateRoot.put("account", new byte[]{1, 2, 3}, new byte[]{4, 5, 6}); + assertEquals(32, stateRoot.rootHash().length); + assertNotNull(account.nodeStore().get(new byte[0])); + assertNotNull(stores.superStore().nodeStore().get(new byte[0])); + assertThrows(IllegalStateException.class, stores::createRoot); + } + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertArrayEquals(sameSuffix, reopened.participant("account").getFlat(secureKey)); + assertArrayEquals(new byte[]{4, 5, 6}, reopened.participant("account").nodeStore() + .get(secureKey)); + AtomicInteger flatEntries = new AtomicInteger(); + reopened.participant("account").scanFlat(ignored -> flatEntries.incrementAndGet()); + assertEquals(1, flatEntries.get()); + } + } + + @Test + public void physicalStoreSetRejectsLegacySharedBaseNodes() throws Exception { + Path root = temporaryFolder.newFolder("physical-legacy-rejection").toPath(); + Files.createDirectories(root.resolve("base").resolve("nodes")); + assertThrows(java.io.IOException.class, + () -> PathStatePhysicalStoreSet.open(root, new PathStateCanonicalizer().participantScope(), + Engine.ROCKSDB)); + } + + @Test + public void physicalStoreSetRejectsOldSharedManifestEvenWithoutNodeFiles() throws Exception { + Path root = temporaryFolder.newFolder("physical-old-manifest-rejection").toPath(); + PathStateStoreManifest.createOrOpen(root, Engine.ROCKSDB); + assertThrows(java.io.IOException.class, + () -> PathStatePhysicalStoreSet.open(root, new PathStateCanonicalizer().participantScope(), + Engine.ROCKSDB)); + } + + @Test + public void physicalStoreSetRejectsAnExactNameScopeWithAChangedStableStoreId() + throws Exception { + Path root = temporaryFolder.newFolder("physical-store-id-rejection").toPath(); + List changed = new ArrayList<>(); + for (PathStateParticipant participant + : new PathStateCanonicalizer().participantScope().getParticipants()) { + changed.add("proposal".equals(participant.getDbName()) + ? new PathStateParticipant(99, participant.getDbName(), + participant.getStoreFormatVersion()) : participant); + } + PathStateParticipantScope changedScope = new PathStateParticipantScope(changed); + assertThrows(IllegalArgumentException.class, + () -> PathStatePhysicalStoreSet.open(root, changedScope, Engine.ROCKSDB)); + } + + @Test + public void physicalFlatSnapshotRestoresAndVerifiesTheRoot() throws Exception { + Path root = temporaryFolder.newFolder("physical-flat-restore").toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + byte[] expectedRoot; + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + PathStateRoot stateRoot = stores.createRoot(); + stateRoot.put("account", new byte[]{1, 2}, new byte[]{3, 4}); + stateRoot.put("proposal", new byte[]{5, 6}, new byte[]{7, 8}); + expectedRoot = stateRoot.rootHash(); + stores.persistFlatSnapshot(stateRoot); + } + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertArrayEquals(expectedRoot, reopened.restoreRootFromFlat().rootHash()); + } + } + + @Test + public void physicalFlatRestoreFailsClosedForMissingOrCorruptLeaf() throws Exception { + Path root = temporaryFolder.newFolder("physical-flat-corruption").toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(4, new byte[]{1, 2}); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + PathStateRoot stateRoot = stores.createRoot(); + stateRoot.put("account", new byte[]{1, 2}, new byte[]{3, 4}); + stateRoot.rootHash(); + stores.persistFlatSnapshot(stateRoot); + } + try (PathStatePhysicalStoreSet corrupted = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + corrupted.participant("account").putFlat(secureKey, new byte[]{1}); + assertThrows(IllegalStateException.class, corrupted::restoreRootFromFlat); + } + try (PathStatePhysicalStoreSet missing = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + missing.participant("account").deleteFlat(secureKey); + assertThrows(IllegalStateException.class, missing::restoreRootFromFlat); + } + } + + @Test + public void physicalFlatBuildStreamsIntoNAndReusesPerStoreCompletionOnRetry() throws Exception { + Path root = temporaryFolder.newFolder("physical-flat-stream-build").toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + byte[] expectedRoot; + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + PathStateRoot stateRoot = stores.createRoot(); + stateRoot.put("account", new byte[]{1, 2}, new byte[]{3, 4}); + stateRoot.put("proposal", new byte[]{5, 6}, new byte[]{7, 8}); + expectedRoot = stateRoot.rootHash(); + stores.persistFlatSnapshot(stateRoot); + } + try (PathStatePhysicalStoreSet rebuilt = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertArrayEquals(expectedRoot, rebuilt.buildRootFromFlat().rootHash()); + assertNotNull(rebuilt.participant("account").getMetadata( + "flat-complete".getBytes(java.nio.charset.StandardCharsets.US_ASCII))); + assertNotNull(rebuilt.participant("proposal").getMetadata( + "flat-complete".getBytes(java.nio.charset.StandardCharsets.US_ASCII))); + } + try (PathStatePhysicalStoreSet retried = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertArrayEquals(expectedRoot, retried.buildRootFromFlat().rootHash()); + } + } + + @Test + public void physicalFlatBuildClearsIncompleteNodesAndRetriesWithoutSource() throws Exception { + Path root = temporaryFolder.newFolder("physical-flat-incomplete-retry").toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + byte[] stalePath = new byte[]{15, 15, 15, 15}; + byte[] expectedRoot; + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + PathStateRoot stateRoot = stores.createRoot(); + stateRoot.put("account", new byte[]{1, 2}, new byte[]{3, 4}); + stateRoot.put("proposal", new byte[]{5, 6}, new byte[]{7, 8}); + expectedRoot = stateRoot.rootHash(); + stores.persistFlatSnapshot(stateRoot); + } + + AtomicInteger injectedFailures = new AtomicInteger(); + try (PathStatePhysicalStoreSet interrupted = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, () -> interrupted.buildRootFromFlat( + (participant, storeRoot) -> { + if ("proposal".equals(participant.getDbName()) + && injectedFailures.getAndIncrement() == 0) { + throw new java.io.IOException("injected failure before FLAT_COMPLETE"); + } + })); + interrupted.participant("proposal").nodeStore().put(stalePath, new byte[]{99}); + } + assertEquals(1, injectedFailures.get()); + + try (PathStatePhysicalStoreSet retried = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertNotNull(retried.participant("proposal").nodeStore().get(stalePath)); + assertArrayEquals(expectedRoot, retried.buildRootFromFlat().rootHash()); + assertNull(retried.participant("proposal").nodeStore().get(stalePath)); + assertNotNull(retried.participant("proposal").getMetadata( + "flat-complete".getBytes(java.nio.charset.StandardCharsets.US_ASCII))); + } + + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertArrayEquals(expectedRoot, reopened.buildRootFromFlat().rootHash()); + } + } + + @Test + public void physicalGlobalPublicationRecoversEveryIntentCurrentCrashWindow() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + for (PathStatePhysicalStoreSet.PublicationStage stage + : PathStatePhysicalStoreSet.PublicationStage.values()) { + Path root = new File(temporaryFolder.getRoot(), "physical-publish-" + stage).toPath(); + byte[] expectedRoot = preparePhysicalTarget(root, scope); + + try (PathStatePhysicalStoreSet interrupted = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, () -> interrupted.publishCurrent(present -> { + if (present == stage) { + throw new java.io.IOException("injected publication failure at " + stage); + } + })); + } + + try (PathStatePhysicalStoreSet recovered = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + PathStatePhysicalStoreSet.PublicationRecovery action = recovered.recoverPublication(); + assertEquals(stage == PathStatePhysicalStoreSet.PublicationStage.AFTER_RETIRE + ? PathStatePhysicalStoreSet.PublicationRecovery.NONE + : PathStatePhysicalStoreSet.PublicationRecovery.COMPLETED_INTENT, + action); + assertEquals(PathStatePhysicalStoreSet.PublicationRecovery.NONE, + recovered.recoverPublication()); + } + assertFalse(Files.exists(root.resolve(PathStatePhysicalStoreSet.INTENT_FILE))); + assertTrue(Files.isRegularFile(root.resolve(PathStatePhysicalStoreSet.CURRENT_FILE))); + PathStatePhysicalGlobalIntent current = PathStatePhysicalGlobalIntent.decode( + Files.readAllBytes(root.resolve(PathStatePhysicalStoreSet.CURRENT_FILE))); + assertArrayEquals(expectedRoot, current.getSuperRoot()); + assertEquals(27, current.getParticipants().size()); + } + } + + @Test + public void physicalBlockFinalTransitionPreviewsPublishesAndRestarts() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-block-final").toPath(); + preparePublishedPhysicalTarget(root, scope); + byte[] key = new byte[]{1, 2, 3}; + byte[] blockHash = bytes(31); + + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + PathStateRootMetadata parent = stores.currentMetadata(); + PathStateBlockTransition transition = new PathStateBlockTransition(1, blockHash, + parent.getBlockHash(), 3, P66Phase.P66_ON, + Collections.singletonList(PathStateMutation.put("code", key, new byte[]{4, 5}))); + PathStateRootMetadata preview = stores.previewTransition(transition); + assertEquals(0, stores.currentMetadata().getBlockNumber()); + PathStateRootMetadata committed = stores.applyAndPublish(transition); + assertArrayEquals(preview.encode(), committed.encode()); + assertEquals(1, committed.getBlockNumber()); + assertArrayEquals(PathStateCommitmentCodec.presentLeafValue(new byte[]{4, 5}), + stores.participant("code").getFlat( + PathStateCommitmentCodec.storeLeafKey(scope.require("code").getStoreId(), key))); + } + + try (PathStatePhysicalSnapshotHead head = PathStatePhysicalSnapshotHead.open(root, + Engine.ROCKSDB)) { + assertEquals(1, head.getHead().getBlockNumber()); + PathStateBlockTransition update = new PathStateBlockTransition(2, bytes(32), blockHash, + 6, P66Phase.P66_ON, Arrays.asList( + PathStateMutation.put("code", key, new byte[]{6}), + PathStateMutation.put("proposal", new byte[]{7}, new byte[]{8}))); + assertArrayEquals(head.preview(update), head.advance(update).getStateRoot()); + PathStateBlockTransition delete = new PathStateBlockTransition(3, bytes(33), bytes(32), + 9, P66Phase.P66_ON, + Collections.singletonList(PathStateMutation.delete("code", key))); + head.advance(delete); + assertEquals(3, head.getHead().getBlockNumber()); + } + + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.openExisting(root, scope, + Engine.ROCKSDB)) { + assertEquals(PathStatePhysicalStoreSet.PublicationRecovery.NONE, + reopened.recoverPublication()); + assertEquals(3, reopened.currentMetadata().getBlockNumber()); + assertNull(reopened.participant("code").getFlat( + PathStateCommitmentCodec.storeLeafKey(scope.require("code").getStoreId(), key))); + } + } + + @Test + public void physicalBlockFinalCrashAfterSuperCompletesIntentOnRestart() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-block-final-super-crash").toPath(); + preparePublishedPhysicalTarget(root, scope); + PathStateBlockTransition transition = new PathStateBlockTransition(1, bytes(41), + new byte[32], 3, P66Phase.P66_ON, + Collections.singletonList(PathStateMutation.put("code", new byte[]{1}, new byte[]{2}))); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, () -> stores.applyAndPublish(transition, stage -> { + if (stage == PathStatePhysicalStoreSet.TransitionStage.AFTER_SUPER_BATCH) { + throw new java.io.IOException("injected failure after super batch"); + } + })); + } + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.openExisting(root, scope, + Engine.ROCKSDB)) { + assertEquals(PathStatePhysicalStoreSet.PublicationRecovery.COMPLETED_INTENT, + reopened.recoverPublication()); + assertEquals(1, reopened.currentMetadata().getBlockNumber()); + } + } + + @Test + public void physicalBlockFinalCrashAfterParticipantBatchFailsClosed() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-block-final-participant-crash").toPath(); + preparePublishedPhysicalTarget(root, scope); + PathStateBlockTransition transition = new PathStateBlockTransition(1, bytes(51), + new byte[32], 3, P66Phase.P66_ON, + Collections.singletonList(PathStateMutation.put("code", new byte[]{1}, new byte[]{2}))); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, () -> stores.applyAndPublish(transition, stage -> { + if (stage == PathStatePhysicalStoreSet.TransitionStage.AFTER_PARTICIPANT_BATCH) { + throw new java.io.IOException("injected failure after participant batch"); + } + })); + } + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.openExisting(root, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, reopened::recoverPublication); + } + assertTrue(Files.isRegularFile(root.resolve(PathStatePhysicalStoreSet.INTENT_FILE))); + assertTrue(Files.isRegularFile(root.resolve(PathStatePhysicalStoreSet.CURRENT_FILE))); + } + + @Test + public void physicalShortReorgRestoresAncestorAndAdvancesSiblingAcrossRestart() + throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-short-reorg").toPath(); + byte[] baseRoot = preparePublishedPhysicalTarget(root, scope); + byte[] key = new byte[]{1, 2}; + PathStateLayerLimits limits = new PathStateLayerLimits(4, 1L << 20); + + try (PathStatePhysicalSnapshotHead head = PathStatePhysicalSnapshotHead.open(root, + Engine.ROCKSDB, limits)) { + head.advance(new PathStateBlockTransition(1, bytes(61), new byte[32], 3, + P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", key, new byte[]{3})))); + head.advance(new PathStateBlockTransition(2, bytes(62), bytes(61), 6, + P66Phase.P66_ON, Arrays.asList( + PathStateMutation.put("code", key, new byte[]{4}), + PathStateMutation.put("proposal", new byte[]{5}, new byte[]{6})))); + PathStateRootMetadata rewound = head.rewindTo(0, new byte[32]); + assertEquals(0, rewound.getBlockNumber()); + assertArrayEquals(baseRoot, rewound.getStateRoot()); + PathStateRootMetadata sibling = head.advance(new PathStateBlockTransition(1, bytes(63), + new byte[32], 9, P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", key, new byte[]{9})))); + assertEquals(1, sibling.getBlockNumber()); + assertArrayEquals(bytes(63), sibling.getBlockHash()); + } + + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.openExisting(root, scope, + Engine.ROCKSDB)) { + assertEquals(1, reopened.currentMetadata().getBlockNumber()); + assertArrayEquals(PathStateCommitmentCodec.presentLeafValue(new byte[]{9}), + reopened.participant("code").getFlat( + PathStateCommitmentCodec.storeLeafKey(scope.require("code").getStoreId(), key))); + assertNull(reopened.participant("proposal").getFlat( + PathStateCommitmentCodec.storeLeafKey(scope.require("proposal").getStoreId(), + new byte[]{5}))); + } + } + + @Test + public void physicalShortReorgIsBoundedAndFailsBeforeAuthorityMoves() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-short-reorg-bounded").toPath(); + preparePublishedPhysicalTarget(root, scope); + PathStateLayerLimits limits = new PathStateLayerLimits(2, 1L << 20); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + stores.applyAndPublish(new PathStateBlockTransition(1, bytes(71), new byte[32], 3, + P66Phase.P66_ON, Collections.emptyList()), limits, stage -> { }); + stores.applyAndPublish(new PathStateBlockTransition(2, bytes(72), bytes(71), 6, + P66Phase.P66_ON, Collections.emptyList()), limits, stage -> { }); + stores.applyAndPublish(new PathStateBlockTransition(3, bytes(73), bytes(72), 9, + P66Phase.P66_ON, Collections.emptyList()), limits, stage -> { }); + byte[] current = stores.currentMetadata().encode(); + assertThrows(java.io.IOException.class, + () -> stores.rewindTo(0, new byte[32], limits)); + assertArrayEquals(current, stores.currentMetadata().encode()); + assertEquals(1, stores.rewindTo(1, bytes(71), limits).getBlockNumber()); + } + } + + @Test + public void physicalShortReorgCrashWindowsCompleteOrFailClosed() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path completed = temporaryFolder.newFolder("physical-rewind-super-crash").toPath(); + preparePublishedPhysicalTarget(completed, scope); + PathStateLayerLimits limits = new PathStateLayerLimits(4, 1L << 20); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(completed, scope, + Engine.ROCKSDB)) { + stores.applyAndPublish(new PathStateBlockTransition(1, bytes(81), new byte[32], 3, + P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{2})))); + assertThrows(java.io.IOException.class, () -> stores.rewindTo(0, new byte[32], limits, + stage -> { + if (stage == PathStatePhysicalStoreSet.RewindStage.AFTER_SUPER_BATCH) { + throw new java.io.IOException("injected rewind failure after super"); + } + })); + } + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.openExisting(completed, + scope, Engine.ROCKSDB)) { + assertEquals(PathStatePhysicalStoreSet.PublicationRecovery.COMPLETED_INTENT, + stores.recoverPublication()); + assertEquals(0, stores.currentMetadata().getBlockNumber()); + } + + Path partial = temporaryFolder.newFolder("physical-rewind-participant-crash").toPath(); + preparePublishedPhysicalTarget(partial, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(partial, scope, + Engine.ROCKSDB)) { + stores.applyAndPublish(new PathStateBlockTransition(1, bytes(82), new byte[32], 3, + P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{2})))); + assertThrows(java.io.IOException.class, () -> stores.rewindTo(0, new byte[32], limits, + stage -> { + if (stage == PathStatePhysicalStoreSet.RewindStage.AFTER_PARTICIPANT_BATCH) { + throw new java.io.IOException("injected rewind failure after participant"); + } + })); + } + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.openExisting(partial, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, stores::recoverPublication); + } + } + + @Test + public void physicalStartupRejectsCorruptReverseJournal() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-reverse-corrupt").toPath(); + preparePublishedPhysicalTarget(root, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + stores.applyAndPublish(new PathStateBlockTransition(1, bytes(91), new byte[32], 3, + P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{2})))); + } + Path reverse; + try (Stream files = Files.list(root.resolve("reverse"))) { + reverse = files.findFirst().get(); + } + byte[] corrupt = Files.readAllBytes(reverse); + corrupt[corrupt.length - 1] ^= 1; + Files.write(reverse, corrupt); + assertThrows(java.io.IOException.class, + () -> PathStatePhysicalSnapshotHead.open(root, Engine.ROCKSDB)); + } + + @Test + public void physicalGlobalPublicationAcceptsOnlyOldCurrentOrExactIntentTarget() + throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path uncommitted = temporaryFolder.newFolder("physical-publish-before-intent").toPath(); + preparePhysicalTarget(uncommitted, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(uncommitted, scope, + Engine.ROCKSDB)) { + assertEquals(PathStatePhysicalStoreSet.PublicationRecovery.NONE, + stores.recoverPublication()); + } + assertFalse(Files.exists(uncommitted.resolve(PathStatePhysicalStoreSet.CURRENT_FILE))); + + Path tampered = temporaryFolder.newFolder("physical-publish-tampered-target").toPath(); + preparePhysicalTarget(tampered, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(tampered, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, () -> stores.publishCurrent(stage -> { + if (stage == PathStatePhysicalStoreSet.PublicationStage.AFTER_INTENT) { + throw new java.io.IOException("injected failure after INTENT"); + } + })); + } + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(tampered, scope, + Engine.ROCKSDB)) { + stores.participant("proposal").putMetadata( + "store-generation".getBytes(java.nio.charset.StandardCharsets.US_ASCII), bytes(77)); + assertThrows(java.io.IOException.class, stores::recoverPublication); + } + assertFalse(Files.exists(tampered.resolve(PathStatePhysicalStoreSet.CURRENT_FILE))); + assertTrue(Files.isRegularFile(tampered.resolve(PathStatePhysicalStoreSet.INTENT_FILE))); + + Path oldCurrent = temporaryFolder.newFolder("physical-publish-old-current").toPath(); + byte[] expectedRoot = preparePhysicalTarget(oldCurrent, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(oldCurrent, scope, + Engine.ROCKSDB)) { + assertArrayEquals(expectedRoot, stores.publishCurrent()); + } + PathStateMetadataFile.publishImmutableBytes( + oldCurrent.resolve(PathStatePhysicalStoreSet.INTENT_FILE), new byte[]{1}); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(oldCurrent, scope, + Engine.ROCKSDB)) { + assertEquals(PathStatePhysicalStoreSet.PublicationRecovery.RETAINED_CURRENT, + stores.recoverPublication()); + } + assertFalse(Files.exists(oldCurrent.resolve(PathStatePhysicalStoreSet.INTENT_FILE))); + assertArrayEquals(expectedRoot, PathStatePhysicalGlobalIntent.decode(Files.readAllBytes( + oldCurrent.resolve(PathStatePhysicalStoreSet.CURRENT_FILE))).getSuperRoot()); + } + + @Test + public void physicalGlobalRecordRejectsTruncationChecksumAndCorruptCurrent() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-global-record-corruption").toPath(); + preparePublishedPhysicalTarget(root, scope); + Path currentPath = root.resolve(PathStatePhysicalStoreSet.CURRENT_FILE); + byte[] encoded = Files.readAllBytes(currentPath); + + assertThrows(IllegalArgumentException.class, + () -> PathStatePhysicalGlobalIntent.decode(Arrays.copyOf(encoded, encoded.length - 1))); + byte[] corruptBody = Arrays.copyOf(encoded, encoded.length); + corruptBody[20] ^= 1; + assertThrows(IllegalArgumentException.class, + () -> PathStatePhysicalGlobalIntent.decode(corruptBody)); + byte[] corruptChecksum = Arrays.copyOf(encoded, encoded.length); + corruptChecksum[corruptChecksum.length - 1] ^= 1; + assertThrows(IllegalArgumentException.class, + () -> PathStatePhysicalGlobalIntent.decode(corruptChecksum)); + + Files.write(currentPath, corruptChecksum); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, stores::recoverPublication); + } + } + + @Test + public void physicalPublicationRejectsMissingCompletionMetadataAndRootNodes() + throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + + Path missingRoot = temporaryFolder.newFolder("physical-missing-store-root").toPath(); + preparePublishedPhysicalTarget(missingRoot, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(missingRoot, scope, + Engine.ROCKSDB)) { + stores.participant("proposal").deleteMetadata(metadata("flat-complete")); + } + assertPublicationRejected(missingRoot, scope); + + Path missingDigest = temporaryFolder.newFolder("physical-missing-flat-digest").toPath(); + preparePublishedPhysicalTarget(missingDigest, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(missingDigest, scope, + Engine.ROCKSDB)) { + stores.participant("proposal").deleteMetadata(metadata("flat-digest")); + } + assertPublicationRejected(missingDigest, scope); + + Path corruptGeneration = temporaryFolder.newFolder( + "physical-corrupt-store-generation").toPath(); + preparePublishedPhysicalTarget(corruptGeneration, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(corruptGeneration, + scope, Engine.ROCKSDB)) { + stores.participant("proposal").putMetadata(metadata("store-generation"), bytes(88)); + } + assertPublicationRejected(corruptGeneration, scope); + + Path missingSuperGeneration = temporaryFolder.newFolder( + "physical-missing-super-generation").toPath(); + preparePublishedPhysicalTarget(missingSuperGeneration, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open( + missingSuperGeneration, scope, Engine.ROCKSDB)) { + stores.superStore().deleteMetadata(metadata("super-generation")); + } + assertPublicationRejected(missingSuperGeneration, scope); + + Path missingNode = temporaryFolder.newFolder("physical-missing-root-node").toPath(); + preparePublishedPhysicalTarget(missingNode, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(missingNode, scope, + Engine.ROCKSDB)) { + stores.participant("proposal").nodeStore().delete(new byte[0]); + } + assertPublicationRejected(missingNode, scope); + } + + @Test + public void physicalDeleteRecomputesSecureKeyCommitsChangedPathsAndPublishesCurrent() + throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-delete-publish").toPath(); + byte[] originalRoot = preparePublishedPhysicalTarget(root, scope); + byte[] expectedRoot = rootWithOnlyAccount(scope); + byte[] proposalKey = new byte[]{5, 6}; + byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(21, proposalKey); + + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + PathStatePhysicalStoreSet.PhysicalDeleteResult result = stores.deleteAndPublish( + "proposal", proposalKey, stage -> { }); + assertFalse(Arrays.equals(originalRoot, result.getStateRoot())); + assertArrayEquals(expectedRoot, result.getStateRoot()); + assertTrue(result.getParticipantNodeDeletes() > 0); + assertTrue(result.getSuperNodePuts() > 0); + assertNull(stores.participant("proposal").getFlat(secureKey)); + } + + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + assertEquals(PathStatePhysicalStoreSet.PublicationRecovery.NONE, + reopened.recoverPublication()); + PathStatePhysicalGlobalIntent current = PathStatePhysicalGlobalIntent.decode( + Files.readAllBytes(root.resolve(PathStatePhysicalStoreSet.CURRENT_FILE))); + assertArrayEquals(expectedRoot, current.getSuperRoot()); + PathStateRoot restored = reopened.createRoot(); + restored.restoreStoredRoots(current.getSuperRoot()); + restored.verifyNodeStores(); + assertArrayEquals(expectedRoot, restored.rootHash()); + } + } + + @Test + public void physicalDeleteFailsClosedBetweenParticipantSuperAndCurrent() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path participantFailure = temporaryFolder.newFolder( + "physical-delete-participant-failure").toPath(); + preparePublishedPhysicalTarget(participantFailure, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(participantFailure, + scope, Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, () -> stores.deleteAndPublish("proposal", + new byte[]{5, 6}, stage -> { + if (stage == PathStatePhysicalStoreSet.DeleteStage.AFTER_PARTICIPANT_BATCH) { + throw new java.io.IOException("injected failure after participant batch"); + } + })); + } + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.open(participantFailure, + scope, Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, reopened::recoverPublication); + assertThrows(java.io.IOException.class, reopened::publishCurrent); + } + + Path superFailure = temporaryFolder.newFolder("physical-delete-super-failure").toPath(); + preparePublishedPhysicalTarget(superFailure, scope); + byte[] expectedRoot = rootWithOnlyAccount(scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(superFailure, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, () -> stores.deleteAndPublish("proposal", + new byte[]{5, 6}, stage -> { + if (stage == PathStatePhysicalStoreSet.DeleteStage.AFTER_SUPER_BATCH) { + throw new java.io.IOException("injected failure after super batch"); + } + })); + } + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.open(superFailure, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, reopened::recoverPublication); + assertArrayEquals(expectedRoot, reopened.publishCurrent()); + } + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.open(superFailure, scope, + Engine.ROCKSDB)) { + assertEquals(PathStatePhysicalStoreSet.PublicationRecovery.NONE, + reopened.recoverPublication()); + PathStatePhysicalGlobalIntent current = PathStatePhysicalGlobalIntent.decode( + Files.readAllBytes(superFailure.resolve(PathStatePhysicalStoreSet.CURRENT_FILE))); + assertArrayEquals(expectedRoot, current.getSuperRoot()); + assertThrows(java.io.IOException.class, + () -> reopened.deleteAndPublish("proposal", new byte[]{5, 6})); + } + } + + @Test + public void physicalIngestResumesAfterFailureThenBuildsAndReopensTheSameRoot() + throws Exception { + Path directory = temporaryFolder.newFolder("physical-ingest-e2e").toPath(); + Path referenceDirectory = temporaryFolder.newFolder("physical-ingest-reference").toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + List rows = Arrays.asList( + new PhysicalRow(new byte[]{1}, new byte[]{11}), + new PhysicalRow(new byte[]{1, 0}, new byte[]{12}), + new PhysicalRow(new byte[]{2}, new byte[]{22})); + byte[] sourceIdentity = bytes(42); + ResumablePhysicalSource interrupted = new ResumablePhysicalSource( + "proposal", rows, sourceIdentity, 2); + + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, + () -> stores.ingestAndBuild(interrupted, 1, Long.MAX_VALUE)); + PathStatePhysicalIngestCheckpoint checkpoint = stores.ingestCheckpoint("proposal"); + assertEquals(2, checkpoint.getRows()); + assertArrayEquals(new byte[]{1, 0}, checkpoint.getCursor()); + } + + ResumablePhysicalSource resumed = new ResumablePhysicalSource( + "proposal", rows, sourceIdentity, Integer.MAX_VALUE); + byte[] rebuiltRoot; + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + rebuiltRoot = stores.ingestAndBuild(resumed, 1, Long.MAX_VALUE).rootHash(); + assertEquals(1, resumed.getScanCount("proposal")); + assertEquals(0, resumed.getScanCount("abi")); + assertArrayEquals(new byte[]{1, 0}, resumed.getLastCursor("proposal")); + PathStatePhysicalIngestCheckpoint checkpoint = stores.ingestCheckpoint("proposal"); + assertEquals(3, checkpoint.getRows()); + assertArrayEquals(new byte[]{2}, checkpoint.getCursor()); + } + + byte[] referenceRoot; + try (PathStatePhysicalStoreSet reference = PathStatePhysicalStoreSet.open( + referenceDirectory, scope, Engine.ROCKSDB)) { + PathStateRoot root = reference.createRoot(); + for (PhysicalRow row : rows) { + root.put("proposal", row.key, row.value); + } + referenceRoot = root.rootHash(); + } + assertArrayEquals(referenceRoot, rebuiltRoot); + + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + assertArrayEquals(referenceRoot, reopened.buildRootFromFlat().rootHash()); + } + } + private byte[] rootFor(PathStateStoreManifest manifest) throws Exception { byte[] stateRoot; PathStateRootMetadata progress; @@ -253,6 +975,56 @@ private byte[] rootFor(PathStateStoreManifest manifest) throws Exception { return stateRoot; } + private byte[] preparePhysicalTarget(Path directory, PathStateParticipantScope scope) + throws Exception { + byte[] expectedRoot; + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + PathStateRoot stateRoot = stores.createRoot(); + stateRoot.put("account", new byte[]{1, 2}, new byte[]{3, 4}); + stateRoot.put("proposal", new byte[]{5, 6}, new byte[]{7, 8}); + expectedRoot = stateRoot.rootHash(); + stores.persistFlatSnapshot(stateRoot); + } + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + assertArrayEquals(expectedRoot, stores.buildRootFromFlat().rootHash()); + } + return expectedRoot; + } + + private byte[] rootWithOnlyAccount(PathStateParticipantScope scope) throws Exception { + Path directory = temporaryFolder.newFolder("physical-delete-reference").toPath(); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + PathStateRoot root = stores.createRoot(); + root.put("account", new byte[]{1, 2}, new byte[]{3, 4}); + return root.rootHash(); + } + } + + private byte[] preparePublishedPhysicalTarget(Path directory, + PathStateParticipantScope scope) throws Exception { + byte[] expectedRoot = preparePhysicalTarget(directory, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + assertArrayEquals(expectedRoot, stores.publishCurrent()); + } + return expectedRoot; + } + + private void assertPublicationRejected(Path directory, PathStateParticipantScope scope) + throws Exception { + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, stores::recoverPublication); + } + } + + private static byte[] metadata(String name) { + return name.getBytes(java.nio.charset.StandardCharsets.US_ASCII); + } + private PathStateStoreManifest manifest(String name, Engine engine) throws Exception { return PathStateStoreManifest.createOrOpen(temporaryFolder.newFolder(name).toPath(), engine); } @@ -287,4 +1059,102 @@ private static byte[] bytes(int seed) { } return value; } + + private static final class PhysicalRow { + + private final byte[] key; + private final byte[] value; + + private PhysicalRow(byte[] key, byte[] value) { + this.key = key; + this.value = value; + } + } + + private static final class ResumablePhysicalSource implements SnapshotSource { + + private final String dbName; + private final List rows; + private final byte[] identity; + private final int failAfter; + private final java.util.Map scanCounts = new java.util.LinkedHashMap<>(); + private final java.util.Map lastCursors = new java.util.LinkedHashMap<>(); + + private ResumablePhysicalSource(String dbName, List rows, byte[] identity, + int failAfter) { + this.dbName = dbName; + this.rows = rows; + this.identity = identity; + this.failAfter = failAfter; + } + + @Override + public SnapshotIdentity identity() { + return new SnapshotIdentity(100, bytes(1), bytes(2), 300, P66Phase.P66_ON); + } + + @Override + public Collection databases() { + List names = new ArrayList<>(); + for (PathStateParticipantDescriptor.StoreIdentity store + : PathStateParticipantDescriptor.current().getStores()) { + names.add(store.getDbName()); + } + return names; + } + + @Override + public byte[] sourceIdentityDigest() { + return Arrays.copyOf(identity, identity.length); + } + + @Override + public void scan(String name, EntryConsumer consumer) throws java.io.IOException { + scanAfter(name, null, consumer); + } + + @Override + public void scanAfter(String name, byte[] cursor, EntryConsumer consumer) + throws java.io.IOException { + scanCounts.put(name, scanCounts.getOrDefault(name, 0) + 1); + lastCursors.put(name, cursor == null ? null : Arrays.copyOf(cursor, cursor.length)); + if (!dbName.equals(name)) { + return; + } + int emitted = 0; + for (PhysicalRow row : rows) { + if (cursor != null && compareUnsigned(row.key, cursor) <= 0) { + continue; + } + consumer.accept(row.key, row.value); + emitted++; + if (emitted >= failAfter) { + throw new java.io.IOException("injected physical ingest interruption"); + } + } + } + + @Override + public void verifyIdentity(SnapshotIdentity expected) { + } + + private int getScanCount(String name) { + return scanCounts.getOrDefault(name, 0); + } + + private byte[] getLastCursor(String name) { + byte[] cursor = lastCursors.get(name); + return cursor == null ? null : Arrays.copyOf(cursor, cursor.length); + } + } + + private static int compareUnsigned(byte[] left, byte[] right) { + for (int index = 0; index < Math.min(left.length, right.length); index++) { + int compared = Integer.compare(left[index] & 0xff, right[index] & 0xff); + if (compared != 0) { + return compared; + } + } + return Integer.compare(left.length, right.length); + } } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java index d92ae400192..a067da5af67 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeSnapshotSourceTest.java @@ -85,6 +85,37 @@ public void pinsExactStoresPagesLexicallyAndOrdersMarketRows() throws Exception assertEquals(27, registry.totalCloses()); } + @Test + public void lexicalPagingPreservesAKeyThatExtendsThePreviousPageBoundary() throws Exception { + Registry registry = registry(); + registry.probes.get("proposal").add(new byte[]{1}, new byte[]{11}); + registry.probes.get("proposal").add(new byte[]{1, 0}, new byte[]{12}); + List keys = new ArrayList<>(); + try (PathStateNativeSnapshotSource source = PathStateNativeSnapshotSource.acquire( + registry.manager, Collections.emptyMap(), PathStateNativeSnapshotSourceTest::identity, + 1, 10)) { + source.scan("proposal", (key, value) -> keys.add(key)); + } + assertEquals(2, keys.size()); + assertArrayEquals(new byte[]{1}, keys.get(0)); + assertArrayEquals(new byte[]{1, 0}, keys.get(1)); + } + + @Test + public void resumableLexicalScanStartsStrictlyAfterThePhysicalCursor() throws Exception { + Registry registry = registry(); + registry.probes.get("proposal").add(new byte[]{1}, new byte[]{11}); + registry.probes.get("proposal").add(new byte[]{1, 0}, new byte[]{12}); + List keys = new ArrayList<>(); + try (PathStateNativeSnapshotSource source = PathStateNativeSnapshotSource.acquire( + registry.manager, Collections.emptyMap(), PathStateNativeSnapshotSourceTest::identity, + 1, 10)) { + source.scanAfter("proposal", new byte[]{1}, (key, value) -> keys.add(key)); + } + assertEquals(1, keys.size()); + assertArrayEquals(new byte[]{1, 0}, keys.get(0)); + } + @Test public void acceptsSupplementalAccountAssetAndRejectsMarketOverflow() throws Exception { Registry registry = registry(); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmissionTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmissionTest.java index f8b8619f22d..2dc4097a2a4 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmissionTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRuntimeAdmissionTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import java.nio.file.Files; import java.nio.file.Path; @@ -56,6 +57,41 @@ public void enabledAdmissionDistinguishesRebuildFromCurrentReady() throws Except assertArrayEquals(manifest.getIdentityDigest(), ready.getManifest().getIdentityDigest()); } + @Test + public void physicalAdmissionIsNonCreatingRejectsLegacyAndBindsCurrentMetadata() + throws Exception { + assertSame(PathStatePhysicalRuntimeAdmission.Status.DISABLED, + PathStatePhysicalRuntimeAdmission.inspect(false, null, null).getStatus()); + + Path missing = temporaryFolder.getRoot().toPath().resolve("physical-missing"); + assertSame(PathStatePhysicalRuntimeAdmission.Status.REBUILD_REQUIRED, + PathStatePhysicalRuntimeAdmission.inspect(true, missing, Engine.ROCKSDB).getStatus()); + assertFalse(Files.exists(missing)); + + Path legacy = temporaryFolder.getRoot().toPath().resolve("physical-legacy"); + PathStateStoreManifest.createOrOpen(legacy, Engine.ROCKSDB); + assertThrows(java.io.IOException.class, + () -> PathStatePhysicalRuntimeAdmission.inspect(true, legacy, Engine.ROCKSDB)); + + Path physical = temporaryFolder.getRoot().toPath().resolve("physical-ready"); + PathStateRootMetadata expected; + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(physical, + new PathStateCanonicalizer().participantScope(), Engine.ROCKSDB)) { + PathStateRoot root = stores.buildRootFromFlat(); + expected = PathStateRootMetadata.base(100, bytes(4), bytes(5), 300, + P66Phase.P66_ON, stores.getFormatDigest(), root.rootHash(), bytes(6)); + stores.publishCurrent(expected); + } + assertSame(PathStatePhysicalRuntimeAdmission.Status.CURRENT_CANDIDATE, + PathStatePhysicalRuntimeAdmission.inspect(true, physical, Engine.ROCKSDB).getStatus()); + try (PathStatePhysicalSnapshotHead head = PathStatePhysicalSnapshotHead.open( + physical, Engine.ROCKSDB)) { + assertArrayEquals(expected.encode(), head.getHead().encode()); + } + assertFalse(Files.exists(physical.resolve("base").resolve("nodes"))); + assertFalse(Files.exists(physical.resolve("rebuild-spool"))); + } + private static byte[] bytes(int seed) { byte[] value = new byte[32]; for (int index = 0; index < value.length; index++) { From 3cd76bc37d200460e459483485ffeda2b70024e7 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 1 Sep 2026 08:11:32 +0800 Subject: [PATCH 103/161] perf(trie): batch physical state bootstrap writes --- .../stateroot/PathStateNativeNodeStore.java | 12 + .../stateroot/PathStatePhysicalStoreSet.java | 259 ++++++++++++++++-- .../PathStateNativeNodeStoreTest.java | 181 +++++++++++- 3 files changed, 429 insertions(+), 23 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java index a04cc8957c1..a9aae424bda 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java @@ -29,6 +29,8 @@ final class PathStateNativeNodeStore implements Closeable { private final Path directory; private final Engine engine; private final Delegate delegate; + private long writeBatchCalls; + private long writeBatchMutations; private volatile boolean closed; private PathStateNativeNodeStore(Path directory, Engine engine, Delegate delegate) { @@ -78,6 +80,16 @@ synchronized void writeBatch(List mutations) { Objects.requireNonNull(mutation, "mutation"); } delegate.writeBatch(supplied); + writeBatchCalls = Math.addExact(writeBatchCalls, 1L); + writeBatchMutations = Math.addExact(writeBatchMutations, supplied.size()); + } + + synchronized long getWriteBatchCalls() { + return writeBatchCalls; + } + + synchronized long getWriteBatchMutations() { + return writeBatchMutations; } synchronized List scanPrefix(byte[] prefix) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java index d7a4bfe0c1b..6e4c388cd46 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java @@ -8,15 +8,21 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; -import java.util.HashSet; import java.util.ArrayList; import java.util.Arrays; -import java.util.List; +import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.tron.common.crypto.Hash; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; @@ -29,8 +35,15 @@ */ public final class PathStatePhysicalStoreSet implements Closeable { + private static final Logger logger = LoggerFactory.getLogger("DB"); + static final long DEFAULT_CHECKPOINT_ROWS = 1_000_000L; static final long DEFAULT_CHECKPOINT_BYTES = 256L * 1024 * 1024; + static final int BOOTSTRAP_WRITE_BATCH_ENTRIES = 4096; + static final long BOOTSTRAP_WRITE_BATCH_BYTES = 8L * 1024 * 1024; + private static final Set LARGE_BOOTSTRAP_STORES = java.util.Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + "account", "account-asset", "delegation", "storage-row"))); private static final String STORES_DIRECTORY = "stores"; private static final String SUPER_DIRECTORY = "super"; @@ -159,52 +172,100 @@ synchronized PathStatePhysicalIngestCheckpoint ingestCheckpoint(String dbName) { } /** Ingests exact physical rows into one F domain and durably advances its source cursor. */ - synchronized void ingestFlat(String dbName, PathStateRebuildCoordinator.SnapshotSource source, + void ingestFlat(String dbName, PathStateRebuildCoordinator.SnapshotSource source, long rowThreshold, long byteThreshold) throws IOException { if (rowThreshold <= 0 || byteThreshold <= 0) { throw new IllegalArgumentException("ingest checkpoint thresholds must be positive"); } PathStateParticipant participant = scope.require(dbName); + PhysicalStore store = participants.get(dbName); + if (store == null) { + throw new IllegalArgumentException("unknown path-state participant: " + dbName); + } PathStateRebuildCoordinator.SnapshotSource pinned = Objects.requireNonNull(source, "source"); byte[] identity = pinned.sourceIdentityDigest(); if (identity.length != PathStateCommitmentCodec.ROOT_LENGTH) { throw new IOException("physical ingest source identity must contain exactly 32 bytes"); } - byte[] complete = participant(dbName).getMetadata(FLAT_INGEST_COMPLETE); + byte[] complete = store.getMetadata(FLAT_INGEST_COMPLETE); if (complete != null) { if (!Arrays.equals(complete, identity)) { throw new IOException("physical ingest completion source identity differs: " + dbName); } return; } - PathStatePhysicalIngestCheckpoint prior = ingestCheckpoint(dbName); + byte[] encodedCheckpoint = store.getMetadata(FLAT_INGEST_CHECKPOINT); + PathStatePhysicalIngestCheckpoint prior = encodedCheckpoint == null ? null + : PathStatePhysicalIngestCheckpoint.decode(encodedCheckpoint); if (prior != null && !Arrays.equals(prior.getSourceIdentity(), identity)) { throw new IOException("physical ingest checkpoint source identity differs: " + dbName); } long[] progress = prior == null ? new long[]{0, 0} : new long[]{prior.getRows(), prior.getBytes()}; byte[][] cursor = new byte[][]{prior == null ? null : prior.getCursor()}; + List pending = + new ArrayList<>(BOOTSTRAP_WRITE_BATCH_ENTRIES + 2); + long[] pendingBytes = new long[1]; long[] sinceCheckpoint = new long[2]; + long startedNanos = System.nanoTime(); + long initialRows = progress[0]; + long initialBatchCalls = store.getWriteBatchCalls(); + long initialBatchMutations = store.getWriteBatchMutations(); pinned.scanAfter(dbName, cursor[0], (physicalKey, physicalValue) -> { byte[] key = Arrays.copyOf(physicalKey, physicalKey.length); - participant(dbName).putFlat(PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), key), - PathStateCommitmentCodec.presentLeafValue(physicalValue)); + byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), key); + byte[] encodedLeaf = PathStateCommitmentCodec.presentLeafValue(physicalValue); + byte[] storedKey = prefixed(FLAT_PREFIX, secureKey, "secureKey"); + long mutationBytes = storedKey.length + encodedLeaf.length; + if (!pending.isEmpty() + && pendingBytes[0] + mutationBytes > BOOTSTRAP_WRITE_BATCH_BYTES) { + store.writeBatch(pending); + pending.clear(); + pendingBytes[0] = 0; + } + pending.add(PathStateNativeNodeStore.BatchMutation.put(storedKey, encodedLeaf)); + pendingBytes[0] = Math.addExact(pendingBytes[0], mutationBytes); cursor[0] = key; progress[0]++; progress[1] += key.length + physicalValue.length; sinceCheckpoint[0]++; sinceCheckpoint[1] += key.length + physicalValue.length; - if (sinceCheckpoint[0] >= rowThreshold || sinceCheckpoint[1] >= byteThreshold) { - saveIngestCheckpoint(dbName, new PathStatePhysicalIngestCheckpoint(identity, cursor[0], - progress[0], progress[1])); + boolean checkpointDue = sinceCheckpoint[0] >= rowThreshold + || sinceCheckpoint[1] >= byteThreshold; + if (checkpointDue) { + pending.add(PhysicalStore.metadataMutation(FLAT_INGEST_CHECKPOINT, + new PathStatePhysicalIngestCheckpoint(identity, cursor[0], progress[0], progress[1]) + .encode())); + } + if (checkpointDue || pending.size() >= BOOTSTRAP_WRITE_BATCH_ENTRIES + || pendingBytes[0] >= BOOTSTRAP_WRITE_BATCH_BYTES) { + store.writeBatch(pending); + pending.clear(); + pendingBytes[0] = 0; + } + if (checkpointDue) { + logger.info("Path-state physical ingest checkpointed: storeId={}, dbName={}, rows={}, " + + "inputBytes={}, batches={}, mutations={}, elapsedMs={}, rowsPerSecond={}", + participant.getStoreId(), dbName, progress[0], progress[1], + store.getWriteBatchCalls() - initialBatchCalls, + store.getWriteBatchMutations() - initialBatchMutations, + elapsedMillis(startedNanos), rowsPerSecond(progress[0] - initialRows, startedNanos)); sinceCheckpoint[0] = 0; sinceCheckpoint[1] = 0; } }); if (cursor[0] != null) { - saveIngestCheckpoint(dbName, new PathStatePhysicalIngestCheckpoint(identity, cursor[0], - progress[0], progress[1])); + pending.add(PhysicalStore.metadataMutation(FLAT_INGEST_CHECKPOINT, + new PathStatePhysicalIngestCheckpoint(identity, cursor[0], progress[0], progress[1]) + .encode())); } - participant(dbName).putMetadata(FLAT_INGEST_COMPLETE, identity); + pending.add(PhysicalStore.metadataMutation(FLAT_INGEST_COMPLETE, identity)); + store.writeBatch(pending); + logger.info("Path-state physical ingest completed: storeId={}, dbName={}, rows={}, " + + "inputBytes={}, batches={}, mutations={}, elapsedMs={}, rowsPerSecond={}", + participant.getStoreId(), dbName, progress[0], progress[1], + store.getWriteBatchCalls() - initialBatchCalls, + store.getWriteBatchMutations() - initialBatchMutations, + elapsedMillis(startedNanos), rowsPerSecond(progress[0] - initialRows, startedNanos)); } /** Validates one exact-27 pinned source, resumes all unfinished F ingests, then builds the root. */ @@ -222,12 +283,66 @@ synchronized PathStateRoot ingestAndBuild(PathStateRebuildCoordinator.SnapshotSo pinned.identity(), "source identity"); pinned.verifyIdentity(identity); PathStateParticipantDescriptor.current().requireExactDatabases(pinned.databases()); - for (PathStateParticipant participant : scope.getParticipants()) { - ingestFlat(participant.getDbName(), pinned, rowThreshold, byteThreshold); - } + ingestFlatParticipants(pinned, rowThreshold, byteThreshold); + pinned.verifyIdentity(identity); return buildRootFromFlat(); } + private void ingestFlatParticipants(PathStateRebuildCoordinator.SnapshotSource source, + long rowThreshold, long byteThreshold) throws IOException { + ExecutorService largeExecutor = newBootstrapExecutor("large"); + ExecutorService smallExecutor = newBootstrapExecutor("small"); + List> futures = new ArrayList<>(); + try { + for (PathStateParticipant participant : scope.getParticipants()) { + ExecutorService executor = LARGE_BOOTSTRAP_STORES.contains(participant.getDbName()) + ? largeExecutor : smallExecutor; + futures.add(executor.submit(() -> { + ingestFlat(participant.getDbName(), source, rowThreshold, byteThreshold); + return null; + })); + } + for (Future future : futures) { + try { + future.get(); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + cancelOutstanding(futures); + throw new IOException("path-state physical ingest interrupted", interrupted); + } catch (ExecutionException failed) { + cancelOutstanding(futures); + Throwable cause = failed.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + throw new IOException("path-state physical ingest failed", cause); + } + } + } finally { + largeExecutor.shutdownNow(); + smallExecutor.shutdownNow(); + } + } + + private static ExecutorService newBootstrapExecutor(String tier) { + return Executors.newSingleThreadExecutor(task -> { + Thread thread = new Thread(task, "path-state-physical-bootstrap-" + tier); + thread.setDaemon(true); + return thread; + }); + } + + private static void cancelOutstanding(List> futures) { + for (Future future : futures) { + if (!future.isDone()) { + future.cancel(true); + } + } + } + /** * Persists a complete local F-domain snapshot and its root marker. * @@ -298,7 +413,12 @@ synchronized PathStateRoot buildRootFromFlat(BuildFaultHook faultHook) throws IO continue; } store.clearNodes(); - PathStateStackTrie trie = new PathStateStackTrie(store.nodeStore()::put); + long startedNanos = System.nanoTime(); + long initialBatchCalls = store.getWriteBatchCalls(); + long initialBatchMutations = store.getWriteBatchMutations(); + long[] rows = new long[1]; + PhysicalNodeBatchWriter nodeWriter = store.nodeBatchWriter(); + PathStateStackTrie trie = new PathStateStackTrie(nodeWriter::put); Hasher flatHasher = Hashing.sha256().newHasher(); store.scanFlat(entry -> { byte[] secureKey = unprefixedFlatKey(entry.getKey()); @@ -306,14 +426,20 @@ synchronized PathStateRoot buildRootFromFlat(BuildFaultHook faultHook) throws IO flatHasher.putInt(secureKey.length).putBytes(secureKey) .putInt(encodedValue.length).putBytes(encodedValue); trie.update(secureKey, encodedValue); + rows[0]++; }); byte[] storeRoot = trie.rootHash(); + nodeWriter.flush(); byte[] flatDigest = flatHasher.hash().asBytes(); byte[] generation = participantGeneration(participant, flatDigest, storeRoot); - store.putMetadata(FLAT_DIGEST_METADATA, flatDigest); - store.putMetadata(STORE_GENERATION_METADATA, generation); hook.beforeCompletion(participant, storeRoot); - store.putMetadata(FLAT_COMPLETE_METADATA, storeRoot); + store.completeFlatBuild(flatDigest, generation, storeRoot); + logger.info("Path-state physical trie completed: storeId={}, dbName={}, rows={}, " + + "batches={}, mutations={}, elapsedMs={}, rowsPerSecond={}", + participant.getStoreId(), participant.getDbName(), rows[0], + store.getWriteBatchCalls() - initialBatchCalls, + store.getWriteBatchMutations() - initialBatchMutations, + elapsedMillis(startedNanos), rowsPerSecond(rows[0], startedNanos)); targets.add(new PathStatePhysicalGlobalIntent.ParticipantTarget(participant.getStoreId(), generation, flatDigest, storeRoot)); root.completeRebuildParticipant(participant.getDbName(), storeRoot); @@ -1114,6 +1240,16 @@ private static byte[] unprefixedFlatKey(byte[] storedKey) { return Arrays.copyOfRange(key, 1, key.length); } + private static long elapsedMillis(long startedNanos) { + return java.util.concurrent.TimeUnit.NANOSECONDS.toMillis( + System.nanoTime() - startedNanos); + } + + private static long rowsPerSecond(long rows, long startedNanos) { + long elapsedNanos = Math.max(1L, System.nanoTime() - startedNanos); + return (long) (rows * 1_000_000_000D / elapsedNanos); + } + /** One participant or super database with independent F/N/M key domains. */ public static final class PhysicalStore implements Closeable { @@ -1127,6 +1263,10 @@ public void putFlat(byte[] secureKey, byte[] encodedLeaf) { nativeStore.put(prefixed(FLAT_PREFIX, secureKey, "secureKey"), encodedLeaf); } + private void writeBatch(List mutations) { + nativeStore.writeBatch(new ArrayList<>(mutations)); + } + public byte[] getFlat(byte[] secureKey) { return nativeStore.get(prefixed(FLAT_PREFIX, secureKey, "secureKey")); } @@ -1143,6 +1283,26 @@ public PathNodeStore nodeStore() { return new PhysicalNodeStore(nativeStore); } + private PhysicalNodeBatchWriter nodeBatchWriter() { + return new PhysicalNodeBatchWriter(nativeStore); + } + + private void completeFlatBuild(byte[] flatDigest, byte[] generation, byte[] storeRoot) { + List mutations = new ArrayList<>(3); + mutations.add(metadataMutation(FLAT_DIGEST_METADATA, flatDigest)); + mutations.add(metadataMutation(STORE_GENERATION_METADATA, generation)); + mutations.add(metadataMutation(FLAT_COMPLETE_METADATA, storeRoot)); + nativeStore.writeBatch(mutations); + } + + long getWriteBatchCalls() { + return nativeStore.getWriteBatchCalls(); + } + + long getWriteBatchMutations() { + return nativeStore.getWriteBatchMutations(); + } + void clearNodes() throws IOException { List pending = new ArrayList<>(4096); nativeStore.scanPrefix(new byte[]{NODE_PREFIX}, entry -> { @@ -1258,6 +1418,65 @@ public void delete(byte[] path) { } } + private static final class PhysicalNodeBatchWriter implements PathNodeStore { + + private final PathStateNativeNodeStore nativeStore; + private final List pending = + new ArrayList<>(BOOTSTRAP_WRITE_BATCH_ENTRIES); + private long pendingBytes; + + private PhysicalNodeBatchWriter(PathStateNativeNodeStore nativeStore) { + this.nativeStore = nativeStore; + } + + @Override + public byte[] get(byte[] path) { + flush(); + return nativeStore.get(prefixed(NODE_PREFIX, path, "path")); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + byte[] key = prefixed(NODE_PREFIX, path, "path"); + byte[] value = Arrays.copyOf(Objects.requireNonNull(encodedNode, "encodedNode"), + encodedNode.length); + flushBeforeOversizedMutation(key.length + value.length); + pending.add(PathStateNativeNodeStore.BatchMutation.put(key, value)); + pendingBytes = Math.addExact(pendingBytes, key.length + value.length); + flushIfFull(); + } + + @Override + public void delete(byte[] path) { + byte[] key = prefixed(NODE_PREFIX, path, "path"); + flushBeforeOversizedMutation(key.length); + pending.add(PathStateNativeNodeStore.BatchMutation.delete(key)); + pendingBytes = Math.addExact(pendingBytes, key.length); + flushIfFull(); + } + + private void flushIfFull() { + if (pending.size() >= BOOTSTRAP_WRITE_BATCH_ENTRIES + || pendingBytes >= BOOTSTRAP_WRITE_BATCH_BYTES) { + flush(); + } + } + + private void flushBeforeOversizedMutation(long mutationBytes) { + if (!pending.isEmpty() && pendingBytes + mutationBytes > BOOTSTRAP_WRITE_BATCH_BYTES) { + flush(); + } + } + + private void flush() { + if (!pending.isEmpty()) { + nativeStore.writeBatch(new ArrayList<>(pending)); + pending.clear(); + pendingBytes = 0; + } + } + } + private static final class RecordingNodeStore implements PathNodeStore { private final PathNodeStore base; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index 1cafc57db1f..33d72744017 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -901,6 +901,79 @@ public void physicalDeleteFailsClosedBetweenParticipantSuperAndCurrent() throws } } + @Test + public void physicalBootstrapBatchesFlatAndNodeWrites() throws Exception { + Path directory = temporaryFolder.newFolder("physical-batched-bootstrap").toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + List rows = new ArrayList<>(); + for (int index = 0; index < PathStatePhysicalStoreSet.BOOTSTRAP_WRITE_BATCH_ENTRIES * 2 + 1; + index++) { + rows.add(new PhysicalRow(new byte[]{ + (byte) (index >>> 24), (byte) (index >>> 16), (byte) (index >>> 8), (byte) index}, + new byte[]{(byte) (index + 1)})); + } + ResumablePhysicalSource source = new ResumablePhysicalSource( + "proposal", rows, bytes(43), Integer.MAX_VALUE); + + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + PathStatePhysicalStoreSet.PhysicalStore proposal = stores.participant("proposal"); + long ingestCalls = proposal.getWriteBatchCalls(); + long ingestMutations = proposal.getWriteBatchMutations(); + stores.ingestFlat("proposal", source, Long.MAX_VALUE, Long.MAX_VALUE); + assertEquals(3, proposal.getWriteBatchCalls() - ingestCalls); + assertEquals(rows.size() + 2, proposal.getWriteBatchMutations() - ingestMutations); + + long buildCalls = proposal.getWriteBatchCalls(); + long buildMutations = proposal.getWriteBatchMutations(); + stores.buildRootFromFlat(); + long nodeCalls = proposal.getWriteBatchCalls() - buildCalls; + long nodeMutations = proposal.getWriteBatchMutations() - buildMutations; + assertTrue(nodeMutations > rows.size()); + assertTrue(nodeCalls < 16); + assertTrue(nodeCalls * 1000 < nodeMutations); + } + } + + @Test + public void physicalFlatIngestFlushesBeforeByteLimit() throws Exception { + Path directory = temporaryFolder.newFolder("physical-byte-batched-bootstrap").toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + int valueBytes = 3 * 1024 * 1024; + List rows = Arrays.asList( + new PhysicalRow(new byte[]{1}, new byte[valueBytes]), + new PhysicalRow(new byte[]{2}, new byte[valueBytes]), + new PhysicalRow(new byte[]{3}, new byte[valueBytes])); + ResumablePhysicalSource source = new ResumablePhysicalSource( + "proposal", rows, bytes(45), Integer.MAX_VALUE); + + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + PathStatePhysicalStoreSet.PhysicalStore proposal = stores.participant("proposal"); + long calls = proposal.getWriteBatchCalls(); + long mutations = proposal.getWriteBatchMutations(); + stores.ingestFlat("proposal", source, Long.MAX_VALUE, Long.MAX_VALUE); + assertEquals(2, proposal.getWriteBatchCalls() - calls); + assertEquals(rows.size() + 2, proposal.getWriteBatchMutations() - mutations); + } + } + + @Test + public void physicalBootstrapRunsOneLargeAndOneSmallIngestQueue() throws Exception { + Path directory = temporaryFolder.newFolder("physical-tiered-bootstrap").toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + ConcurrentTierPhysicalSource source = new ConcurrentTierPhysicalSource(); + + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + stores.ingestAndBuild(source); + } + + assertEquals(2, source.getMaxActive()); + assertTrue(source.sawThread("path-state-physical-bootstrap-large")); + assertTrue(source.sawThread("path-state-physical-bootstrap-small")); + } + @Test public void physicalIngestResumesAfterFailureThenBuildsAndReopensTheSameRoot() throws Exception { @@ -955,6 +1028,38 @@ public void physicalIngestResumesAfterFailureThenBuildsAndReopensTheSameRoot() } } + @Test + public void physicalIngestReplaysUncheckpointedBatchAfterFailure() throws Exception { + Path directory = temporaryFolder.newFolder("physical-uncheckpointed-replay").toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + List rows = Arrays.asList( + new PhysicalRow(new byte[]{1}, new byte[]{11}), + new PhysicalRow(new byte[]{2}, new byte[]{12}), + new PhysicalRow(new byte[]{3}, new byte[]{13})); + byte[] sourceIdentity = bytes(46); + ResumablePhysicalSource interrupted = new ResumablePhysicalSource( + "proposal", rows, sourceIdentity, 2); + + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + assertThrows(java.io.IOException.class, + () -> stores.ingestFlat("proposal", interrupted, Long.MAX_VALUE, Long.MAX_VALUE)); + assertNull(stores.ingestCheckpoint("proposal")); + assertEquals(0, stores.participant("proposal").getWriteBatchCalls()); + } + + ResumablePhysicalSource resumed = new ResumablePhysicalSource( + "proposal", rows, sourceIdentity, Integer.MAX_VALUE); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, + Engine.ROCKSDB)) { + stores.ingestFlat("proposal", resumed, Long.MAX_VALUE, Long.MAX_VALUE); + assertNull(resumed.getLastCursor("proposal")); + PathStatePhysicalIngestCheckpoint checkpoint = stores.ingestCheckpoint("proposal"); + assertEquals(3, checkpoint.getRows()); + assertArrayEquals(new byte[]{3}, checkpoint.getCursor()); + } + } + private byte[] rootFor(PathStateStoreManifest manifest) throws Exception { byte[] stateRoot; PathStateRootMetadata progress; @@ -1114,7 +1219,7 @@ public void scan(String name, EntryConsumer consumer) throws java.io.IOException } @Override - public void scanAfter(String name, byte[] cursor, EntryConsumer consumer) + public synchronized void scanAfter(String name, byte[] cursor, EntryConsumer consumer) throws java.io.IOException { scanCounts.put(name, scanCounts.getOrDefault(name, 0) + 1); lastCursors.put(name, cursor == null ? null : Arrays.copyOf(cursor, cursor.length)); @@ -1138,16 +1243,86 @@ public void scanAfter(String name, byte[] cursor, EntryConsumer consumer) public void verifyIdentity(SnapshotIdentity expected) { } - private int getScanCount(String name) { + private synchronized int getScanCount(String name) { return scanCounts.getOrDefault(name, 0); } - private byte[] getLastCursor(String name) { + private synchronized byte[] getLastCursor(String name) { byte[] cursor = lastCursors.get(name); return cursor == null ? null : Arrays.copyOf(cursor, cursor.length); } } + private static final class ConcurrentTierPhysicalSource implements SnapshotSource { + + private final java.util.concurrent.CountDownLatch started = + new java.util.concurrent.CountDownLatch(2); + private final AtomicInteger active = new AtomicInteger(); + private final AtomicInteger maxActive = new AtomicInteger(); + private final java.util.Set threads = java.util.Collections.synchronizedSet( + new java.util.HashSet<>()); + + @Override + public SnapshotIdentity identity() { + return new SnapshotIdentity(100, bytes(1), bytes(2), 300, P66Phase.P66_ON); + } + + @Override + public Collection databases() { + List names = new ArrayList<>(); + for (PathStateParticipantDescriptor.StoreIdentity store + : PathStateParticipantDescriptor.current().getStores()) { + names.add(store.getDbName()); + } + return names; + } + + @Override + public byte[] sourceIdentityDigest() { + return bytes(44); + } + + @Override + public void scan(String name, EntryConsumer consumer) throws java.io.IOException { + scanAfter(name, null, consumer); + } + + @Override + public void scanAfter(String name, byte[] cursor, EntryConsumer consumer) + throws java.io.IOException { + if (!"account".equals(name) && !"proposal".equals(name)) { + return; + } + int current = active.incrementAndGet(); + maxActive.accumulateAndGet(current, Math::max); + threads.add(Thread.currentThread().getName()); + started.countDown(); + try { + if (!started.await(5, java.util.concurrent.TimeUnit.SECONDS)) { + throw new java.io.IOException("physical tier queues did not overlap"); + } + consumer.accept(new byte[]{1}, new byte[]{(byte) ("account".equals(name) ? 1 : 2)}); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new java.io.IOException("physical tier queue test interrupted", interrupted); + } finally { + active.decrementAndGet(); + } + } + + @Override + public void verifyIdentity(SnapshotIdentity expected) { + } + + private int getMaxActive() { + return maxActive.get(); + } + + private boolean sawThread(String name) { + return threads.contains(name); + } + } + private static int compareUnsigned(byte[] left, byte[] right) { for (int index = 0; index < Math.min(left.length, right.length); index++) { int compared = Integer.compare(left[index] & 0xff, right[index] & 0xff); From 574ad05252850cb5ec2fb5c4097b69571fc5ed74 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 2 Sep 2026 00:56:22 +0800 Subject: [PATCH 104/161] perf(trie): optimize physical state updates --- .../stateroot/PathStateNativeNodeStore.java | 42 ++- .../PathStatePhysicalSnapshotHead.java | 2 +- .../stateroot/PathStatePhysicalStoreSet.java | 285 +++++++++++++++--- .../PathStateNativeNodeStoreTest.java | 86 ++++++ 4 files changed, 360 insertions(+), 55 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java index a9aae424bda..022a2fb0109 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java @@ -31,6 +31,8 @@ final class PathStateNativeNodeStore implements Closeable { private final Delegate delegate; private long writeBatchCalls; private long writeBatchMutations; + private long syncedWriteBatchCalls; + private long unsyncedWriteBatchCalls; private volatile boolean closed; private PathStateNativeNodeStore(Path directory, Engine engine, Delegate delegate) { @@ -39,7 +41,7 @@ private PathStateNativeNodeStore(Path directory, Engine engine, Delegate delegat this.delegate = delegate; } - /** Opens one independent node database; every mutation is synchronously WAL-backed. */ + /** Opens one independent node database; callers choose the WAL sync boundary per batch. */ static PathStateNativeNodeStore open(Path directory, Engine engine) throws IOException { Path path = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); Engine selected = Objects.requireNonNull(engine, "engine"); @@ -71,6 +73,14 @@ synchronized void delete(byte[] key) { } synchronized void writeBatch(List mutations) { + writeBatch(mutations, true); + } + + synchronized void writeBatchUnsynced(List mutations) { + writeBatch(mutations, false); + } + + private void writeBatch(List mutations, boolean sync) { requireOpen(); List supplied = Objects.requireNonNull(mutations, "mutations"); if (supplied.isEmpty()) { @@ -79,9 +89,14 @@ synchronized void writeBatch(List mutations) { for (BatchMutation mutation : supplied) { Objects.requireNonNull(mutation, "mutation"); } - delegate.writeBatch(supplied); + delegate.writeBatch(supplied, sync); writeBatchCalls = Math.addExact(writeBatchCalls, 1L); writeBatchMutations = Math.addExact(writeBatchMutations, supplied.size()); + if (sync) { + syncedWriteBatchCalls = Math.addExact(syncedWriteBatchCalls, 1L); + } else { + unsyncedWriteBatchCalls = Math.addExact(unsyncedWriteBatchCalls, 1L); + } } synchronized long getWriteBatchCalls() { @@ -92,6 +107,14 @@ synchronized long getWriteBatchMutations() { return writeBatchMutations; } + synchronized long getSyncedWriteBatchCalls() { + return syncedWriteBatchCalls; + } + + synchronized long getUnsyncedWriteBatchCalls() { + return unsyncedWriteBatchCalls; + } + synchronized List scanPrefix(byte[] prefix) throws IOException { List entries = new ArrayList<>(); scanPrefix(prefix, entries::add); @@ -149,7 +172,7 @@ private interface Delegate extends Closeable { byte[] get(byte[] key); - void writeBatch(List mutations); + void writeBatch(List mutations, boolean sync); void scanPrefix(byte[] prefix, EntryConsumer consumer) throws IOException; @@ -160,6 +183,7 @@ private static final class LevelDelegate implements Delegate { private final org.iq80.leveldb.Options options = DbOptionalsUtils.createDefaultDbOptions(); private final WriteOptions syncWrites = new WriteOptions().sync(true); + private final WriteOptions unsyncedWrites = new WriteOptions().sync(false); private final DB database; private LevelDelegate(Path directory) throws IOException { @@ -172,7 +196,7 @@ public byte[] get(byte[] key) { } @Override - public void writeBatch(List mutations) { + public void writeBatch(List mutations, boolean sync) { try (org.iq80.leveldb.WriteBatch batch = database.createWriteBatch()) { for (BatchMutation mutation : mutations) { if (mutation.value == null) { @@ -181,7 +205,7 @@ public void writeBatch(List mutations) { batch.put(mutation.key, mutation.value); } } - database.write(batch, syncWrites); + database.write(batch, sync ? syncWrites : unsyncedWrites); } catch (IOException failure) { throw new IllegalStateException("failed to apply path-state LevelDB node batch", failure); } @@ -224,12 +248,15 @@ private static final class RocksDelegate implements Delegate { new org.rocksdb.Options().setCreateIfMissing(true).setParanoidChecks(true); private final org.rocksdb.WriteOptions syncWrites = new org.rocksdb.WriteOptions().setSync(true); + private final org.rocksdb.WriteOptions unsyncedWrites = + new org.rocksdb.WriteOptions().setSync(false); private final org.rocksdb.RocksDB database; private RocksDelegate(Path directory) throws IOException { try { database = org.rocksdb.RocksDB.open(options, directory.toString()); } catch (RocksDBException failure) { + unsyncedWrites.close(); syncWrites.close(); options.close(); throw new IOException("failed to open path-state RocksDB node database", failure); @@ -246,7 +273,7 @@ public byte[] get(byte[] key) { } @Override - public void writeBatch(List mutations) { + public void writeBatch(List mutations, boolean sync) { try (org.rocksdb.WriteBatch batch = new org.rocksdb.WriteBatch()) { for (BatchMutation mutation : mutations) { if (mutation.value == null) { @@ -255,7 +282,7 @@ public void writeBatch(List mutations) { batch.put(mutation.key, mutation.value); } } - database.write(syncWrites, batch); + database.write(sync ? syncWrites : unsyncedWrites, batch); } catch (RocksDBException failure) { throw new IllegalStateException("failed to apply path-state RocksDB node batch", failure); } @@ -291,6 +318,7 @@ public void scanAll(EntryConsumer consumer) throws IOException { @Override public void close() { + unsyncedWrites.close(); syncWrites.close(); database.close(); options.close(); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java index 0b2476e2b31..2523eda68db 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java @@ -53,7 +53,7 @@ public synchronized PathStateRootMetadata advance(PathStateBlockTransition trans requireHealthy(); PathStateRootMetadata previous = head; try { - PathStateRootMetadata committed = stores.applyAndPublish(transition, limits, stage -> { }); + PathStateRootMetadata committed = stores.applyAndPublish(transition, limits); if (!same(committed, stores.currentMetadata())) { failed = true; throw new IOException("physical path-state committed CURRENT identity mismatch"); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java index 6e4c388cd46..a232faae389 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java @@ -41,6 +41,7 @@ public final class PathStatePhysicalStoreSet implements Closeable { static final long DEFAULT_CHECKPOINT_BYTES = 256L * 1024 * 1024; static final int BOOTSTRAP_WRITE_BATCH_ENTRIES = 4096; static final long BOOTSTRAP_WRITE_BATCH_BYTES = 8L * 1024 * 1024; + private static final int MAX_PARALLEL_PARTICIPANT_WRITES = 4; private static final Set LARGE_BOOTSTRAP_STORES = java.util.Collections.unmodifiableSet( new HashSet<>(Arrays.asList( "account", "account-asset", "delegation", "storage-row"))); @@ -74,6 +75,8 @@ public final class PathStatePhysicalStoreSet implements Closeable { private final PathStateParticipantScope scope; private final Map participants = new LinkedHashMap<>(); private final PhysicalStore superStore; + private final ExecutorService participantWriteExecutor; + private Map reverseJournalIndex; private boolean rootClaimed; private boolean closed; @@ -83,6 +86,7 @@ private PathStatePhysicalStoreSet(PathStatePhysicalStoreManifest manifest, this.manifest = manifest; this.directory = manifest.getDirectory(); this.scope = requireExactScope(scope); + this.participantWriteExecutor = newParticipantWriteExecutor(); try { for (PathStateParticipant participant : scope.getParticipants()) { Path participantDirectory = directory.resolve(STORES_DIRECTORY).resolve(String.format( @@ -335,6 +339,14 @@ private static ExecutorService newBootstrapExecutor(String tier) { }); } + private static ExecutorService newParticipantWriteExecutor() { + return Executors.newFixedThreadPool(MAX_PARALLEL_PARTICIPANT_WRITES, task -> { + Thread thread = new Thread(task, "path-state-physical-participant-write"); + thread.setDaemon(true); + return thread; + }); + } + private static void cancelOutstanding(List> futures) { for (Future future : futures) { if (!future.isDone()) { @@ -491,7 +503,7 @@ public synchronized PathStateRootMetadata currentMetadata() throws IOException { synchronized void verifyReverseJournals(PathStateLayerLimits limits) throws IOException { requireOpen(); - loadReverseJournals(Objects.requireNonNull(limits, "limits")); + reverseJournalIndex = loadReverseJournalIndex(Objects.requireNonNull(limits, "limits")); } /** Computes one exact child target without changing F/N/M, INTENT, or CURRENT. */ @@ -505,19 +517,33 @@ public synchronized PathStateRootMetadata previewTransition(PathStateBlockTransi /** Applies one block-final child to the physical 27+1 stores and publishes its CURRENT. */ public synchronized PathStateRootMetadata applyAndPublish(PathStateBlockTransition transition) throws IOException { - return applyAndPublish(transition, PathStateLayerLimits.defaults(), stage -> { }); + return applyAndPublishInternal(transition, PathStateLayerLimits.defaults(), stage -> { }, + true); + } + + synchronized PathStateRootMetadata applyAndPublish(PathStateBlockTransition transition, + PathStateLayerLimits limits) throws IOException { + return applyAndPublishInternal(transition, limits, stage -> { }, true); } synchronized PathStateRootMetadata applyAndPublish(PathStateBlockTransition transition, TransitionFaultHook faultHook) throws IOException { - return applyAndPublish(transition, PathStateLayerLimits.defaults(), faultHook); + return applyAndPublishInternal(transition, PathStateLayerLimits.defaults(), faultHook, false); } synchronized PathStateRootMetadata applyAndPublish(PathStateBlockTransition transition, PathStateLayerLimits limits, TransitionFaultHook faultHook) throws IOException { + return applyAndPublishInternal(transition, limits, faultHook, false); + } + + private PathStateRootMetadata applyAndPublishInternal(PathStateBlockTransition transition, + PathStateLayerLimits limits, TransitionFaultHook faultHook, boolean parallelParticipants) + throws IOException { requireOpen(); + long startedNanos = System.nanoTime(); recoverPublication(); TransitionPlan plan = prepareTransition(transition); + long preparedNanos = System.nanoTime(); TransitionFaultHook hook = Objects.requireNonNull(faultHook, "faultHook"); byte[] encoded = plan.target.encode(); byte[] encodedJournal = plan.journal.encode(); @@ -525,17 +551,26 @@ synchronized PathStateRootMetadata applyAndPublish(PathStateBlockTransition tran pruneReverseJournals(currentTarget(), Objects.requireNonNull(limits, "limits"), journal, encodedJournal.length); PathStateMetadataFile.publishImmutableBytes(journal, encodedJournal); + rememberReverseJournal(plan.journal, journal, encodedJournal.length); + long journalNanos = System.nanoTime(); hook.after(TransitionStage.AFTER_JOURNAL); Path intent = directory.resolve(INTENT_FILE); Path current = directory.resolve(CURRENT_FILE); PathStateMetadataFile.publishImmutableBytes(intent, encoded); hook.after(TransitionStage.AFTER_INTENT); - for (ParticipantTransition participant : plan.participants) { - participant.store.applyParticipantTransition(participant.flatMutations, - participant.nodeMutations, participant.flatDigest, participant.generation, - participant.storeRoot); - hook.after(TransitionStage.AFTER_PARTICIPANT_BATCH); + long intentNanos = System.nanoTime(); + if (parallelParticipants) { + applyParticipantTransitionsInParallel(plan.participants); + for (int completed = 0; completed < plan.participants.size(); completed++) { + hook.after(TransitionStage.AFTER_PARTICIPANT_BATCH); + } + } else { + for (ParticipantTransition participant : plan.participants) { + applyParticipantTransition(participant); + hook.after(TransitionStage.AFTER_PARTICIPANT_BATCH); + } } + long participantsNanos = System.nanoTime(); plan.superStore.applySuperTransition(plan.superNodeMutations, plan.target.getSuperGeneration(), plan.target.getSuperRoot()); hook.after(TransitionStage.AFTER_SUPER_BATCH); @@ -543,6 +578,16 @@ synchronized PathStateRootMetadata applyAndPublish(PathStateBlockTransition tran hook.after(TransitionStage.AFTER_CURRENT); PathStateMetadataFile.deleteDurable(intent); hook.after(TransitionStage.AFTER_RETIRE); + long completedNanos = System.nanoTime(); + logger.info("Path-state physical transition completed: head={}, changedStores={}, " + + "journalBytes={}, journalCount={}, journalWindowBytes={}, prepareMs={}, " + + "journalMs={}, intentMs={}, participantWaitMs={}, finalizeMs={}, totalMs={}", + plan.target.getMetadata().getBlockNumber(), plan.participants.size(), + encodedJournal.length, reverseJournalCount(), reverseJournalBytes(), + elapsedMillis(startedNanos, preparedNanos), elapsedMillis(preparedNanos, journalNanos), + elapsedMillis(journalNanos, intentNanos), elapsedMillis(intentNanos, participantsNanos), + elapsedMillis(participantsNanos, completedNanos), + elapsedMillis(startedNanos, completedNanos)); return plan.target.getMetadata(); } @@ -651,6 +696,58 @@ private TransitionPlan prepareTransition(PathStateBlockTransition supplied) thro recordings.get(0).mutations(), journal); } + private void applyParticipantTransitionsInParallel(List transitions) + throws IOException { + List writes = new ArrayList<>(); + for (ParticipantTransition participant : transitions) { + writes.add(() -> applyParticipantTransition(participant)); + } + awaitParallelWrites(participantWriteExecutor, writes); + } + + static void awaitParallelWrites(ExecutorService executor, List writes) + throws IOException { + List> futures = new ArrayList<>(); + for (Runnable write : Objects.requireNonNull(writes, "writes")) { + futures.add(Objects.requireNonNull(executor, "executor").submit( + Objects.requireNonNull(write, "write"))); + } + IOException failure = null; + boolean interrupted = false; + for (Future future : futures) { + boolean complete = false; + while (!complete) { + try { + future.get(); + complete = true; + } catch (InterruptedException interruptedFailure) { + interrupted = true; + } catch (ExecutionException writeFailure) { + complete = true; + Throwable cause = writeFailure.getCause(); + IOException participantFailure = cause instanceof IOException + ? (IOException) cause + : new IOException("path-state participant batch failed", cause); + failure = append(failure, participantFailure); + } + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + failure = append(failure, + new IOException("path-state participant batch wait was interrupted")); + } + if (failure != null) { + throw failure; + } + } + + private static void applyParticipantTransition(ParticipantTransition participant) { + participant.store.applyParticipantTransition(participant.flatMutations, + participant.nodeMutations, participant.flatDigest, participant.generation, + participant.storeRoot); + } + private void applyReverseJournal(PathStatePhysicalReverseJournal journal, RewindFaultHook hook) throws IOException { PathStatePhysicalGlobalIntent child = PathStatePhysicalGlobalIntent.decode( @@ -688,16 +785,17 @@ private List loadRewindChain( if (targetNumber < 0 || targetNumber > current.getMetadata().getBlockNumber()) { throw new IOException("physical rewind target height is outside CURRENT ancestry"); } - Map journals = loadReverseJournals(limits); + Map journals = reverseJournalIndex(limits); List chain = new ArrayList<>(); PathStatePhysicalGlobalIntent cursor = current; while (cursor.getMetadata().getBlockNumber() > targetNumber) { - PathStatePhysicalReverseJournal journal = journals.get(new BytesKey(cursor.encode())); - if (journal == null) { + ReverseJournalIndexEntry entry = journals.get(new BytesKey(cursor.encode())); + if (entry == null) { throw new IOException("physical reverse journal ancestry is missing"); } + PathStatePhysicalReverseJournal journal = loadReverseJournal(entry.path); chain.add(journal); - cursor = PathStatePhysicalGlobalIntent.decode(journal.getParentTarget()); + cursor = PathStatePhysicalGlobalIntent.decode(entry.parentTarget); } if (cursor.getMetadata().getBlockNumber() != targetNumber || !Arrays.equals(cursor.getMetadata().getBlockHash(), targetHash)) { @@ -706,9 +804,9 @@ private List loadRewindChain( return chain; } - private Map loadReverseJournals( + private Map loadReverseJournalIndex( PathStateLayerLimits limits) throws IOException { - Map journals = new LinkedHashMap<>(); + Map journals = new LinkedHashMap<>(); Path reverse = directory.resolve(REVERSE_DIRECTORY); if (!Files.exists(reverse, LinkOption.NOFOLLOW_LINKS)) { return journals; @@ -726,15 +824,10 @@ private Map loadReverseJournals( long length = Files.size(file); total = Math.addExact(total, length); count = Math.addExact(count, 1); - PathStatePhysicalReverseJournal journal; - try { - journal = PathStatePhysicalReverseJournal.decode( - PathStateMetadataFile.loadImmutableBytes(file, - PathStatePhysicalReverseJournal.MAX_ENCODED_LENGTH)); - } catch (IllegalArgumentException invalid) { - throw new IOException("physical reverse journal is corrupt: " + file, invalid); - } - if (journals.put(new BytesKey(journal.getChildTarget()), journal) != null) { + PathStatePhysicalReverseJournal journal = loadReverseJournal(file); + ReverseJournalIndexEntry entry = new ReverseJournalIndexEntry(file, length, + journal.getChildTarget(), journal.getParentTarget()); + if (journals.put(new BytesKey(entry.childTarget), entry) != null) { throw new IOException("physical reverse journal child identity is duplicated"); } } @@ -747,6 +840,75 @@ private Map loadReverseJournals( return journals; } + private PathStatePhysicalReverseJournal loadReverseJournal(Path file) throws IOException { + try { + return PathStatePhysicalReverseJournal.decode( + PathStateMetadataFile.loadImmutableBytes(file, + PathStatePhysicalReverseJournal.MAX_ENCODED_LENGTH)); + } catch (IllegalArgumentException invalid) { + throw new IOException("physical reverse journal is corrupt: " + file, invalid); + } + } + + private Map reverseJournalIndex( + PathStateLayerLimits limits) throws IOException { + if (reverseJournalIndex == null) { + reverseJournalIndex = loadReverseJournalIndex(limits); + } else { + requireReverseJournalLimits(reverseJournalIndex, limits); + } + return reverseJournalIndex; + } + + private static void requireReverseJournalLimits( + Map journals, PathStateLayerLimits limits) + throws IOException { + long total = 0; + try { + for (ReverseJournalIndexEntry entry : journals.values()) { + total = Math.addExact(total, entry.length); + } + } catch (ArithmeticException overflow) { + throw new IOException("physical reverse journal usage overflow", overflow); + } + if (journals.size() > limits.getMaxLayers() || total > limits.getMaxLogicalBytes()) { + throw new IOException("physical reverse journal exceeds configured bounds"); + } + } + + private void rememberReverseJournal(PathStatePhysicalReverseJournal journal, Path path, + long length) throws IOException { + Map journals = reverseJournalIndex( + new PathStateLayerLimits(Integer.MAX_VALUE, Long.MAX_VALUE)); + ReverseJournalIndexEntry entry = new ReverseJournalIndexEntry(path, length, + journal.getChildTarget(), journal.getParentTarget()); + BytesKey identity = new BytesKey(entry.childTarget); + ReverseJournalIndexEntry previous = journals.put(identity, entry); + if (previous != null && !previous.path.equals(entry.path)) { + journals.put(identity, previous); + throw new IOException("physical reverse journal child identity is duplicated"); + } + } + + private int reverseJournalCount() { + return reverseJournalIndex == null ? 0 : reverseJournalIndex.size(); + } + + private long reverseJournalBytes() throws IOException { + if (reverseJournalIndex == null) { + return 0; + } + long total = 0; + try { + for (ReverseJournalIndexEntry entry : reverseJournalIndex.values()) { + total = Math.addExact(total, entry.length); + } + return total; + } catch (ArithmeticException overflow) { + throw new IOException("physical reverse journal usage overflow", overflow); + } + } + private void pruneReverseJournals(PathStatePhysicalGlobalIntent current, PathStateLayerLimits limits, Path candidate, long candidateBytes) throws IOException { if (candidateBytes > limits.getMaxLogicalBytes()) { @@ -756,7 +918,7 @@ private void pruneReverseJournals(PathStatePhysicalGlobalIntent current, if (!Files.exists(reverse, LinkOption.NOFOLLOW_LINKS)) { return; } - Map decoded = loadReverseJournals( + Map indexed = reverseJournalIndex( new PathStateLayerLimits(Integer.MAX_VALUE, Long.MAX_VALUE)); Set keep = new HashSet<>(); byte[] cursor = current.encode(); @@ -764,32 +926,26 @@ private void pruneReverseJournals(PathStatePhysicalGlobalIntent current, long remainingBytes = limits.getMaxLogicalBytes() - candidateBytes; while (remainingCount > 0) { BytesKey identity = new BytesKey(cursor); - PathStatePhysicalReverseJournal journal = decoded.get(identity); - if (journal == null) { + ReverseJournalIndexEntry entry = indexed.get(identity); + if (entry == null) { break; } - Path path = reverseJournalPath(PathStatePhysicalGlobalIntent.decode( - journal.getChildTarget()).getMetadata()); - long length = Files.size(path); - if (length > remainingBytes) { + if (entry.length > remainingBytes) { break; } keep.add(identity); remainingCount--; - remainingBytes -= length; - cursor = journal.getParentTarget(); - } - try (Stream files = Files.list(reverse)) { - for (Path file : (Iterable) files::iterator) { - if (file.equals(candidate)) { - continue; - } - PathStatePhysicalReverseJournal journal = PathStatePhysicalReverseJournal.decode( - PathStateMetadataFile.loadImmutableBytes(file, - PathStatePhysicalReverseJournal.MAX_ENCODED_LENGTH)); - if (!keep.contains(new BytesKey(journal.getChildTarget()))) { - PathStateMetadataFile.deleteDurable(file); - } + remainingBytes -= entry.length; + cursor = entry.parentTarget; + } + java.util.Iterator> iterator = + indexed.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry indexedJournal = iterator.next(); + ReverseJournalIndexEntry entry = indexedJournal.getValue(); + if (!entry.path.equals(candidate) && !keep.contains(indexedJournal.getKey())) { + PathStateMetadataFile.deleteDurable(entry.path); + iterator.remove(); } } } @@ -1150,6 +1306,7 @@ public synchronized void close() throws IOException { return; } closed = true; + participantWriteExecutor.shutdownNow(); IOException failure = null; for (PhysicalStore store : participants.values()) { try { @@ -1169,6 +1326,7 @@ public synchronized void close() throws IOException { } private void closeAfterFailure(Throwable original) { + participantWriteExecutor.shutdownNow(); for (PhysicalStore store : participants.values()) { try { store.close(); @@ -1245,6 +1403,10 @@ private static long elapsedMillis(long startedNanos) { System.nanoTime() - startedNanos); } + private static long elapsedMillis(long startedNanos, long completedNanos) { + return java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(completedNanos - startedNanos); + } + private static long rowsPerSecond(long rows, long startedNanos) { long elapsedNanos = Math.max(1L, System.nanoTime() - startedNanos); return (long) (rows * 1_000_000_000D / elapsedNanos); @@ -1303,6 +1465,14 @@ long getWriteBatchMutations() { return nativeStore.getWriteBatchMutations(); } + long getSyncedWriteBatchCalls() { + return nativeStore.getSyncedWriteBatchCalls(); + } + + long getUnsyncedWriteBatchCalls() { + return nativeStore.getUnsyncedWriteBatchCalls(); + } + void clearNodes() throws IOException { List pending = new ArrayList<>(4096); nativeStore.scanPrefix(new byte[]{NODE_PREFIX}, entry -> { @@ -1326,7 +1496,7 @@ void applyParticipantDelete(byte[] secureKey, List nodeMutations, mutations.add(metadataMutation(FLAT_DIGEST_METADATA, flatDigest)); mutations.add(metadataMutation(STORE_GENERATION_METADATA, generation)); mutations.add(metadataMutation(FLAT_COMPLETE_METADATA, storeRoot)); - nativeStore.writeBatch(mutations); + nativeStore.writeBatchUnsynced(mutations); } void applyParticipantTransition(List flatMutations, @@ -1343,7 +1513,7 @@ void applyParticipantTransition(List flatMutations, mutations.add(metadataMutation(FLAT_DIGEST_METADATA, flatDigest)); mutations.add(metadataMutation(STORE_GENERATION_METADATA, generation)); mutations.add(metadataMutation(FLAT_COMPLETE_METADATA, storeRoot)); - nativeStore.writeBatch(mutations); + nativeStore.writeBatchUnsynced(mutations); } void applySuperTransition(List nodeMutations, byte[] generation, @@ -1352,7 +1522,7 @@ void applySuperTransition(List nodeMutations, byte[] generation, appendNodeMutations(mutations, nodeMutations); mutations.add(metadataMutation(SUPER_GENERATION_METADATA, generation)); mutations.add(metadataMutation(FLAT_ROOT_METADATA, superRoot)); - nativeStore.writeBatch(mutations); + nativeStore.writeBatchUnsynced(mutations); } private static void appendNodeMutations( @@ -1627,6 +1797,27 @@ public int hashCode() { } } + private static final class ReverseJournalIndexEntry { + + private final Path path; + private final long length; + private final byte[] childTarget; + private final byte[] parentTarget; + + private ReverseJournalIndexEntry(Path path, long length, byte[] childTarget, + byte[] parentTarget) { + this.path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); + if (length <= 0) { + throw new IllegalArgumentException("reverse journal length must be positive"); + } + this.length = length; + this.childTarget = Arrays.copyOf(Objects.requireNonNull(childTarget, "childTarget"), + childTarget.length); + this.parentTarget = Arrays.copyOf(Objects.requireNonNull(parentTarget, "parentTarget"), + parentTarget.length); + } + } + @FunctionalInterface interface BuildFaultHook { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index 33d72744017..8b2c699728d 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -16,6 +16,11 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import org.junit.Rule; @@ -482,9 +487,15 @@ public void physicalBlockFinalTransitionPreviewsPublishesAndRestarts() throws Ex Collections.singletonList(PathStateMutation.put("code", key, new byte[]{4, 5}))); PathStateRootMetadata preview = stores.previewTransition(transition); assertEquals(0, stores.currentMetadata().getBlockNumber()); + assertEquals(0, stores.participant("code").getSyncedWriteBatchCalls()); + assertEquals(0, stores.participant("code").getUnsyncedWriteBatchCalls()); PathStateRootMetadata committed = stores.applyAndPublish(transition); assertArrayEquals(preview.encode(), committed.encode()); assertEquals(1, committed.getBlockNumber()); + assertEquals(0, stores.participant("code").getSyncedWriteBatchCalls()); + assertEquals(1, stores.participant("code").getUnsyncedWriteBatchCalls()); + assertEquals(0, stores.superStore().getSyncedWriteBatchCalls()); + assertEquals(1, stores.superStore().getUnsyncedWriteBatchCalls()); assertArrayEquals(PathStateCommitmentCodec.presentLeafValue(new byte[]{4, 5}), stores.participant("code").getFlat( PathStateCommitmentCodec.storeLeafKey(scope.require("code").getStoreId(), key))); @@ -692,6 +703,69 @@ public void physicalStartupRejectsCorruptReverseJournal() throws Exception { () -> PathStatePhysicalSnapshotHead.open(root, Engine.ROCKSDB)); } + @Test + public void physicalSteadyTransitionDoesNotRereadIndexedReverseJournal() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-reverse-runtime-index").toPath(); + preparePublishedPhysicalTarget(root, scope); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + stores.applyAndPublish(new PathStateBlockTransition(1, bytes(92), new byte[32], 3, + P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{2})))); + Path reverse; + try (Stream files = Files.list(root.resolve("reverse"))) { + reverse = files.findFirst().get(); + } + byte[] corrupt = Files.readAllBytes(reverse); + corrupt[corrupt.length - 1] ^= 1; + Files.write(reverse, corrupt); + + assertEquals(2, stores.applyAndPublish(new PathStateBlockTransition(2, bytes(93), bytes(92), + 6, P66Phase.P66_ON, Collections.emptyList())).getBlockNumber()); + } + assertThrows(java.io.IOException.class, + () -> PathStatePhysicalSnapshotHead.open(root, Engine.ROCKSDB)); + } + + @Test + public void parallelParticipantWritesStartTogetherAndWaitForEveryCompletion() throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + CountDownLatch started = new CountDownLatch(2); + CountDownLatch release = new CountDownLatch(1); + AtomicBoolean bothStarted = new AtomicBoolean(); + List concurrentWrites = Arrays.asList( + () -> awaitTestLatch(started, release), + () -> awaitTestLatch(started, release)); + Thread releaser = new Thread(() -> { + try { + bothStarted.set(started.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + } finally { + release.countDown(); + } + }); + releaser.start(); + PathStatePhysicalStoreSet.awaitParallelWrites(executor, concurrentWrites); + releaser.join(); + assertTrue(bothStarted.get()); + assertEquals(0, started.getCount()); + + AtomicBoolean secondCompleted = new AtomicBoolean(); + assertThrows(java.io.IOException.class, + () -> PathStatePhysicalStoreSet.awaitParallelWrites(executor, Arrays.asList( + () -> { + throw new IllegalStateException("injected participant failure"); + }, + () -> secondCompleted.set(true)))); + assertTrue(secondCompleted.get()); + } finally { + executor.shutdownNow(); + } + } + @Test public void physicalGlobalPublicationAcceptsOnlyOldCurrentOrExactIntentTarget() throws Exception { @@ -1332,4 +1406,16 @@ private static int compareUnsigned(byte[] left, byte[] right) { } return Integer.compare(left.length, right.length); } + + private static void awaitTestLatch(CountDownLatch started, CountDownLatch release) { + started.countDown(); + try { + if (!release.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("timed out waiting to release participant write"); + } + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("participant write test interrupted", failure); + } + } } From 07215f27484b7539b89cc8b84a3e94e802be37e5 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 2 Sep 2026 09:28:12 +0800 Subject: [PATCH 105/161] perf(trie): cache transition node reads --- .../stateroot/PathStatePhysicalStoreSet.java | 50 ++++++++++++++++--- .../PathStateNativeNodeStoreTest.java | 41 +++++++++++++++ 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java index a232faae389..af2dec97f45 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java @@ -580,10 +580,12 @@ private PathStateRootMetadata applyAndPublishInternal(PathStateBlockTransition t hook.after(TransitionStage.AFTER_RETIRE); long completedNanos = System.nanoTime(); logger.info("Path-state physical transition completed: head={}, changedStores={}, " - + "journalBytes={}, journalCount={}, journalWindowBytes={}, prepareMs={}, " - + "journalMs={}, intentMs={}, participantWaitMs={}, finalizeMs={}, totalMs={}", + + "journalBytes={}, journalCount={}, journalWindowBytes={}, nodeReadMisses={}, " + + "nodeReadHits={}, prepareMs={}, journalMs={}, intentMs={}, participantWaitMs={}, " + + "finalizeMs={}, totalMs={}", plan.target.getMetadata().getBlockNumber(), plan.participants.size(), encodedJournal.length, reverseJournalCount(), reverseJournalBytes(), + plan.nodeReadMisses, plan.nodeReadHits, elapsedMillis(startedNanos, preparedNanos), elapsedMillis(preparedNanos, journalNanos), elapsedMillis(journalNanos, intentNanos), elapsedMillis(intentNanos, participantsNanos), elapsedMillis(participantsNanos, completedNanos), @@ -692,8 +694,12 @@ private TransitionPlan prepareTransition(PathStateBlockTransition supplied) thro manifest.getIdentityDigest(), metadata, targets, superGeneration, stateRoot); PathStatePhysicalReverseJournal journal = new PathStatePhysicalReverseJournal(target.encode(), current.encode(), reverseStores, recordings.get(0).reverseEntries()); + long nodeReadMisses = recordings.values().stream() + .mapToLong(RecordingNodeStore::getBaseReadMisses).sum(); + long nodeReadHits = recordings.values().stream() + .mapToLong(RecordingNodeStore::getBaseReadHits).sum(); return new TransitionPlan(target, participantTransitions, superStore, - recordings.get(0).mutations(), journal); + recordings.get(0).mutations(), journal, nodeReadMisses, nodeReadHits); } private void applyParticipantTransitionsInParallel(List transitions) @@ -1647,12 +1653,15 @@ private void flush() { } } - private static final class RecordingNodeStore implements PathNodeStore { + static final class RecordingNodeStore implements PathNodeStore { private final PathNodeStore base; private final Map changes = new LinkedHashMap<>(); + private final Map baseReads = new LinkedHashMap<>(); + private long baseReadMisses; + private long baseReadHits; - private RecordingNodeStore(PathNodeStore base) { + RecordingNodeStore(PathNodeStore base) { this.base = Objects.requireNonNull(base, "base"); } @@ -1663,7 +1672,7 @@ public byte[] get(byte[] path) { byte[] value = changes.get(key); return value == null ? null : Arrays.copyOf(value, value.length); } - return base.get(path); + return baseValue(key); } @Override @@ -1692,11 +1701,31 @@ private List mutations() { private List reverseEntries() { List entries = new ArrayList<>(); for (BytesKey key : changes.keySet()) { - entries.add(new PathStatePhysicalReverseJournal.Entry(key.bytes, base.get(key.bytes))); + entries.add(new PathStatePhysicalReverseJournal.Entry(key.bytes, baseValue(key))); } return entries; } + private byte[] baseValue(BytesKey key) { + if (!baseReads.containsKey(key)) { + byte[] value = base.get(key.bytes); + baseReads.put(key, value == null ? null : Arrays.copyOf(value, value.length)); + baseReadMisses++; + } else { + baseReadHits++; + } + byte[] value = baseReads.get(key); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + long getBaseReadMisses() { + return baseReadMisses; + } + + long getBaseReadHits() { + return baseReadHits; + } + private int putCount() { int count = 0; for (byte[] value : changes.values()) { @@ -1765,15 +1794,20 @@ private static final class TransitionPlan { private final PhysicalStore superStore; private final List superNodeMutations; private final PathStatePhysicalReverseJournal journal; + private final long nodeReadMisses; + private final long nodeReadHits; private TransitionPlan(PathStatePhysicalGlobalIntent target, List participants, PhysicalStore superStore, - List superNodeMutations, PathStatePhysicalReverseJournal journal) { + List superNodeMutations, PathStatePhysicalReverseJournal journal, + long nodeReadMisses, long nodeReadHits) { this.target = target; this.participants = participants; this.superStore = superStore; this.superNodeMutations = superNodeMutations; this.journal = journal; + this.nodeReadMisses = nodeReadMisses; + this.nodeReadHits = nodeReadHits; } } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index 8b2c699728d..feb9e09bffd 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -73,6 +73,47 @@ public void nativeStoreRejectsInvalidEntriesAndUseAfterClose() throws Exception assertThrows(IllegalStateException.class, () -> store.get(new byte[0])); } + @Test + public void transitionRecordingStoreCachesBaseReadsAndOwnsReturnedBytes() { + AtomicInteger reads = new AtomicInteger(); + PathNodeStore base = new PathNodeStore() { + @Override + public byte[] get(byte[] path) { + reads.incrementAndGet(); + return path[0] == 1 ? new byte[]{4, 5, 6} : null; + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + throw new UnsupportedOperationException(); + } + + @Override + public void delete(byte[] path) { + throw new UnsupportedOperationException(); + } + }; + PathStatePhysicalStoreSet.RecordingNodeStore store = + new PathStatePhysicalStoreSet.RecordingNodeStore(base); + + byte[] first = store.get(new byte[]{1}); + first[0] = 9; + assertArrayEquals(new byte[]{4, 5, 6}, store.get(new byte[]{1})); + assertNull(store.get(new byte[]{2})); + assertNull(store.get(new byte[]{2})); + assertEquals(2, reads.get()); + assertEquals(2, store.getBaseReadMisses()); + assertEquals(2, store.getBaseReadHits()); + + store.put(new byte[]{1}, new byte[]{7, 8}); + byte[] changed = store.get(new byte[]{1}); + changed[0] = 9; + assertArrayEquals(new byte[]{7, 8}, store.get(new byte[]{1})); + assertEquals(2, reads.get()); + assertEquals(2, store.getBaseReadMisses()); + assertEquals(2, store.getBaseReadHits()); + } + @Test public void streamsNativeScansWithoutCollectingTheResultSet() throws Exception { for (Engine engine : availableEngines()) { From 92ba6c46443be485f747439606d730b96f868abf Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 2 Sep 2026 10:20:31 +0800 Subject: [PATCH 106/161] perf(trie): parallelize path state updates --- .../core/db2/stateroot/PathMerkleTrie.java | 275 +++++++++++++- .../stateroot/PathStatePhysicalStoreSet.java | 346 ++++++++++++++++-- .../core/db2/stateroot/PathStateRoot.java | 67 ++++ .../PathStateNativeNodeStoreTest.java | 96 +++++ .../core/db2/stateroot/PathStateRootTest.java | 96 +++++ 5 files changed, 842 insertions(+), 38 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index 284befd19e9..802849d2fe6 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -11,6 +11,10 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; import org.tron.common.crypto.Hash; /** Backend-neutral secure-key MPT with path-local immutable node updates. */ @@ -20,6 +24,7 @@ public final class PathMerkleTrie { private static final byte[] EMPTY_PATH = new byte[0]; private static final byte[] EMPTY_RLP_ITEM = new byte[]{(byte) 0x80}; + private static final int PARALLEL_UPDATE_THRESHOLD = 4; private static final Comparator UNSIGNED_KEY_COMPARATOR = (left, right) -> { int length = Math.min(left.bytes.length, right.bytes.length); for (int i = 0; i < length; i++) { @@ -43,6 +48,8 @@ public final class PathMerkleTrie { private boolean frozen; private int lastNodePuts; private int lastNodeDeletes; + private final AtomicLong nodeDecodeCount = new AtomicLong(); + private final AtomicLong nodeHashVerifyCount = new AtomicLong(); public PathMerkleTrie(PathNodeStore nodeStore) { this.nodeStore = Objects.requireNonNull(nodeStore, "nodeStore"); @@ -75,6 +82,136 @@ public synchronized void delete(byte[] secureKey) { } } + synchronized void applyBatch(List mutations, ExecutorService executor) { + requireMutable(); + List batch = new ArrayList<>(Objects.requireNonNull(mutations, "mutations")); + Node resolvedRoot = resolve(rootNode, EMPTY_PATH); + rootNode = resolvedRoot; + if (batch.size() < PARALLEL_UPDATE_THRESHOLD || !(resolvedRoot instanceof BranchNode)) { + applySequential(batch); + return; + } + BranchNode root = (BranchNode) resolvedRoot; + List> groups = new ArrayList<>(16); + boolean[] deletes = new boolean[16]; + for (int i = 0; i < 16; i++) { + groups.add(new ArrayList<>()); + } + for (BatchMutation mutation : batch) { + BatchMutation present = Objects.requireNonNull(mutation, "mutation"); + int nibble = (present.secureKey[0] >>> 4) & 0x0f; + groups.get(nibble).add(present); + deletes[nibble] |= present.encodedValue == null; + } + int guaranteedSurvivors = 0; + for (int i = 0; i < root.children.length; i++) { + if (root.children[i] != null && !deletes[i]) { + guaranteedSurvivors++; + } + } + if (guaranteedSurvivors < 2) { + applySequential(batch); + return; + } + List> futures = new ArrayList<>(); + List positions = new ArrayList<>(); + for (int i = 0; i < groups.size(); i++) { + if (!groups.get(i).isEmpty()) { + final int position = i; + positions.add(position); + futures.add(Objects.requireNonNull(executor, "executor").submit( + () -> applySubtree(root.children[position], position, groups.get(position)))); + } + } + Node[] children = Arrays.copyOf(root.children, root.children.length); + Map previous = new LinkedHashMap<>(); + Map changed = new LinkedHashMap<>(); + try { + for (int i = 0; i < futures.size(); i++) { + SubtreeResult result = futures.get(i).get(); + children[positions.get(i)] = result.node; + previous.putAll(result.previous); + changed.putAll(result.changed); + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + cancel(futures); + throw new IllegalStateException("path trie batch update interrupted", interrupted); + } catch (ExecutionException failed) { + cancel(futures); + Throwable cause = failed.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + throw new IllegalStateException("path trie batch update failed", cause); + } + if (changed.isEmpty()) { + return; + } + for (BatchMutation mutation : batch) { + BytesKey key = new BytesKey(mutation.secureKey); + if (!changed.containsKey(key)) { + continue; + } + byte[] oldValue = previous.get(key); + leaves.put(key, mutation.encodedValue); + if (oldValue == null && mutation.encodedValue != null) { + leafCount++; + } else if (oldValue != null && mutation.encodedValue == null) { + leafCount--; + } + } + rootNode = new BranchNode(children); + dirty = true; + } + + private void applySequential(List mutations) { + for (BatchMutation mutation : mutations) { + if (mutation.encodedValue == null) { + delete(mutation.secureKey); + } else { + put(mutation.secureKey, mutation.encodedValue); + } + } + } + + private SubtreeResult applySubtree(Node initial, int nibble, + List mutations) { + Node node = initial; + byte[] path = new byte[]{(byte) nibble}; + Map previous = new LinkedHashMap<>(); + Map changed = new LinkedHashMap<>(); + for (BatchMutation mutation : mutations) { + BytesKey key = new BytesKey(mutation.secureKey); + byte[] oldValue; + if (leaves.containsKey(key)) { + oldValue = leaves.get(key); + } else if (inheritedSnapshot != null && inheritedSnapshot.containsLeaf(key)) { + oldValue = inheritedSnapshot.leafValue(key); + } else { + byte[] nibbles = toNibbles(key.bytes); + ValueResult result = resolvedValueAt(node, nibbles, 1, path); + node = result.node; + oldValue = result.value; + } + previous.put(key, oldValue); + if (Arrays.equals(oldValue, mutation.encodedValue)) { + continue; + } + node = update(node, toNibbles(key.bytes), 1, path, mutation.encodedValue); + changed.put(key, mutation.encodedValue); + } + return new SubtreeResult(node, previous, changed); + } + + private static void cancel(List> futures) { + for (Future future : futures) { + if (!future.isDone()) { + future.cancel(true); + } + } + } + public synchronized byte[] get(byte[] secureKey) { byte[] value = leafValue(secureKey(secureKey)); return value == null ? null : Arrays.copyOf(value, value.length); @@ -100,6 +237,14 @@ synchronized int getLastNodeDeletes() { return lastNodeDeletes; } + synchronized long getNodeDecodeCount() { + return nodeDecodeCount.get(); + } + + synchronized long getNodeHashVerifyCount() { + return nodeHashVerifyCount.get(); + } + synchronized List leafEntries() { Map effective = effectiveLeaves(); List entries = new ArrayList<>(effective.size()); @@ -166,22 +311,32 @@ synchronized void restoreRoot(byte[] expectedRoot) { return; } byte[] encoded = nodeStore.get(EMPTY_PATH); + nodeHashVerifyCount.incrementAndGet(); if (encoded == null || !Arrays.equals(Hash.sha3(encoded), expected)) { throw new IllegalStateException("durable path trie root is missing or corrupt"); } - rootNode = new StoredNode(encoded, EMPTY_PATH); - materializedRoot = rootNode; - materializedNodes.put(rootNode, new BytesKey(EMPTY_PATH)); - rootHash = expected; + installStoredRoot(encoded, expected); } synchronized byte[] restoreRoot() { byte[] encoded = nodeStore.get(EMPTY_PATH); - byte[] expected = encoded == null ? Hash.EMPTY_TRIE_HASH : Hash.sha3(encoded); - restoreRoot(expected); + if (encoded == null) { + rootHash = Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length); + return Arrays.copyOf(rootHash, rootHash.length); + } + nodeHashVerifyCount.incrementAndGet(); + byte[] expected = Hash.sha3(encoded); + installStoredRoot(encoded, expected); return Arrays.copyOf(expected, expected.length); } + private void installStoredRoot(byte[] encoded, byte[] expected) { + rootNode = new StoredNode(encoded, EMPTY_PATH); + materializedRoot = rootNode; + rememberMaterialized(rootNode, EMPTY_PATH); + rootHash = Arrays.copyOf(expected, expected.length); + } + private void importLeaves(Collection entries, String operation) { if (!leaves.isEmpty() || inheritedSnapshot != null || rootNode != null || materializedRoot != null || dirty) { @@ -299,11 +454,31 @@ private Map effectiveLeaves() { } private BytesKey materializedPath(Node node) { - BytesKey path = materializedNodes.get(node); + BytesKey path; + synchronized (materializedNodes) { + path = materializedNodes.get(node); + } return path != null || inheritedSnapshot == null ? path : inheritedSnapshot.materializedPath(node); } + private void rememberMaterialized(Node node, byte[] path) { + synchronized (materializedNodes) { + materializedNodes.put(node, new BytesKey(path)); + } + } + + private Node retainResolvedReplacement(Node original, Node replacement, byte[] path) { + BytesKey materialized = materializedPath(original); + if (materialized != null) { + if (!Arrays.equals(materialized.bytes, path)) { + throw new IllegalStateException("resolved path trie node moved from its durable path"); + } + rememberMaterialized(replacement, path); + } + return replacement; + } + private void requireMutable() { if (frozen) { throw new IllegalStateException("path trie is frozen as an immutable parent snapshot"); @@ -541,30 +716,55 @@ private static int commonPrefix(byte[] left, int leftOffset, byte[] right, int r } private byte[] valueAt(Node node, byte[] key, int offset, byte[] path) { + ValueResult result = resolvedValueAt(node, key, offset, path); + if (path.length == 0) { + rootNode = result.node; + } + return result.value; + } + + private ValueResult resolvedValueAt(Node node, byte[] key, int offset, byte[] path) { if (node == null) { - return null; + return new ValueResult(null, null); } Node present = resolve(node, path); if (present instanceof LeafNode) { LeafNode leaf = (LeafNode) present; int remaining = key.length - offset; - return remaining == leaf.path.length + byte[] value = remaining == leaf.path.length && commonPrefix(leaf.path, 0, key, offset) == remaining ? Arrays.copyOf(leaf.value, leaf.value.length) : null; + return new ValueResult(value, present); } if (present instanceof ExtensionNode) { ExtensionNode extension = (ExtensionNode) present; int shared = commonPrefix(extension.path, 0, key, offset); - return shared == extension.path.length - ? valueAt(extension.child, key, offset + shared, append(path, extension.path)) : null; + if (shared != extension.path.length) { + return new ValueResult(null, present); + } + ValueResult child = resolvedValueAt(extension.child, key, offset + shared, + append(path, extension.path)); + if (child.node == extension.child) { + return new ValueResult(child.value, present); + } + Node replacement = retainResolvedReplacement(present, + new ExtensionNode(extension.path, child.node), path); + return new ValueResult(child.value, replacement); } BranchNode branch = (BranchNode) present; if (offset >= key.length) { - return null; + return new ValueResult(null, present); } int nibble = key[offset]; - return valueAt(branch.children[nibble], key, offset + 1, + ValueResult child = resolvedValueAt(branch.children[nibble], key, offset + 1, append(path, new byte[]{(byte) nibble})); + if (child.node == branch.children[nibble]) { + return new ValueResult(child.value, present); + } + Node[] children = Arrays.copyOf(branch.children, branch.children.length); + children[nibble] = child.node; + Node replacement = retainResolvedReplacement(present, new BranchNode(children), path); + return new ValueResult(child.value, replacement); } private Node resolve(Node node, byte[] expectedPath) { @@ -572,6 +772,7 @@ private Node resolve(Node node, byte[] expectedPath) { return node; } StoredNode stored = (StoredNode) node; + nodeDecodeCount.incrementAndGet(); byte[] storedEncoding = node.encoded; if (!Arrays.equals(stored.path, expectedPath)) { throw new IllegalStateException("stored path trie node moved from its durable path"); @@ -608,7 +809,7 @@ private Node resolve(Node node, byte[] expectedPath) { if (!Arrays.equals(decoded.encoded, storedEncoding)) { throw new IllegalStateException("path-state durable node is not canonically encoded"); } - return decoded; + return retainResolvedReplacement(stored, decoded, expectedPath); } private Node requiredStoredChild(RlpElement reference, byte[] path) { @@ -624,16 +825,21 @@ private Node storedChild(RlpElement reference, byte[] path) { return null; } if (reference.list) { - return new StoredNode(reference.encoded, path); + Node stored = new StoredNode(reference.encoded, path); + rememberMaterialized(stored, path); + return stored; } if (reference.payload.length != SECURE_KEY_LENGTH) { throw new IllegalStateException("path-state child hash must contain exactly 32 bytes"); } byte[] encoded = nodeStore.get(path); + nodeHashVerifyCount.incrementAndGet(); if (encoded == null || !Arrays.equals(Hash.sha3(encoded), reference.payload)) { throw new IllegalStateException("path-state durable child is missing or corrupt"); } - return new StoredNode(encoded, path); + Node stored = new StoredNode(encoded, path); + rememberMaterialized(stored, path); + return stored; } private static List decodeList(byte[] encoded) { @@ -902,6 +1108,43 @@ private NodePath(Node node, byte[] path) { } } + private static final class ValueResult { + + private final byte[] value; + private final Node node; + + private ValueResult(byte[] value, Node node) { + this.value = value; + this.node = node; + } + } + + static final class BatchMutation { + + private final byte[] secureKey; + private final byte[] encodedValue; + + BatchMutation(byte[] secureKey, byte[] encodedValue) { + this.secureKey = PathMerkleTrie.secureKey(secureKey).copy(); + this.encodedValue = encodedValue == null ? null + : nonEmpty(encodedValue, "encodedValue"); + } + } + + private static final class SubtreeResult { + + private final Node node; + private final Map previous; + private final Map changed; + + private SubtreeResult(Node node, Map previous, + Map changed) { + this.node = node; + this.previous = previous; + this.changed = changed; + } + } + private static final class RlpElement { private final boolean list; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java index af2dec97f45..0899ff63855 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java @@ -16,10 +16,12 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +43,10 @@ public final class PathStatePhysicalStoreSet implements Closeable { static final long DEFAULT_CHECKPOINT_BYTES = 256L * 1024 * 1024; static final int BOOTSTRAP_WRITE_BATCH_ENTRIES = 4096; static final long BOOTSTRAP_WRITE_BATCH_BYTES = 8L * 1024 * 1024; + static final long STEADY_NODE_CACHE_BYTES = 256L * 1024 * 1024; private static final int MAX_PARALLEL_PARTICIPANT_WRITES = 4; + private static final int MAX_PARALLEL_PARTICIPANT_PREPARES = 4; + private static final int MAX_PARALLEL_TRIE_BRANCHES = 8; private static final Set LARGE_BOOTSTRAP_STORES = java.util.Collections.unmodifiableSet( new HashSet<>(Arrays.asList( "account", "account-asset", "delegation", "storage-row"))); @@ -76,6 +81,9 @@ public final class PathStatePhysicalStoreSet implements Closeable { private final Map participants = new LinkedHashMap<>(); private final PhysicalStore superStore; private final ExecutorService participantWriteExecutor; + private final ExecutorService participantPrepareExecutor; + private final ExecutorService trieBranchExecutor; + private final ResidentNodeCache residentNodeCache; private Map reverseJournalIndex; private boolean rootClaimed; private boolean closed; @@ -87,15 +95,19 @@ private PathStatePhysicalStoreSet(PathStatePhysicalStoreManifest manifest, this.directory = manifest.getDirectory(); this.scope = requireExactScope(scope); this.participantWriteExecutor = newParticipantWriteExecutor(); + this.participantPrepareExecutor = newTrieExecutor("participant-prepare", + MAX_PARALLEL_PARTICIPANT_PREPARES); + this.trieBranchExecutor = newTrieExecutor("branch-prepare", MAX_PARALLEL_TRIE_BRANCHES); + this.residentNodeCache = new ResidentNodeCache(STEADY_NODE_CACHE_BYTES); try { for (PathStateParticipant participant : scope.getParticipants()) { Path participantDirectory = directory.resolve(STORES_DIRECTORY).resolve(String.format( "%02d-%s", participant.getStoreId(), participant.getDbName())).resolve(NODES_DIRECTORY); participants.put(participant.getDbName(), new PhysicalStore(participantDirectory, - manifest.getEngine())); + manifest.getEngine(), participant.getStoreId(), residentNodeCache)); } superStore = new PhysicalStore(directory.resolve(SUPER_DIRECTORY).resolve(NODES_DIRECTORY), - manifest.getEngine()); + manifest.getEngine(), 0, residentNodeCache); } catch (IOException | RuntimeException failure) { closeAfterFailure(failure); throw failure; @@ -347,6 +359,14 @@ private static ExecutorService newParticipantWriteExecutor() { }); } + private static ExecutorService newTrieExecutor(String name, int threads) { + return Executors.newFixedThreadPool(threads, task -> { + Thread thread = new Thread(task, "path-state-physical-" + name); + thread.setDaemon(true); + return thread; + }); + } + private static void cancelOutstanding(List> futures) { for (Future future : futures) { if (!future.isDone()) { @@ -581,11 +601,17 @@ private PathStateRootMetadata applyAndPublishInternal(PathStateBlockTransition t long completedNanos = System.nanoTime(); logger.info("Path-state physical transition completed: head={}, changedStores={}, " + "journalBytes={}, journalCount={}, journalWindowBytes={}, nodeReadMisses={}, " - + "nodeReadHits={}, prepareMs={}, journalMs={}, intentMs={}, participantWaitMs={}, " - + "finalizeMs={}, totalMs={}", + + "nodeReadHits={}, residentNodeHits={}, residentCleanHits={}, " + + "residentUpdatedHits={}, nativeNodeReads={}, residentCacheBytes={}, " + + "residentEvictions={}, nodeDecodes={}, nodeHashVerifies={}, prepareMs={}, " + + "journalMs={}, intentMs={}, participantWaitMs={}, finalizeMs={}, totalMs={}", plan.target.getMetadata().getBlockNumber(), plan.participants.size(), encodedJournal.length, reverseJournalCount(), reverseJournalBytes(), - plan.nodeReadMisses, plan.nodeReadHits, + plan.nodeReadMisses, plan.nodeReadHits, plan.residentNodeHits, + plan.residentCleanHits, plan.residentUpdatedHits, plan.nativeNodeReads, + residentNodeCache.bytes(), residentNodeCache.evictions() - plan.initialResidentEvictions, + plan.nodeDecodes, + plan.nodeHashVerifies, elapsedMillis(startedNanos, preparedNanos), elapsedMillis(preparedNanos, journalNanos), elapsedMillis(journalNanos, intentNanos), elapsedMillis(intentNanos, participantsNanos), elapsedMillis(participantsNanos, completedNanos), @@ -623,6 +649,7 @@ synchronized PathStateRootMetadata rewindTo(long blockNumber, byte[] blockHash, } private TransitionPlan prepareTransition(PathStateBlockTransition supplied) throws IOException { + long initialResidentEvictions = residentNodeCache.evictions(); PathStateBlockTransition transition = Objects.requireNonNull(supplied, "transition"); PathStatePhysicalGlobalIntent current = currentTarget(); PathStateRootMetadata parent = current.getMetadata(); @@ -640,7 +667,8 @@ private TransitionPlan prepareTransition(PathStateBlockTransition supplied) thro candidate.restoreStoredRoots(current.getSuperRoot()); requireParticipantRoots(candidate, current); if (!transition.getMutations().isEmpty()) { - candidate.apply(transition.getMutations()); + candidate.applyParallel(transition.getMutations(), participantPrepareExecutor, + trieBranchExecutor); } byte[] stateRoot = candidate.rootHash(); @@ -698,8 +726,19 @@ private TransitionPlan prepareTransition(PathStateBlockTransition supplied) thro .mapToLong(RecordingNodeStore::getBaseReadMisses).sum(); long nodeReadHits = recordings.values().stream() .mapToLong(RecordingNodeStore::getBaseReadHits).sum(); + long residentNodeHits = recordings.values().stream() + .mapToLong(RecordingNodeStore::getResidentReadHits).sum(); + long nativeNodeReads = recordings.values().stream() + .mapToLong(RecordingNodeStore::getNativeReads).sum(); + long residentCleanHits = recordings.values().stream() + .mapToLong(RecordingNodeStore::getResidentCleanHits).sum(); + long residentUpdatedHits = recordings.values().stream() + .mapToLong(RecordingNodeStore::getResidentUpdatedHits).sum(); return new TransitionPlan(target, participantTransitions, superStore, - recordings.get(0).mutations(), journal, nodeReadMisses, nodeReadHits); + recordings.get(0).mutations(), journal, nodeReadMisses, nodeReadHits, + residentNodeHits, residentCleanHits, residentUpdatedHits, nativeNodeReads, + initialResidentEvictions, candidate.nodeDecodeCount(), + candidate.nodeHashVerifyCount()); } private void applyParticipantTransitionsInParallel(List transitions) @@ -1313,6 +1352,8 @@ public synchronized void close() throws IOException { } closed = true; participantWriteExecutor.shutdownNow(); + participantPrepareExecutor.shutdownNow(); + trieBranchExecutor.shutdownNow(); IOException failure = null; for (PhysicalStore store : participants.values()) { try { @@ -1333,6 +1374,8 @@ public synchronized void close() throws IOException { private void closeAfterFailure(Throwable original) { participantWriteExecutor.shutdownNow(); + participantPrepareExecutor.shutdownNow(); + trieBranchExecutor.shutdownNow(); for (PhysicalStore store : participants.values()) { try { store.close(); @@ -1422,9 +1465,13 @@ private static long rowsPerSecond(long rows, long startedNanos) { public static final class PhysicalStore implements Closeable { private final PathStateNativeNodeStore nativeStore; + private final ResidentNodeStore nodeStore; - private PhysicalStore(Path directory, Engine engine) throws IOException { + private PhysicalStore(Path directory, Engine engine, int storeId, + ResidentNodeCache residentNodeCache) throws IOException { nativeStore = PathStateNativeNodeStore.open(directory, engine); + nodeStore = new ResidentNodeStore(new PhysicalNodeStore(nativeStore), residentNodeCache, + storeId); } public void putFlat(byte[] secureKey, byte[] encodedLeaf) { @@ -1448,7 +1495,7 @@ void scanFlat(PathStateNativeNodeStore.EntryConsumer consumer) throws IOExceptio } public PathNodeStore nodeStore() { - return new PhysicalNodeStore(nativeStore); + return nodeStore; } private PhysicalNodeBatchWriter nodeBatchWriter() { @@ -1480,6 +1527,7 @@ long getUnsyncedWriteBatchCalls() { } void clearNodes() throws IOException { + nodeStore.clear(); List pending = new ArrayList<>(4096); nativeStore.scanPrefix(new byte[]{NODE_PREFIX}, entry -> { pending.add(PathStateNativeNodeStore.BatchMutation.delete(entry.getKey())); @@ -1503,6 +1551,7 @@ void applyParticipantDelete(byte[] secureKey, List nodeMutations, mutations.add(metadataMutation(STORE_GENERATION_METADATA, generation)); mutations.add(metadataMutation(FLAT_COMPLETE_METADATA, storeRoot)); nativeStore.writeBatchUnsynced(mutations); + nodeStore.apply(nodeMutations); } void applyParticipantTransition(List flatMutations, @@ -1520,6 +1569,7 @@ void applyParticipantTransition(List flatMutations, mutations.add(metadataMutation(STORE_GENERATION_METADATA, generation)); mutations.add(metadataMutation(FLAT_COMPLETE_METADATA, storeRoot)); nativeStore.writeBatchUnsynced(mutations); + nodeStore.apply(nodeMutations); } void applySuperTransition(List nodeMutations, byte[] generation, @@ -1529,6 +1579,7 @@ void applySuperTransition(List nodeMutations, byte[] generation, mutations.add(metadataMutation(SUPER_GENERATION_METADATA, generation)); mutations.add(metadataMutation(FLAT_ROOT_METADATA, superRoot)); nativeStore.writeBatchUnsynced(mutations); + nodeStore.apply(nodeMutations); } private static void appendNodeMutations( @@ -1566,6 +1617,7 @@ public Path getDirectory() { @Override public void close() throws IOException { + nodeStore.clear(); nativeStore.close(); } } @@ -1594,6 +1646,206 @@ public void delete(byte[] path) { } } + static final class ResidentNodeStore implements PathNodeStore { + + private final PathNodeStore base; + private final ResidentNodeCache cache; + private final int storeId; + private final AtomicLong hits = new AtomicLong(); + private final AtomicLong cleanHits = new AtomicLong(); + private final AtomicLong updatedHits = new AtomicLong(); + private final AtomicLong nativeReads = new AtomicLong(); + + ResidentNodeStore(PathNodeStore base, ResidentNodeCache cache, int storeId) { + this.base = Objects.requireNonNull(base, "base"); + this.cache = Objects.requireNonNull(cache, "cache"); + this.storeId = storeId; + } + + @Override + public byte[] get(byte[] path) { + ResidentNodeCache.Lookup lookup = cache.get(storeId, path); + if (lookup.found) { + hits.incrementAndGet(); + if (lookup.updated) { + updatedHits.incrementAndGet(); + } else { + cleanHits.incrementAndGet(); + } + return owned(lookup.value); + } + byte[] value = base.get(path); + nativeReads.incrementAndGet(); + cache.put(storeId, path, value, false); + return owned(value); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + base.put(path, encodedNode); + cache.put(storeId, path, Objects.requireNonNull(encodedNode, "encodedNode"), true); + } + + @Override + public void delete(byte[] path) { + base.delete(path); + cache.put(storeId, path, null, true); + } + + void apply(List mutations) { + for (NodeMutation mutation : Objects.requireNonNull(mutations, "mutations")) { + cache.put(storeId, mutation.path, mutation.encodedNode, true); + } + } + + void clear() { + cache.clear(storeId); + } + + long getHits() { + return hits.get(); + } + + long getCleanHits() { + return cleanHits.get(); + } + + long getUpdatedHits() { + return updatedHits.get(); + } + + long getNativeReads() { + return nativeReads.get(); + } + + private static byte[] owned(byte[] value) { + return value == null ? null : Arrays.copyOf(value, value.length); + } + } + + static final class ResidentNodeCache { + + private static final long ENTRY_OVERHEAD_BYTES = 64; + private final long maxBytes; + private final Map values = + new LinkedHashMap<>(1024, 0.75f, true); + private long bytes; + private long evictions; + + ResidentNodeCache(long maxBytes) { + if (maxBytes <= 0) { + throw new IllegalArgumentException("resident node cache bytes must be positive"); + } + this.maxBytes = maxBytes; + } + + synchronized Lookup get(int storeId, byte[] path) { + ResidentNodeCacheValue value = values.get(new ResidentNodeCacheKey(storeId, path)); + return value == null ? Lookup.missing() : Lookup.found(value.value, value.updated); + } + + synchronized void put(int storeId, byte[] path, byte[] value, boolean updated) { + ResidentNodeCacheKey key = new ResidentNodeCacheKey(storeId, path); + ResidentNodeCacheValue replacement = new ResidentNodeCacheValue(value, updated, + ENTRY_OVERHEAD_BYTES + Integer.BYTES + key.path.length + + (value == null ? 0 : value.length)); + ResidentNodeCacheValue previous = values.put(key, replacement); + if (previous != null) { + bytes -= previous.weight; + } + bytes += replacement.weight; + java.util.Iterator> iterator = + values.entrySet().iterator(); + while (bytes > maxBytes && iterator.hasNext()) { + Map.Entry eldest = iterator.next(); + bytes -= eldest.getValue().weight; + iterator.remove(); + evictions++; + } + } + + synchronized void clear(int storeId) { + java.util.Iterator> iterator = + values.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getKey().storeId == storeId) { + bytes -= entry.getValue().weight; + iterator.remove(); + } + } + } + + synchronized long bytes() { + return bytes; + } + + synchronized int size() { + return values.size(); + } + + synchronized long evictions() { + return evictions; + } + + static final class Lookup { + + private final boolean found; + private final byte[] value; + private final boolean updated; + + private Lookup(boolean found, byte[] value, boolean updated) { + this.found = found; + this.value = value == null ? null : Arrays.copyOf(value, value.length); + this.updated = updated; + } + + private static Lookup missing() { + return new Lookup(false, null, false); + } + + private static Lookup found(byte[] value, boolean updated) { + return new Lookup(true, value, updated); + } + } + } + + private static final class ResidentNodeCacheKey { + + private final int storeId; + private final byte[] path; + + private ResidentNodeCacheKey(int storeId, byte[] path) { + this.storeId = storeId; + this.path = Arrays.copyOf(Objects.requireNonNull(path, "path"), path.length); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof ResidentNodeCacheKey + && storeId == ((ResidentNodeCacheKey) other).storeId + && Arrays.equals(path, ((ResidentNodeCacheKey) other).path); + } + + @Override + public int hashCode() { + return 31 * storeId + Arrays.hashCode(path); + } + } + + private static final class ResidentNodeCacheValue { + + private final byte[] value; + private final boolean updated; + private final long weight; + + private ResidentNodeCacheValue(byte[] value, boolean updated, long weight) { + this.value = value == null ? null : Arrays.copyOf(value, value.length); + this.updated = updated; + this.weight = weight; + } + } + private static final class PhysicalNodeBatchWriter implements PathNodeStore { private final PathStateNativeNodeStore nativeStore; @@ -1655,14 +1907,25 @@ private void flush() { static final class RecordingNodeStore implements PathNodeStore { + private static final byte[] ABSENT = new byte[0]; private final PathNodeStore base; private final Map changes = new LinkedHashMap<>(); - private final Map baseReads = new LinkedHashMap<>(); - private long baseReadMisses; - private long baseReadHits; + private final Map baseReads = new ConcurrentHashMap<>(); + private final long initialResidentReadHits; + private final long initialResidentCleanHits; + private final long initialResidentUpdatedHits; + private final long initialNativeReads; + private final AtomicLong baseReadMisses = new AtomicLong(); + private final AtomicLong baseReadHits = new AtomicLong(); RecordingNodeStore(PathNodeStore base) { this.base = Objects.requireNonNull(base, "base"); + ResidentNodeStore resident = base instanceof ResidentNodeStore + ? (ResidentNodeStore) base : null; + initialResidentReadHits = resident == null ? 0 : resident.getHits(); + initialResidentCleanHits = resident == null ? 0 : resident.getCleanHits(); + initialResidentUpdatedHits = resident == null ? 0 : resident.getUpdatedHits(); + initialNativeReads = resident == null ? 0 : resident.getNativeReads(); } @Override @@ -1707,23 +1970,46 @@ private List reverseEntries() { } private byte[] baseValue(BytesKey key) { - if (!baseReads.containsKey(key)) { - byte[] value = base.get(key.bytes); - baseReads.put(key, value == null ? null : Arrays.copyOf(value, value.length)); - baseReadMisses++; + byte[] value = baseReads.get(key); + if (value == null) { + byte[] loaded = base.get(key.bytes); + baseReadMisses.incrementAndGet(); + byte[] owned = loaded == null ? ABSENT : Arrays.copyOf(loaded, loaded.length); + byte[] raced = baseReads.putIfAbsent(key, owned); + value = raced == null ? owned : raced; } else { - baseReadHits++; + baseReadHits.incrementAndGet(); } - byte[] value = baseReads.get(key); - return value == null ? null : Arrays.copyOf(value, value.length); + return value == ABSENT ? null : Arrays.copyOf(value, value.length); } long getBaseReadMisses() { - return baseReadMisses; + return baseReadMisses.get(); } long getBaseReadHits() { - return baseReadHits; + return baseReadHits.get(); + } + + long getResidentReadHits() { + return base instanceof ResidentNodeStore + ? ((ResidentNodeStore) base).getHits() - initialResidentReadHits : 0; + } + + long getResidentCleanHits() { + return base instanceof ResidentNodeStore + ? ((ResidentNodeStore) base).getCleanHits() - initialResidentCleanHits : 0; + } + + long getResidentUpdatedHits() { + return base instanceof ResidentNodeStore + ? ((ResidentNodeStore) base).getUpdatedHits() - initialResidentUpdatedHits : 0; + } + + long getNativeReads() { + return base instanceof ResidentNodeStore + ? ((ResidentNodeStore) base).getNativeReads() - initialNativeReads + : baseReadMisses.get(); } private int putCount() { @@ -1796,11 +2082,20 @@ private static final class TransitionPlan { private final PathStatePhysicalReverseJournal journal; private final long nodeReadMisses; private final long nodeReadHits; + private final long residentNodeHits; + private final long residentCleanHits; + private final long residentUpdatedHits; + private final long nativeNodeReads; + private final long initialResidentEvictions; + private final long nodeDecodes; + private final long nodeHashVerifies; private TransitionPlan(PathStatePhysicalGlobalIntent target, List participants, PhysicalStore superStore, List superNodeMutations, PathStatePhysicalReverseJournal journal, - long nodeReadMisses, long nodeReadHits) { + long nodeReadMisses, long nodeReadHits, long residentNodeHits, + long residentCleanHits, long residentUpdatedHits, long nativeNodeReads, + long initialResidentEvictions, long nodeDecodes, long nodeHashVerifies) { this.target = target; this.participants = participants; this.superStore = superStore; @@ -1808,6 +2103,13 @@ private TransitionPlan(PathStatePhysicalGlobalIntent target, this.journal = journal; this.nodeReadMisses = nodeReadMisses; this.nodeReadHits = nodeReadHits; + this.residentNodeHits = residentNodeHits; + this.residentCleanHits = residentCleanHits; + this.residentUpdatedHits = residentUpdatedHits; + this.nativeNodeReads = nativeNodeReads; + this.initialResidentEvictions = initialResidentEvictions; + this.nodeDecodes = nodeDecodes; + this.nodeHashVerifies = nodeHashVerifies; } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index a8f8a68a3cc..9d68c6a96d9 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -12,6 +12,9 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; /** * Current-only per-Store trie and super-trie aggregator for TASK-016. @@ -100,6 +103,54 @@ public synchronized void apply(Collection mutations) { rootMaterialized = false; } + synchronized void applyParallel(Collection mutations, + ExecutorService participantExecutor, ExecutorService branchExecutor) { + List prepared = prepare(mutations); + Map> grouped = new LinkedHashMap<>(); + for (PreparedMutation mutation : prepared) { + grouped.computeIfAbsent(mutation.participant.getStoreId(), ignored -> new ArrayList<>()) + .add(mutation); + } + List> futures = new ArrayList<>(); + for (List participantMutations : grouped.values()) { + futures.add(Objects.requireNonNull(participantExecutor, "participantExecutor").submit(() -> { + PathStateParticipant participant = participantMutations.get(0).participant; + List batch = new ArrayList<>(participantMutations.size()); + for (PreparedMutation mutation : participantMutations) { + batch.add(new PathMerkleTrie.BatchMutation(mutation.secureKey, mutation.encodedValue)); + } + participantTries.get(participant.getDbName()).applyBatch(batch, + Objects.requireNonNull(branchExecutor, "branchExecutor")); + })); + } + try { + for (Future future : futures) { + future.get(); + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + cancel(futures); + throw new IllegalStateException("parallel path-state prepare interrupted", interrupted); + } catch (ExecutionException failed) { + cancel(futures); + Throwable cause = failed.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + throw new IllegalStateException("parallel path-state prepare failed", cause); + } + recordPendingLeafMutations(prepared); + rootMaterialized = false; + } + + private static void cancel(List> futures) { + for (Future future : futures) { + if (!future.isDone()) { + future.cancel(true); + } + } + } + /** Applies one rebuild batch while locking only the participant tries touched by that batch. */ void applyRebuild(Collection mutations) { List prepared = prepare(mutations); @@ -176,6 +227,22 @@ public synchronized byte[] participantRoot(String dbName) { return participantTries.get(participant.getDbName()).rootHash(); } + synchronized long nodeDecodeCount() { + long count = superTrie.getNodeDecodeCount(); + for (PathMerkleTrie trie : participantTries.values()) { + count += trie.getNodeDecodeCount(); + } + return count; + } + + synchronized long nodeHashVerifyCount() { + long count = superTrie.getNodeHashVerifyCount(); + for (PathMerkleTrie trie : participantTries.values()) { + count += trie.getNodeHashVerifyCount(); + } + return count; + } + /** Returns the super root after binding every participant identity, format, and current root. */ public synchronized byte[] rootHash() { if (rootMaterialized) { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index feb9e09bffd..b3820e0026c 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -114,6 +114,102 @@ public void delete(byte[] path) { assertEquals(2, store.getBaseReadHits()); } + @Test + public void residentNodeStoreIsBoundedAndTracksCommittedValues() { + java.util.Map durable = new java.util.HashMap<>(); + durable.put("1", new byte[]{4}); + AtomicInteger reads = new AtomicInteger(); + PathNodeStore base = new PathNodeStore() { + @Override + public byte[] get(byte[] path) { + reads.incrementAndGet(); + byte[] value = durable.get(Integer.toString(path[0])); + return value == null ? null : Arrays.copyOf(value, value.length); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + durable.put(Integer.toString(path[0]), Arrays.copyOf(encodedNode, encodedNode.length)); + } + + @Override + public void delete(byte[] path) { + durable.remove(Integer.toString(path[0])); + } + }; + PathStatePhysicalStoreSet.ResidentNodeCache cache = + new PathStatePhysicalStoreSet.ResidentNodeCache(150); + PathStatePhysicalStoreSet.ResidentNodeStore store = + new PathStatePhysicalStoreSet.ResidentNodeStore(base, cache, 1); + + assertArrayEquals(new byte[]{4}, store.get(new byte[]{1})); + assertArrayEquals(new byte[]{4}, store.get(new byte[]{1})); + assertNull(store.get(new byte[]{2})); + assertEquals(2, reads.get()); + assertEquals(1, store.getHits()); + assertEquals(2, store.getNativeReads()); + + store.put(new byte[]{1}, new byte[]{7}); + assertArrayEquals(new byte[]{7}, store.get(new byte[]{1})); + store.delete(new byte[]{2}); + assertNull(store.get(new byte[]{2})); + assertEquals(2, reads.get()); + + assertNull(store.get(new byte[]{3})); + assertTrue(cache.bytes() <= 150); + assertTrue(cache.size() <= 2); + assertArrayEquals(new byte[]{7}, store.get(new byte[]{1})); + assertTrue(cache.bytes() <= 150); + assertTrue(cache.evictions() > 0); + } + + @Test + public void residentNodeCacheSharesBudgetWithoutCrossStoreAliasing() { + AtomicInteger firstReads = new AtomicInteger(); + AtomicInteger secondReads = new AtomicInteger(); + PathNodeStore firstBase = fixedNodeStore(new byte[]{1}, firstReads); + PathNodeStore secondBase = fixedNodeStore(new byte[]{2}, secondReads); + PathStatePhysicalStoreSet.ResidentNodeCache cache = + new PathStatePhysicalStoreSet.ResidentNodeCache(300); + PathStatePhysicalStoreSet.ResidentNodeStore first = + new PathStatePhysicalStoreSet.ResidentNodeStore(firstBase, cache, 1); + PathStatePhysicalStoreSet.ResidentNodeStore second = + new PathStatePhysicalStoreSet.ResidentNodeStore(secondBase, cache, 2); + + byte[] samePath = new byte[]{7}; + assertArrayEquals(new byte[]{1}, first.get(samePath)); + assertArrayEquals(new byte[]{2}, second.get(samePath)); + assertArrayEquals(new byte[]{1}, first.get(samePath)); + assertArrayEquals(new byte[]{2}, second.get(samePath)); + assertEquals(1, firstReads.get()); + assertEquals(1, secondReads.get()); + assertTrue(cache.bytes() <= 300); + + first.clear(); + assertArrayEquals(new byte[]{2}, second.get(samePath)); + assertEquals(1, secondReads.get()); + assertArrayEquals(new byte[]{1}, first.get(samePath)); + assertEquals(2, firstReads.get()); + } + + private static PathNodeStore fixedNodeStore(byte[] fixedValue, AtomicInteger reads) { + return new PathNodeStore() { + @Override + public byte[] get(byte[] path) { + reads.incrementAndGet(); + return Arrays.copyOf(fixedValue, fixedValue.length); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + } + + @Override + public void delete(byte[] path) { + } + }; + } + @Test public void streamsNativeScansWithoutCollectingTheResultSet() throws Exception { for (Engine engine : availableEngines()) { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java index c1127af6af8..f296ebc7316 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.Arrays; @@ -14,6 +15,7 @@ import java.util.concurrent.Future; import org.bouncycastle.util.encoders.Hex; import org.junit.Test; +import org.tron.common.crypto.Hash; import org.tron.core.trie.TrieImpl; public class PathStateRootTest { @@ -197,6 +199,77 @@ public void concurrentUniqueMutationsMatchSequentialRoot() throws Exception { assertArrayEquals(sequential.rootHash(), concurrent.rootHash()); } + @Test + public void gethStyleParticipantAndRootBranchBatchMatchesSequentialRoot() { + PathStateRoot parallel = stateRoot(participants()); + PathStateRoot sequential = stateRoot(participants()); + List initial = new ArrayList<>(); + for (int i = 0; i < 96; i++) { + initial.add(PathStateMutation.put("account", bytes("initial-" + i), + bytes("value-" + i))); + } + parallel.apply(initial); + sequential.apply(initial); + parallel.rootHash(); + sequential.rootHash(); + + List changes = new ArrayList<>(); + for (int i = 0; i < 32; i++) { + changes.add(PathStateMutation.put("account", bytes("initial-" + i), + bytes("updated-" + i))); + } + for (int i = 32; i < 48; i++) { + changes.add(PathStateMutation.delete("account", bytes("initial-" + i))); + } + for (int i = 0; i < 24; i++) { + changes.add(PathStateMutation.put("storage-row", bytes("slot-" + i), + bytes("storage-" + i))); + changes.add(PathStateMutation.put("abi", bytes("contract-" + i), + bytes("abi-" + i))); + } + sequential.apply(changes); + ExecutorService participants = Executors.newFixedThreadPool(4); + ExecutorService branches = Executors.newFixedThreadPool(8); + try { + parallel.applyParallel(changes, participants, branches); + } finally { + participants.shutdownNow(); + branches.shutdownNow(); + } + assertArrayEquals(sequential.rootHash(), parallel.rootHash()); + assertEquals(sequential.pendingLeafMutations().size(), + parallel.pendingLeafMutations().size()); + } + + @Test + public void restoredTrieAttachesDecodedNodesForRepeatedReads() { + CountingPathNodeStore store = new CountingPathNodeStore(); + PathMerkleTrie built = new PathMerkleTrie(store); + byte[] selectedKey = null; + byte[] selectedValue = null; + for (int i = 0; i < 64; i++) { + byte[] key = Hash.sha3(bytes("key-" + i)); + byte[] value = bytes("value-" + i); + built.put(key, value); + if (i == 31) { + selectedKey = key; + selectedValue = value; + } + } + byte[] root = built.rootHash(); + + PathMerkleTrie restored = new PathMerkleTrie(store); + restored.restoreRoot(root); + int readsAfterRoot = store.reads; + assertArrayEquals(selectedValue, restored.get(selectedKey)); + int readsAfterFirstLookup = store.reads; + assertTrue(readsAfterFirstLookup > readsAfterRoot); + assertTrue(restored.getNodeDecodeCount() > 0); + + assertArrayEquals(selectedValue, restored.get(selectedKey)); + assertEquals(readsAfterFirstLookup, store.reads); + } + private static byte[] referenceRoot(List participants, Mutation[] mutations) { Map stores = new LinkedHashMap<>(); @@ -290,4 +363,27 @@ public void delete(byte[] path) { nodes.remove(Hex.toHexString(path)); } } + + private static final class CountingPathNodeStore implements PathNodeStore { + + private final Map nodes = new LinkedHashMap<>(); + private int reads; + + @Override + public byte[] get(byte[] path) { + reads++; + byte[] node = nodes.get(Hex.toHexString(path)); + return node == null ? null : Arrays.copyOf(node, node.length); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + nodes.put(Hex.toHexString(path), Arrays.copyOf(encodedNode, encodedNode.length)); + } + + @Override + public void delete(byte[] path) { + nodes.remove(Hex.toHexString(path)); + } + } } From 27d1fb678527bc26c7ccc5f741f76c9a39703e42 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 2 Sep 2026 12:00:01 +0800 Subject: [PATCH 107/161] feat(jsonrpc): support historical eth_call --- .../org/tron/core/actuator/VMActuator.java | 49 ++- .../vm/HistoricalCapabilityException.java | 9 + .../core/vm/HistoricalExecutionGuard.java | 28 ++ .../org/tron/core/vm/OperationActions.java | 8 + .../org/tron/core/vm/config/ConfigLoader.java | 60 +++- .../tron/core/vm/program/ContractState.java | 5 + .../org/tron/core/vm/program/Program.java | 14 +- .../org/tron/core/vm/program/Storage.java | 18 +- .../repository/CurrentRepositoryProvider.java | 11 + .../repository/CurrentStoreStateSource.java | 62 ++++ .../HistoricalArchiveStateSource.java | 55 +++ .../HistoricalRepositoryProvider.java | 20 ++ .../tron/core/vm/repository/Repository.java | 19 ++ .../core/vm/repository/RepositoryImpl.java | 164 +++++++-- .../vm/repository/RepositoryProvider.java | 10 + .../vm/repository/RepositoryStateSource.java | 25 ++ .../org/tron/common/utils/StorageUtils.java | 4 +- .../org/tron/core/db/TransactionContext.java | 27 ++ .../core/db2/archive/ArchiveReadContext.java | 42 ++- .../HistoricalQueryBudgetException.java | 9 + .../db2/archive/HistoricalQuerySession.java | 250 ++++++++++++++ .../core/store/DynamicPropertiesStore.java | 60 ++++ .../org/tron/core/vm/config/VMConfig.java | 6 +- .../org/tron/common/runtime/RuntimeImpl.java | 19 +- .../src/main/java/org/tron/core/Wallet.java | 41 ++- .../main/java/org/tron/core/db/Manager.java | 47 +++ .../services/jsonrpc/TronJsonRpcImpl.java | 164 +++++++-- .../db2/archive/ArchiveReadSnapshotTest.java | 315 ++++++++++++++++++ .../tron/core/jsonrpc/JsonrpcServiceTest.java | 12 + 29 files changed, 1451 insertions(+), 102 deletions(-) create mode 100644 actuator/src/main/java/org/tron/core/vm/HistoricalCapabilityException.java create mode 100644 actuator/src/main/java/org/tron/core/vm/HistoricalExecutionGuard.java create mode 100644 actuator/src/main/java/org/tron/core/vm/repository/CurrentRepositoryProvider.java create mode 100644 actuator/src/main/java/org/tron/core/vm/repository/CurrentStoreStateSource.java create mode 100644 actuator/src/main/java/org/tron/core/vm/repository/HistoricalArchiveStateSource.java create mode 100644 actuator/src/main/java/org/tron/core/vm/repository/HistoricalRepositoryProvider.java create mode 100644 actuator/src/main/java/org/tron/core/vm/repository/RepositoryProvider.java create mode 100644 actuator/src/main/java/org/tron/core/vm/repository/RepositoryStateSource.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQueryBudgetException.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQuerySession.java diff --git a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java index d785951027b..d3bccdc7e72 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -13,6 +13,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.function.LongSupplier; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -54,8 +55,10 @@ import org.tron.core.vm.program.ProgramPrecompile; import org.tron.core.vm.program.invoke.ProgramInvoke; import org.tron.core.vm.program.invoke.ProgramInvokeFactory; +import org.tron.core.vm.repository.CurrentRepositoryProvider; import org.tron.core.vm.repository.Repository; import org.tron.core.vm.repository.RepositoryImpl; +import org.tron.core.vm.repository.RepositoryProvider; import org.tron.core.vm.utils.MUtil; import org.tron.protos.Protocol; import org.tron.protos.Protocol.Block; @@ -97,8 +100,15 @@ public class VMActuator implements Actuator2 { private LogInfoTriggerParser logInfoTriggerParser; + private final RepositoryProvider repositoryProvider; + public VMActuator(boolean isConstantCall) { + this(isConstantCall, CurrentRepositoryProvider.INSTANCE); + } + + public VMActuator(boolean isConstantCall, RepositoryProvider repositoryProvider) { this.isConstantCall = isConstantCall; + this.repositoryProvider = Objects.requireNonNull(repositoryProvider, "repositoryProvider"); this.maxEnergyLimit = CommonParameter.getInstance().maxEnergyLimitForConstant; } @@ -111,6 +121,11 @@ private static long getEnergyFee(long callerEnergyUsage, long callerEnergyFrozen .divide(BigInteger.valueOf(callerEnergyTotal)).longValueExact(); } + private long dynamicLong(String key, LongSupplier currentValue) { + return rootRepository.isHistorical() + ? ConfigLoader.property(rootRepository, key) : currentValue.getAsLong(); + } + @Override public void validate(Object object) throws ContractValidateException { @@ -119,16 +134,24 @@ public void validate(Object object) throws ContractValidateException { throw new RuntimeException("TransactionContext is null"); } - // Load Config - ConfigLoader.load(context.getStoreFactory(), isConstantCall); + rootRepository = Objects.requireNonNull(repositoryProvider.createRoot(context), + "repositoryProvider returned null"); + if (rootRepository.isHistorical()) { + if (!isConstantCall) { + throw new ContractValidateException("Historical execution must be a constant call"); + } + ConfigLoader.load(rootRepository); + } else { + ConfigLoader.load(context.getStoreFactory(), isConstantCall); + } // Warm up registry class OperationRegistry.init(); trx = context.getTrxCap().getInstance(); // If tx`s fee limit is set, use it to calc max energy limit for constant call if (isConstantCall && trx.getRawData().getFeeLimit() > 0) { maxEnergyLimit = min(maxEnergyLimit, trx.getRawData().getFeeLimit() - / context.getStoreFactory().getChainBaseManager() - .getDynamicPropertiesStore().getEnergyFee(), VMConfig.disableJavaLangMath()); + / dynamicLong("ENERGY_FEE", () -> context.getStoreFactory().getChainBaseManager() + .getDynamicPropertiesStore().getEnergyFee()), VMConfig.disableJavaLangMath()); } blockCap = context.getBlockCap(); if ((VMConfig.allowTvmFreeze() || VMConfig.allowTvmFreezeV2()) @@ -137,8 +160,10 @@ public void validate(Object object) throws ContractValidateException { } //Route Type ContractType contractType = this.trx.getRawData().getContract(0).getType(); - //Prepare Repository - rootRepository = RepositoryImpl.createRoot(context.getStoreFactory()); + if (rootRepository.isHistorical() && contractType != ContractType.TriggerSmartContract) { + throw new ContractValidateException( + "Historical execution only supports TriggerSmartContract"); + } enableEventListener = context.isEventPluginLoaded(); @@ -462,7 +487,8 @@ private void create() private void call() throws ContractValidateException { - if (!rootRepository.getDynamicPropertiesStore().supportVM()) { + if (dynamicLong("ALLOW_CREATION_OF_CONTRACTS", + () -> rootRepository.getDynamicPropertiesStore().getAllowCreationOfContracts()) != 1L) { logger.info("vm work is off, need to be opened by the committee"); throw new ContractValidateException("VM work is off, need to be opened by the committee"); } @@ -507,10 +533,12 @@ private void call() byte[] code = rootRepository.getCode(contractAddress); if (isNotEmpty(code)) { long feeLimit = trx.getRawData().getFeeLimit(); - if (feeLimit < 0 || feeLimit > rootRepository.getDynamicPropertiesStore().getMaxFeeLimit()) { + long maxFeeLimit = dynamicLong("MAX_FEE_LIMIT", + () -> rootRepository.getDynamicPropertiesStore().getMaxFeeLimit()); + if (feeLimit < 0 || feeLimit > maxFeeLimit) { logger.info("invalid feeLimit {}", feeLimit); throw new ContractValidateException("feeLimit must be >= 0 and <= " - + rootRepository.getDynamicPropertiesStore().getMaxFeeLimit()); + + maxFeeLimit); } AccountCapsule caller = rootRepository.getAccount(callerAddress); long energyLimit; @@ -523,7 +551,8 @@ private void call() } long thisTxCPULimitInUs = calculateCpuLimitInUs(isConstantCall, - rootRepository.getDynamicPropertiesStore().getMaxCpuTimeOfOneTx(), + dynamicLong("MAX_CPU_TIME_OF_ONE_TX", + () -> rootRepository.getDynamicPropertiesStore().getMaxCpuTimeOfOneTx()), getCpuLimitInUsRatio(), CommonParameter.getInstance().getConstantCallTimeoutMs()); long vmStartInUs = System.nanoTime() / VMConstant.ONE_THOUSAND; long vmShouldEndInUs = vmStartInUs + thisTxCPULimitInUs; diff --git a/actuator/src/main/java/org/tron/core/vm/HistoricalCapabilityException.java b/actuator/src/main/java/org/tron/core/vm/HistoricalCapabilityException.java new file mode 100644 index 00000000000..e53eb852ed6 --- /dev/null +++ b/actuator/src/main/java/org/tron/core/vm/HistoricalCapabilityException.java @@ -0,0 +1,9 @@ +package org.tron.core.vm; + +/** Fail-closed rejection for a state capability unavailable to historical execution. */ +public class HistoricalCapabilityException extends RuntimeException { + + public HistoricalCapabilityException(String message) { + super(message); + } +} diff --git a/actuator/src/main/java/org/tron/core/vm/HistoricalExecutionGuard.java b/actuator/src/main/java/org/tron/core/vm/HistoricalExecutionGuard.java new file mode 100644 index 00000000000..49490f8bc1e --- /dev/null +++ b/actuator/src/main/java/org/tron/core/vm/HistoricalExecutionGuard.java @@ -0,0 +1,28 @@ +package org.tron.core.vm; + +/** Runtime allowlist for capabilities reachable only after bytecode dispatch. */ +public final class HistoricalExecutionGuard { + + private HistoricalExecutionGuard() { + } + + public static void requirePrecompileAllowed( + PrecompiledContracts.PrecompiledContract contract) { + if (contract instanceof PrecompiledContracts.Identity + || contract instanceof PrecompiledContracts.Sha256 + || contract instanceof PrecompiledContracts.Ripempd160 + || contract instanceof PrecompiledContracts.ECRecover + || contract instanceof PrecompiledContracts.ModExp + || contract instanceof PrecompiledContracts.BN128Addition + || contract instanceof PrecompiledContracts.BN128Multiplication + || contract instanceof PrecompiledContracts.BN128Pairing + || contract instanceof PrecompiledContracts.EthRipemd160 + || contract instanceof PrecompiledContracts.Blake2F + || contract instanceof PrecompiledContracts.P256Verify) { + return; + } + throw new HistoricalCapabilityException( + "Precompile is not allowed by historical execution: " + + contract.getClass().getSimpleName()); + } +} diff --git a/actuator/src/main/java/org/tron/core/vm/OperationActions.java b/actuator/src/main/java/org/tron/core/vm/OperationActions.java index 88c3c55899e..461f340b2af 100644 --- a/actuator/src/main/java/org/tron/core/vm/OperationActions.java +++ b/actuator/src/main/java/org/tron/core/vm/OperationActions.java @@ -1070,6 +1070,10 @@ public static void revertAction(Program program) { } public static void suicideAction(Program program) { + if (program.getContractState().isHistorical()) { + throw new HistoricalCapabilityException( + "SELFDESTRUCT is not supported by historical execution"); + } if (program.isStaticCall()) { throw new Program.StaticCallModificationException(); } @@ -1085,6 +1089,10 @@ public static void suicideAction(Program program) { } public static void suicideAction2(Program program) { + if (program.getContractState().isHistorical()) { + throw new HistoricalCapabilityException( + "SELFDESTRUCT is not supported by historical execution"); + } if (program.isStaticCall()) { throw new Program.StaticCallModificationException(); } diff --git a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java index 35480935742..1d035d10c19 100644 --- a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java +++ b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java @@ -1,11 +1,11 @@ package org.tron.core.vm.config; -import static org.tron.core.capsule.ReceiptCapsule.checkForEnergyLimit; - import lombok.extern.slf4j.Slf4j; import org.tron.common.parameter.CommonParameter; import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.store.StoreFactory; +import org.tron.core.vm.HistoricalCapabilityException; +import org.tron.core.vm.repository.Repository; @Slf4j(topic = "VMConfigLoader") public class ConfigLoader { @@ -21,8 +21,9 @@ public static void load(StoreFactory storeFactory, boolean isolate) { DynamicPropertiesStore ds = storeFactory.getChainBaseManager().getDynamicPropertiesStore(); VMConfig.setVmTrace(CommonParameter.getInstance().isVmTrace()); if (ds != null) { - VMConfig.initVmHardFork(checkForEnergyLimit(ds)); VMConfig.Snapshot snapshot = new VMConfig.Snapshot(); + snapshot.energyLimitHardFork = ds.getLatestBlockHeaderNumber() + >= CommonParameter.getInstance().getBlockNumForEnergyLimit(); snapshot.allowMultiSign = ds.getAllowMultiSign() == 1; snapshot.allowTvmTransferTrc10 = ds.getAllowTvmTransferTrc10() == 1; snapshot.allowTvmConstantinople = ds.getAllowTvmConstantinople() == 1; @@ -53,9 +54,62 @@ public static void load(StoreFactory storeFactory, boolean isolate) { if (isolate) { VMConfig.setLocalSnapshot(snapshot); } else { + VMConfig.initVmHardFork(snapshot.energyLimitHardFork); VMConfig.setGlobalSnapshot(snapshot); } } } } + + /** Loads an isolated VM view exclusively from a Repository's request-owned state source. */ + public static void load(Repository repository) { + if (disable) { + throw new HistoricalCapabilityException( + "Historical VM config loading cannot be disabled"); + } + VMConfig.setVmTrace(CommonParameter.getInstance().isVmTrace()); + VMConfig.Snapshot snapshot = new VMConfig.Snapshot(); + snapshot.energyLimitHardFork = property(repository, "latest_block_header_number") + >= CommonParameter.getInstance().getBlockNumForEnergyLimit(); + snapshot.allowMultiSign = enabled(repository, "ALLOW_MULTI_SIGN"); + snapshot.allowTvmTransferTrc10 = enabled(repository, "ALLOW_TVM_TRANSFER_TRC10"); + snapshot.allowTvmConstantinople = enabled(repository, "ALLOW_TVM_CONSTANTINOPLE"); + snapshot.allowTvmSolidity059 = enabled(repository, "ALLOW_TVM_SOLIDITY_059"); + snapshot.allowShieldedTRC20Transaction = + enabled(repository, "ALLOW_SHIELDED_TRC20_TRANSACTION"); + snapshot.allowTvmIstanbul = enabled(repository, "ALLOW_TVM_ISTANBUL"); + snapshot.allowTvmFreeze = enabled(repository, "ALLOW_TVM_FREEZE"); + snapshot.allowTvmVote = enabled(repository, "ALLOW_TVM_VOTE"); + snapshot.allowTvmLondon = enabled(repository, "ALLOW_TVM_LONDON"); + snapshot.allowTvmCompatibleEvm = enabled(repository, "ALLOW_TVM_COMPATIBLE_EVM"); + snapshot.allowHigherLimitForMaxCpuTimeOfOneTx = + enabled(repository, "ALLOW_HIGHER_LIMIT_FOR_MAX_CPU_TIME_OF_ONE_TX"); + snapshot.allowTvmFreezeV2 = property(repository, "UNFREEZE_DELAY_DAYS") > 0; + snapshot.allowOptimizedReturnValueOfChainId = + enabled(repository, "ALLOW_OPTIMIZED_RETURN_VALUE_OF_CHAIN_ID"); + snapshot.allowDynamicEnergy = enabled(repository, "ALLOW_DYNAMIC_ENERGY"); + snapshot.dynamicEnergyThreshold = property(repository, "DYNAMIC_ENERGY_THRESHOLD"); + snapshot.dynamicEnergyIncreaseFactor = property(repository, "DYNAMIC_ENERGY_INCREASE_FACTOR"); + snapshot.dynamicEnergyMaxFactor = property(repository, "DYNAMIC_ENERGY_MAX_FACTOR"); + snapshot.allowTvmShanghai = enabled(repository, "ALLOW_TVM_SHANGHAI"); + snapshot.allowEnergyAdjustment = enabled(repository, "ALLOW_ENERGY_ADJUSTMENT"); + snapshot.allowStrictMath = enabled(repository, "ALLOW_STRICT_MATH"); + snapshot.allowTvmCancun = enabled(repository, "ALLOW_TVM_CANCUN"); + snapshot.disableJavaLangMath = enabled(repository, "CONSENSUS_LOGIC_OPTIMIZATION"); + snapshot.allowTvmBlob = enabled(repository, "ALLOW_TVM_BLOB"); + snapshot.allowTvmSelfdestructRestriction = + enabled(repository, "ALLOW_TVM_SELFDESTRUCT_RESTRICTION"); + snapshot.allowTvmOsaka = enabled(repository, "ALLOW_TVM_OSAKA"); + snapshot.allowHardenResourceCalculation = + enabled(repository, "ALLOW_HARDEN_RESOURCE_CALCULATION"); + VMConfig.setLocalSnapshot(snapshot); + } + + public static long property(Repository repository, String key) { + return repository.getDynamicPropertyLong(key); + } + + private static boolean enabled(Repository repository, String key) { + return property(repository, key) == 1L; + } } diff --git a/actuator/src/main/java/org/tron/core/vm/program/ContractState.java b/actuator/src/main/java/org/tron/core/vm/program/ContractState.java index c6347b9a072..c4a6394d55d 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/ContractState.java +++ b/actuator/src/main/java/org/tron/core/vm/program/ContractState.java @@ -41,6 +41,11 @@ public void setProgramListener(ProgramListener listener) { this.programListener = listener; } + @Override + public boolean isHistorical() { + return repository.isHistorical(); + } + @Override public AssetIssueCapsule getAssetIssue(byte[] tokenId) { return repository.getAssetIssue(tokenId); diff --git a/actuator/src/main/java/org/tron/core/vm/program/Program.java b/actuator/src/main/java/org/tron/core/vm/program/Program.java index 590859a9fef..ab2d58f9cac 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/Program.java +++ b/actuator/src/main/java/org/tron/core/vm/program/Program.java @@ -58,6 +58,8 @@ import org.tron.core.exception.TronException; import org.tron.core.utils.TransactionUtil; import org.tron.core.vm.EnergyCost; +import org.tron.core.vm.HistoricalCapabilityException; +import org.tron.core.vm.HistoricalExecutionGuard; import org.tron.core.vm.MessageCall; import org.tron.core.vm.Op; import org.tron.core.vm.OperationRegistry; @@ -811,6 +813,10 @@ public void createContract(DataWord value, DataWord memStart, DataWord memSize) private void createContractImpl(DataWord value, byte[] programCode, byte[] newAddress, boolean isCreate2) { + if (getContractState().isHistorical()) { + throw new HistoricalCapabilityException( + "CREATE and CREATE2 are not supported by historical execution"); + } byte[] senderAddress = getContextAddress(); if (logger.isDebugEnabled()) { @@ -1660,6 +1666,10 @@ public void callToPrecompiledAddress(MessageCall msg, PrecompiledContracts.PrecompiledContract contract) { returnDataBuffer = null; // reset return buffer right before the call + if (getContractState().isHistorical()) { + HistoricalExecutionGuard.requirePrecompileAllowed(contract); + } + if (getCallDeep() == MAX_DEPTH) { stackPushZero(); this.refundEnergy(msg.getEnergy().longValue(), " call deep limit reach"); @@ -2363,11 +2373,11 @@ public long updateContextContractFactor() { if (contractStateCapsule == null) { contractStateCapsule = new ContractStateCapsule( - contractState.getDynamicPropertiesStore().getCurrentCycleNumber()); + contractState.getDynamicPropertyLong("CURRENT_CYCLE_NUMBER")); contractState.updateContractState(getContextAddress(), contractStateCapsule); } else { if (contractStateCapsule.catchUpToCycle( - contractState.getDynamicPropertiesStore().getCurrentCycleNumber(), + contractState.getDynamicPropertyLong("CURRENT_CYCLE_NUMBER"), VMConfig.getDynamicEnergyThreshold(), VMConfig.getDynamicEnergyIncreaseFactor(), VMConfig.getDynamicEnergyMaxFactor(), diff --git a/actuator/src/main/java/org/tron/core/vm/program/Storage.java b/actuator/src/main/java/org/tron/core/vm/program/Storage.java index 666b4611d98..8146dba0bd0 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/Storage.java +++ b/actuator/src/main/java/org/tron/core/vm/program/Storage.java @@ -11,27 +11,40 @@ public class Storage { + @FunctionalInterface + public interface RowLoader { + + StorageRowCapsule get(byte[] physicalKey); + } + @Getter private final Map rowCache = new HashMap<>(); @Getter private byte[] addrHash; @Getter private StorageRowStore store; + private RowLoader rowLoader; @Getter private byte[] address; @Setter private int contractVersion; public Storage(byte[] address, StorageRowStore store) { + this(address, store, store::get); + } + + public Storage(byte[] address, StorageRowStore store, RowLoader rowLoader) { addrHash = StorageRowKeyCodec.addressHash(address, null); this.address = address; this.store = store; + this.rowLoader = rowLoader; } public Storage(Storage storage) { this.addrHash = storage.addrHash.clone(); this.address = storage.getAddress().clone(); this.store = storage.store; + this.rowLoader = storage.rowLoader; this.contractVersion = storage.contractVersion; storage.getRowCache().forEach((DataWord rowKey, StorageRowCapsule row) -> { StorageRowCapsule newRow = new StorageRowCapsule(row); @@ -52,7 +65,7 @@ public DataWord getValue(DataWord key) { if (rowCache.containsKey(key)) { return new DataWord(rowCache.get(key).getValue()); } else { - StorageRowCapsule row = store.get(compose(key.getData(), addrHash)); + StorageRowCapsule row = rowLoader.get(compose(key.getData(), addrHash)); if (row == null || row.getInstance() == null) { return null; } @@ -72,6 +85,9 @@ public void put(DataWord key, DataWord value) { } public void commit() { + if (store == null) { + throw new IllegalStateException("Read-only historical storage cannot be committed"); + } rowCache.forEach((DataWord rowKey, StorageRowCapsule row) -> { if (row.isDirty()) { if (new DataWord(row.getValue()).isZero()) { diff --git a/actuator/src/main/java/org/tron/core/vm/repository/CurrentRepositoryProvider.java b/actuator/src/main/java/org/tron/core/vm/repository/CurrentRepositoryProvider.java new file mode 100644 index 00000000000..5d2a862f2f8 --- /dev/null +++ b/actuator/src/main/java/org/tron/core/vm/repository/CurrentRepositoryProvider.java @@ -0,0 +1,11 @@ +package org.tron.core.vm.repository; + +/** Default provider preserving the existing current Chainbase execution path. */ +public enum CurrentRepositoryProvider implements RepositoryProvider { + INSTANCE; + + @Override + public Repository createRoot(org.tron.core.db.TransactionContext context) { + return RepositoryImpl.createRoot(context.getStoreFactory()); + } +} diff --git a/actuator/src/main/java/org/tron/core/vm/repository/CurrentStoreStateSource.java b/actuator/src/main/java/org/tron/core/vm/repository/CurrentStoreStateSource.java new file mode 100644 index 00000000000..467e02ff697 --- /dev/null +++ b/actuator/src/main/java/org/tron/core/vm/repository/CurrentStoreStateSource.java @@ -0,0 +1,62 @@ +package org.tron.core.vm.repository; + +import org.tron.core.ChainBaseManager; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.BytesCapsule; +import org.tron.core.capsule.CodeCapsule; +import org.tron.core.capsule.ContractCapsule; +import org.tron.core.capsule.ContractStateCapsule; +import org.tron.core.capsule.StorageRowCapsule; +import org.tron.core.exception.BadItemException; +import org.tron.core.exception.ItemNotFoundException; +import org.tron.core.store.StoreFactory; + +/** Existing current Chainbase stores adapted to RepositoryStateSource. */ +final class CurrentStoreStateSource implements RepositoryStateSource { + + private final ChainBaseManager manager; + + CurrentStoreStateSource(StoreFactory storeFactory) { + this.manager = storeFactory.getChainBaseManager(); + } + + @Override + public AccountCapsule getAccount(byte[] address) { + return manager.getAccountStore().get(address); + } + + @Override + public BytesCapsule getDynamicProperty(byte[] key) { + try { + return manager.getDynamicPropertiesStore().get(key); + } catch (BadItemException | ItemNotFoundException ignored) { + return null; + } + } + + @Override + public ContractCapsule getContract(byte[] address) { + return manager.getContractStore().get(address); + } + + @Override + public ContractStateCapsule getContractState(byte[] address) { + return manager.getContractStateStore().get(address); + } + + @Override + public byte[] getCode(byte[] address) { + CodeCapsule code = manager.getCodeStore().get(address); + return code == null ? null : code.getData(); + } + + @Override + public StorageRowCapsule getStorageRow(byte[] physicalKey) { + return manager.getStorageRowStore().get(physicalKey); + } + + @Override + public boolean isReadOnly() { + return false; + } +} diff --git a/actuator/src/main/java/org/tron/core/vm/repository/HistoricalArchiveStateSource.java b/actuator/src/main/java/org/tron/core/vm/repository/HistoricalArchiveStateSource.java new file mode 100644 index 00000000000..1702d608515 --- /dev/null +++ b/actuator/src/main/java/org/tron/core/vm/repository/HistoricalArchiveStateSource.java @@ -0,0 +1,55 @@ +package org.tron.core.vm.repository; + +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.BytesCapsule; +import org.tron.core.capsule.ContractCapsule; +import org.tron.core.capsule.ContractStateCapsule; +import org.tron.core.capsule.StorageRowCapsule; +import org.tron.core.db2.archive.HistoricalQuerySession; + +/** Exact historical state reads backed only by one request-owned Archive session. */ +final class HistoricalArchiveStateSource implements RepositoryStateSource { + + private final HistoricalQuerySession session; + + HistoricalArchiveStateSource(HistoricalQuerySession session) { + this.session = java.util.Objects.requireNonNull(session, "session"); + session.requirePinnedIdentity(); + } + + @Override + public AccountCapsule getAccount(byte[] address) { + return session.getAccount(address).map(AccountCapsule::new).orElse(null); + } + + @Override + public BytesCapsule getDynamicProperty(byte[] key) { + return session.getExact("properties", key).map(BytesCapsule::new).orElse(null); + } + + @Override + public ContractCapsule getContract(byte[] address) { + return session.getContract(address).map(ContractCapsule::new).orElse(null); + } + + @Override + public ContractStateCapsule getContractState(byte[] address) { + return session.getContractState(address).map(ContractStateCapsule::new).orElse(null); + } + + @Override + public byte[] getCode(byte[] address) { + return session.getCode(address).orElse(null); + } + + @Override + public StorageRowCapsule getStorageRow(byte[] physicalKey) { + return session.getExact("storage-row", physicalKey) + .map(StorageRowCapsule::new).orElse(null); + } + + @Override + public boolean isReadOnly() { + return true; + } +} diff --git a/actuator/src/main/java/org/tron/core/vm/repository/HistoricalRepositoryProvider.java b/actuator/src/main/java/org/tron/core/vm/repository/HistoricalRepositoryProvider.java new file mode 100644 index 00000000000..03a521cc0d6 --- /dev/null +++ b/actuator/src/main/java/org/tron/core/vm/repository/HistoricalRepositoryProvider.java @@ -0,0 +1,20 @@ +package org.tron.core.vm.repository; + +import org.tron.core.db.TransactionContext; +import org.tron.core.db.TransactionContext.ExecutionMode; + +/** Provider for a request-owned, exact historical Repository root. */ +public enum HistoricalRepositoryProvider implements RepositoryProvider { + INSTANCE; + + @Override + public Repository createRoot(TransactionContext context) { + if (context.getExecutionMode() != ExecutionMode.HISTORICAL_CONSTANT + || context.getHistoricalQuerySession() == null) { + throw new IllegalArgumentException( + "Historical Repository requires a HISTORICAL_CONSTANT context"); + } + return RepositoryImpl.createHistoricalRoot(context.getStoreFactory(), + context.getHistoricalQuerySession()); + } +} diff --git a/actuator/src/main/java/org/tron/core/vm/repository/Repository.java b/actuator/src/main/java/org/tron/core/vm/repository/Repository.java index 8f91d59d0b8..c5c161e2a7c 100644 --- a/actuator/src/main/java/org/tron/core/vm/repository/Repository.java +++ b/actuator/src/main/java/org/tron/core/vm/repository/Repository.java @@ -2,6 +2,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.tron.common.runtime.vm.DataWord; +import org.tron.common.utils.ByteArray; import org.tron.core.capsule.*; import org.tron.core.store.*; import org.tron.core.vm.program.Storage; @@ -9,6 +10,10 @@ public interface Repository { + default boolean isHistorical() { + return false; + } + AssetIssueCapsule getAssetIssue(byte[] tokenId); AssetIssueV2Store getAssetIssueV2Store(); @@ -27,6 +32,20 @@ public interface Repository { BytesCapsule getDynamicProperty(byte[] bytesKey); + default long getDynamicPropertyLong(String key) { + BytesCapsule value = getDynamicProperty( + key.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + if (value == null) { + Long defaultValue = isHistorical() + ? DynamicPropertiesStore.getLongPropertyDefault(key) : null; + if (defaultValue != null) { + return defaultValue; + } + throw new IllegalArgumentException("Dynamic property is missing: " + key); + } + return ByteArray.toLong(value.getData()); + } + DelegatedResourceCapsule getDelegatedResource(byte[] key); VotesCapsule getVotes(byte[] address); diff --git a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java index 62e7ce6ec08..5b5380002a0 100644 --- a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java +++ b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java @@ -12,7 +12,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Optional; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; import org.bouncycastle.util.Strings; @@ -46,6 +45,7 @@ import org.tron.core.db.BlockStore; import org.tron.core.db.KhaosDatabase; import org.tron.core.db.TransactionTrace; +import org.tron.core.db2.archive.HistoricalQuerySession; import org.tron.core.exception.BadItemException; import org.tron.core.exception.ItemNotFoundException; import org.tron.core.exception.StoreException; @@ -65,6 +65,7 @@ import org.tron.core.store.VotesStore; import org.tron.core.store.WitnessStore; import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.HistoricalCapabilityException; import org.tron.core.vm.program.Program.IllegalOperationException; import org.tron.core.vm.program.Storage; import org.tron.protos.Protocol; @@ -87,42 +88,27 @@ public class RepositoryImpl implements Repository { private static final byte[] TOTAL_TRON_POWER_WEIGHT = "TOTAL_TRON_POWER_WEIGHT".getBytes(); private StoreFactory storeFactory; - @Getter private DynamicPropertiesStore dynamicPropertiesStore; - @Getter private AccountStore accountStore; - @Getter private AssetIssueStore assetIssueStore; - @Getter private AssetIssueV2Store assetIssueV2Store; - @Getter private AbiStore abiStore; - @Getter private CodeStore codeStore; - @Getter private ContractStore contractStore; - @Getter private ContractStateStore contractStateStore; - @Getter private StorageRowStore storageRowStore; - @Getter private BlockStore blockStore; - @Getter private KhaosDatabase khaosDb; - @Getter private BlockIndexStore blockIndexStore; - @Getter private WitnessStore witnessStore; - @Getter private DelegatedResourceStore delegatedResourceStore; - @Getter private VotesStore votesStore; - @Getter private DelegationStore delegationStore; - @Getter private DelegatedResourceAccountIndexStore delegatedResourceAccountIndexStore; private Repository parent = null; + private RepositoryStateSource stateSource; + private boolean readOnlyRoot; private final HashMap> accountCache = new HashMap<>(); private final HashMap> codeCache = new HashMap<>(); @@ -144,16 +130,117 @@ public static void removeLruCache(byte[] address) { } public RepositoryImpl(StoreFactory storeFactory, RepositoryImpl repository) { - init(storeFactory, repository); + this(storeFactory, repository, + repository == null ? new CurrentStoreStateSource(storeFactory) : repository.stateSource, + repository != null && repository.readOnlyRoot); + } + + private RepositoryImpl(StoreFactory storeFactory, Repository parent, + RepositoryStateSource stateSource, boolean readOnlyRoot) { + init(storeFactory, parent, !readOnlyRoot); + this.stateSource = stateSource; + this.readOnlyRoot = readOnlyRoot; } public static RepositoryImpl createRoot(StoreFactory storeFactory) { return new RepositoryImpl(storeFactory, null); } - protected void init(StoreFactory storeFactory, RepositoryImpl parent) { + public static RepositoryImpl createHistoricalRoot(StoreFactory storeFactory, + HistoricalQuerySession session) { + return new RepositoryImpl(storeFactory, null, new HistoricalArchiveStateSource(session), true); + } + + @Override + public DynamicPropertiesStore getDynamicPropertiesStore() { + return currentStore(dynamicPropertiesStore, "DynamicPropertiesStore"); + } + + public AccountStore getAccountStore() { + return currentStore(accountStore, "AccountStore"); + } + + @Override + public AssetIssueStore getAssetIssueStore() { + return currentStore(assetIssueStore, "AssetIssueStore"); + } + + @Override + public AssetIssueV2Store getAssetIssueV2Store() { + return currentStore(assetIssueV2Store, "AssetIssueV2Store"); + } + + public AbiStore getAbiStore() { + return currentStore(abiStore, "AbiStore"); + } + + public CodeStore getCodeStore() { + return currentStore(codeStore, "CodeStore"); + } + + public ContractStore getContractStore() { + return currentStore(contractStore, "ContractStore"); + } + + public ContractStateStore getContractStateStore() { + return currentStore(contractStateStore, "ContractStateStore"); + } + + public StorageRowStore getStorageRowStore() { + return currentStore(storageRowStore, "StorageRowStore"); + } + + public BlockStore getBlockStore() { + return currentStore(blockStore, "BlockStore"); + } + + public KhaosDatabase getKhaosDb() { + return currentStore(khaosDb, "KhaosDatabase"); + } + + public BlockIndexStore getBlockIndexStore() { + return currentStore(blockIndexStore, "BlockIndexStore"); + } + + public WitnessStore getWitnessStore() { + return currentStore(witnessStore, "WitnessStore"); + } + + public DelegatedResourceStore getDelegatedResourceStore() { + return currentStore(delegatedResourceStore, "DelegatedResourceStore"); + } + + public VotesStore getVotesStore() { + return currentStore(votesStore, "VotesStore"); + } + + @Override + public DelegationStore getDelegationStore() { + return currentStore(delegationStore, "DelegationStore"); + } + + public DelegatedResourceAccountIndexStore getDelegatedResourceAccountIndexStore() { + return currentStore(delegatedResourceAccountIndexStore, + "DelegatedResourceAccountIndexStore"); + } + + private T currentStore(T store, String capability) { + if (readOnlyRoot) { + throw new HistoricalCapabilityException( + "Direct " + capability + " access is not supported by historical execution"); + } + return store; + } + + protected void init(StoreFactory storeFactory, Repository parent) { + init(storeFactory, parent, true); + } + + private void init(StoreFactory storeFactory, Repository parent, boolean bindCurrentStores) { if (storeFactory != null) { this.storeFactory = storeFactory; + } + if (bindCurrentStores) { ChainBaseManager manager = storeFactory.getChainBaseManager(); dynamicPropertiesStore = manager.getDynamicPropertiesStore(); accountStore = manager.getAccountStore(); @@ -176,9 +263,14 @@ protected void init(StoreFactory storeFactory, RepositoryImpl parent) { this.parent = parent; } + @Override + public boolean isHistorical() { + return readOnlyRoot; + } + @Override public Repository newRepositoryChild() { - return new RepositoryImpl(storeFactory, this); + return new RepositoryImpl(storeFactory, this, stateSource, readOnlyRoot); } @Override @@ -316,7 +408,7 @@ public AccountCapsule getAccount(byte[] address) { if (parent != null) { accountCapsule = parent.getAccount(address); } else { - accountCapsule = getAccountStore().get(address); + accountCapsule = stateSource.getAccount(address); } if (accountCapsule != null) { @@ -336,11 +428,9 @@ public BytesCapsule getDynamicProperty(byte[] word) { if (parent != null) { bytesCapsule = parent.getDynamicProperty(word); } else { - try { - bytesCapsule = getDynamicPropertiesStore().get(word); - } catch (BadItemException | ItemNotFoundException e) { + bytesCapsule = stateSource.getDynamicProperty(word); + if (bytesCapsule == null) { logger.warn("Not found dynamic property:" + Strings.fromUTF8ByteArray(word)); - bytesCapsule = null; } } @@ -392,7 +482,7 @@ public VotesCapsule getVotes(byte[] address) { @Override public WitnessCapsule getWitness(byte[] address) { - return witnessStore.get(address); + return getWitnessStore().get(address); } @Override @@ -508,7 +598,7 @@ public ContractCapsule getContract(byte[] address) { if (parent != null) { contractCapsule = parent.getContract(address); } else { - contractCapsule = getContractStore().get(address); + contractCapsule = stateSource.getContract(address); } if (contractCapsule != null) { @@ -528,7 +618,7 @@ public ContractStateCapsule getContractState(byte[] address) { if (parent != null) { contractStateCapsule = parent.getContractState(address); } else { - contractStateCapsule = getContractStateStore().get(address); + contractStateCapsule = stateSource.getContractState(address); } if (contractStateCapsule != null) { @@ -657,11 +747,7 @@ public byte[] getCode(byte[] address) { if (parent != null) { code = parent.getCode(address); } else { - if (null == getCodeStore().get(address)) { - code = null; - } else { - code = getCodeStore().get(address).getData(); - } + code = stateSource.getCode(address); } if (code != null) { codeCache.put(key, Value.create(code)); @@ -715,7 +801,8 @@ public Storage getStorage(byte[] address) { storage = parentStorage; } } else { - storage = new Storage(address, getStorageRowStore()); + StorageRowStore persistenceStore = stateSource.isReadOnly() ? null : getStorageRowStore(); + storage = new Storage(address, persistenceStore, stateSource::getStorageRow); } ContractCapsule contract = getContract(address); if (contract != null) { @@ -764,6 +851,9 @@ public void setParent(Repository repository) { @Override public void commit() { + if (parent == null && readOnlyRoot) { + throw new IllegalStateException("Historical Repository root cannot be committed"); + } Repository repository = null; if (parent != null) { repository = parent; @@ -891,6 +981,10 @@ public byte[] getBlackHoleAddress() { @Override public BlockCapsule getBlockByNum(long num) { + if (readOnlyRoot) { + throw new HistoricalCapabilityException( + "BLOCKHASH history is not supported by historical execution"); + } try { Sha256Hash hash = getBlockIdByNum(num); BlockCapsule block = this.khaosDb.getBlock(hash); diff --git a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryProvider.java b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryProvider.java new file mode 100644 index 00000000000..15f6c953290 --- /dev/null +++ b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryProvider.java @@ -0,0 +1,10 @@ +package org.tron.core.vm.repository; + +import org.tron.core.db.TransactionContext; + +/** Creates the request-owned root Repository used by one VM execution. */ +@FunctionalInterface +public interface RepositoryProvider { + + Repository createRoot(TransactionContext context); +} diff --git a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryStateSource.java b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryStateSource.java new file mode 100644 index 00000000000..7cc36a020b1 --- /dev/null +++ b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryStateSource.java @@ -0,0 +1,25 @@ +package org.tron.core.vm.repository; + +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.BytesCapsule; +import org.tron.core.capsule.ContractCapsule; +import org.tron.core.capsule.ContractStateCapsule; +import org.tron.core.capsule.StorageRowCapsule; + +/** Root-miss state reads for a Repository overlay. */ +interface RepositoryStateSource { + + AccountCapsule getAccount(byte[] address); + + BytesCapsule getDynamicProperty(byte[] key); + + ContractCapsule getContract(byte[] address); + + ContractStateCapsule getContractState(byte[] address); + + byte[] getCode(byte[] address); + + StorageRowCapsule getStorageRow(byte[] physicalKey); + + boolean isReadOnly(); +} diff --git a/chainbase/src/main/java/org/tron/common/utils/StorageUtils.java b/chainbase/src/main/java/org/tron/common/utils/StorageUtils.java index 0c7c77bd23f..e6821f916d1 100644 --- a/chainbase/src/main/java/org/tron/common/utils/StorageUtils.java +++ b/chainbase/src/main/java/org/tron/common/utils/StorageUtils.java @@ -1,6 +1,5 @@ package org.tron.common.utils; -import static org.tron.common.parameter.CommonParameter.ENERGY_LIMIT_HARD_FORK; import static org.tron.core.db.common.DbSourceInter.LEVELDB; import java.io.File; @@ -9,6 +8,7 @@ import org.slf4j.LoggerFactory; import org.tron.common.parameter.CommonParameter; import org.tron.core.Constant; +import org.tron.core.vm.config.VMConfig; public class StorageUtils { @@ -16,7 +16,7 @@ public class StorageUtils { private static final org.slf4j.Logger levelDbLogger = LoggerFactory.getLogger(LEVELDB); public static boolean getEnergyLimitHardFork() { - return ENERGY_LIMIT_HARD_FORK; + return VMConfig.getEnergyLimitHardFork(); } public static String getOutputDirectoryByDbName(String dbName) { diff --git a/chainbase/src/main/java/org/tron/core/db/TransactionContext.java b/chainbase/src/main/java/org/tron/core/db/TransactionContext.java index f2b467c7fc0..29c4f66c2ea 100644 --- a/chainbase/src/main/java/org/tron/core/db/TransactionContext.java +++ b/chainbase/src/main/java/org/tron/core/db/TransactionContext.java @@ -4,26 +4,53 @@ import org.tron.common.runtime.ProgramResult; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.db2.archive.HistoricalQuerySession; import org.tron.core.store.StoreFactory; @Data public class TransactionContext { + public enum ExecutionMode { + CURRENT_CONSENSUS, + CURRENT_CONSTANT, + HISTORICAL_CONSTANT + } + private BlockCapsule blockCap; private TransactionCapsule trxCap; private StoreFactory storeFactory; private ProgramResult programResult = new ProgramResult(); private boolean isStatic; private boolean eventPluginLoaded; + private final ExecutionMode executionMode; + private final HistoricalQuerySession historicalQuerySession; public TransactionContext(BlockCapsule blockCap, TransactionCapsule trxCap, StoreFactory storeFactory, boolean isStatic, boolean eventPluginLoaded) { + this(blockCap, trxCap, storeFactory, isStatic, eventPluginLoaded, + isStatic ? ExecutionMode.CURRENT_CONSTANT : ExecutionMode.CURRENT_CONSENSUS, null); + } + + public TransactionContext(BlockCapsule blockCap, TransactionCapsule trxCap, + StoreFactory storeFactory, boolean isStatic, boolean eventPluginLoaded, + ExecutionMode executionMode, HistoricalQuerySession historicalQuerySession) { this.blockCap = blockCap; this.trxCap = trxCap; this.storeFactory = storeFactory; this.isStatic = isStatic; this.eventPluginLoaded = eventPluginLoaded; + this.executionMode = java.util.Objects.requireNonNull(executionMode, "executionMode"); + this.historicalQuerySession = historicalQuerySession; + if (executionMode == ExecutionMode.HISTORICAL_CONSTANT) { + if (!isStatic || historicalQuerySession == null) { + throw new IllegalArgumentException( + "HISTORICAL_CONSTANT requires static execution and a historical query session"); + } + } else if (historicalQuerySession != null) { + throw new IllegalArgumentException( + "A historical query session is only valid for HISTORICAL_CONSTANT execution"); + } } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java index 427230d5c59..a8c48324e8a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java @@ -18,6 +18,7 @@ public final class ArchiveReadContext implements Closeable { private final ArchiveReadSnapshot snapshot; + private final Closeable owner; private final Map> adapters; private final HistoricalAccountAssetBalanceResolver accountAssetResolver = new HistoricalAccountAssetBalanceResolver(); @@ -26,8 +27,9 @@ public final class ArchiveReadContext implements Closeable { private boolean closed; private ArchiveReadContext(ArchiveReadSnapshot snapshot, - Collection> adapters) { + Collection> adapters, Closeable owner) { this.snapshot = Objects.requireNonNull(snapshot, "snapshot"); + this.owner = Objects.requireNonNull(owner, "owner"); this.adapters = validateAdapters(adapters); } @@ -35,13 +37,25 @@ private ArchiveReadContext(ArchiveReadSnapshot snapshot, public static ArchiveReadContext open(ArchiveReadSnapshot snapshot, Collection> adapters) throws IOException { try { - return new ArchiveReadContext(snapshot, adapters); + return new ArchiveReadContext(snapshot, adapters, snapshot); } catch (RuntimeException failure) { closeAfterFailedOpen(snapshot, failure); throw failure; } } + /** Takes ownership of {@code lease} so closing this context also releases gate accounting. */ + public static ArchiveReadContext open(ArchiveRuntimeQueryGate.Lease lease, + Collection> adapters) throws IOException { + Objects.requireNonNull(lease, "lease"); + try { + return new ArchiveReadContext(lease.getSnapshot(), adapters, lease); + } catch (RuntimeException failure) { + closeAfterFailedOpen(lease, failure); + throw failure; + } + } + public synchronized HistoricalStore store(StoreAdapter adapter) { ensureOpen(); Objects.requireNonNull(adapter, "adapter"); @@ -56,6 +70,17 @@ public Set getAdapterDbNames() { return Collections.unmodifiableSet(new LinkedHashSet<>(adapters.keySet())); } + /** Internal exact physical read; public RPC layers must use typed resolvers instead. */ + public synchronized OldValue getExact(String dbName, byte[] physicalRawKey) + throws IOException { + ensureOpen(); + if (!adapters.containsKey(Objects.requireNonNull(dbName, "dbName"))) { + throw new IllegalArgumentException( + "Store adapter does not belong to this archive read context: " + dbName); + } + return snapshot.get(dbName, Objects.requireNonNull(physicalRawKey, "physicalRawKey")); + } + public long getTargetBlock() { return snapshot.getTargetBlock(); } @@ -64,6 +89,11 @@ public long getPinnedBlock() { return snapshot.getPinnedBlock(); } + public synchronized void requirePinnedIdentity() { + ensureOpen(); + snapshot.requirePinnedIdentity(); + } + /** Resolves exact Account bytes and one P66-aware token balance from this request snapshot. */ public synchronized HistoricalAccountAssetBalanceResolver.Result resolveAccountAsset( byte[] address, String tokenId) throws IOException { @@ -107,7 +137,7 @@ public synchronized Optional getStorage(byte[] contractAddress, byte[] l public synchronized void close() throws IOException { if (!closed) { closed = true; - snapshot.close(); + owner.close(); } } @@ -140,13 +170,13 @@ private static Map> validateAdapters( return Collections.unmodifiableMap(indexed); } - private static void closeAfterFailedOpen(ArchiveReadSnapshot snapshot, + private static void closeAfterFailedOpen(Closeable owner, RuntimeException failure) throws IOException { - if (snapshot == null) { + if (owner == null) { return; } try { - snapshot.close(); + owner.close(); } catch (IOException closeFailure) { failure.addSuppressed(closeFailure); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQueryBudgetException.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQueryBudgetException.java new file mode 100644 index 00000000000..abfc65c1337 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQueryBudgetException.java @@ -0,0 +1,9 @@ +package org.tron.core.db2.archive; + +/** A request-owned historical view exceeded its configured resource budget. */ +public class HistoricalQueryBudgetException extends ArchivePersistenceException { + + public HistoricalQueryBudgetException(String message) { + super(message); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQuerySession.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQuerySession.java new file mode 100644 index 00000000000..dbe01f6aea3 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQuerySession.java @@ -0,0 +1,250 @@ +package org.tron.core.db2.archive; + +import com.google.protobuf.InvalidProtocolBufferException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.tron.core.db2.archive.ArchiveReadContext.StoreAdapter; +import org.tron.core.store.StorageRowKeyCodec; +import org.tron.protos.Protocol.Account; +import org.tron.protos.contract.SmartContractOuterClass.ContractState; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +/** One request-owned exact-27 typed view of a canonical historical block-final state. */ +public final class HistoricalQuerySession implements AutoCloseable { + + public static final class Limits { + + private final long maxReads; + private final long maxBytes; + private final long timeoutMillis; + + public Limits(long maxReads, long maxBytes, long timeoutMillis) { + if (maxReads <= 0 || maxBytes <= 0 || timeoutMillis <= 0) { + throw new IllegalArgumentException("Historical query limits must be positive"); + } + this.maxReads = maxReads; + this.maxBytes = maxBytes; + this.timeoutMillis = timeoutMillis; + } + + public static Limits defaults() { + return new Limits(100_000L, 64L * 1024L * 1024L, 10_000L); + } + } + + private static final Map> RAW_ADAPTERS = rawAdapters(); + private static final List> EXACT_ADAPTERS = exactAdapters(); + + private final ArchiveReadContext context; + private final byte[] targetBlockHash; + private final Limits limits; + private final long deadlineNanos; + private long reads; + private long bytes; + private boolean closed; + + private HistoricalQuerySession(ArchiveReadContext context, byte[] targetBlockHash, + Limits limits) { + this.context = Objects.requireNonNull(context, "context"); + this.targetBlockHash = copyHash(targetBlockHash); + this.limits = Objects.requireNonNull(limits, "limits"); + this.deadlineNanos = System.nanoTime() + + java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(limits.timeoutMillis); + context.requirePinnedIdentity(); + } + + /** Takes ownership of {@code lease}, including if session construction fails. */ + public static HistoricalQuerySession open(ArchiveRuntimeQueryGate.Lease lease, + byte[] targetBlockHash) throws IOException { + return open(lease, targetBlockHash, Limits.defaults()); + } + + /** Takes ownership of {@code lease}, including if session construction fails. */ + public static HistoricalQuerySession open(ArchiveRuntimeQueryGate.Lease lease, + byte[] targetBlockHash, Limits limits) throws IOException { + ArchiveReadContext context = ArchiveReadContext.open(lease, EXACT_ADAPTERS); + try { + return new HistoricalQuerySession(context, targetBlockHash, limits); + } catch (RuntimeException failure) { + try { + context.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } + } + + public long getTargetBlock() { + ensureOpen(); + return context.getTargetBlock(); + } + + public long getPinnedBlock() { + ensureOpen(); + return context.getPinnedBlock(); + } + + public byte[] getTargetBlockHash() { + ensureOpen(); + return Arrays.copyOf(targetBlockHash, targetBlockHash.length); + } + + public Optional getAccount(byte[] address) { + byte[] key = copyKey(address, "address"); + Optional encoded = getExact("account", key); + if (!encoded.isPresent()) { + return Optional.empty(); + } + Account account = decodeAccount(encoded.get()); + if (!account.getAddress().equals(com.google.protobuf.ByteString.copyFrom(key))) { + throw new ArchivePersistenceException( + "Historical Account address does not match the physical key"); + } + return Optional.of(account); + } + + public Optional getContract(byte[] address) { + Optional encoded = getExact("contract", copyKey(address, "address")); + return encoded.map(HistoricalQuerySession::decodeContract); + } + + public Optional getCode(byte[] address) { + return getExact("code", copyKey(address, "address")); + } + + public Optional getContractState(byte[] address) { + Optional encoded = getExact("contract-state", copyKey(address, "address")); + return encoded.map(HistoricalQuerySession::decodeContractState); + } + + /** Contract metadata and storage-row lookup are resolved by the same pinned context. */ + public Optional getStorage(byte[] contractAddress, byte[] logicalSlot) { + ensureOpen(); + byte[] address = copyKey(contractAddress, "contractAddress"); + byte[] slot = copyKey(logicalSlot, "logicalSlot"); + SmartContract contract = getContract(address).orElseThrow(() -> + new ArchivePersistenceException( + "Historical Contract is required to derive storage-row key")); + byte[] transactionHash = contract.getTrxHash().isEmpty() + ? null : contract.getTrxHash().toByteArray(); + byte[] physicalKey = StorageRowKeyCodec.physicalKey(address, slot, + contract.getVersion(), transactionHash); + return getExact("storage-row", physicalKey); + } + + public Optional getExact(String dbName, byte[] physicalRawKey) { + ensureOpen(); + requireReadBudget(); + try { + OldValue value = context.getExact(dbName, copyKey(physicalRawKey, "physicalRawKey")); + if (value.isPresent()) { + accountBytes(value.getValue().length); + } + return value.isPresent() ? Optional.of(value.getValue()) : Optional.empty(); + } catch (IOException failure) { + throw new ArchivePersistenceException("Failed to read historical Store " + dbName, + failure); + } + } + + public void requirePinnedIdentity() { + ensureOpen(); + context.requirePinnedIdentity(); + } + + @Override + public synchronized void close() { + if (closed) { + return; + } + closed = true; + try { + context.close(); + } catch (IOException failure) { + throw new ArchivePersistenceException("Failed to close historical query session", failure); + } + } + + private synchronized void ensureOpen() { + if (closed) { + throw new IllegalStateException("Historical query session is closed"); + } + } + + private synchronized void requireReadBudget() { + if (System.nanoTime() - deadlineNanos > 0) { + throw new HistoricalQueryBudgetException("Historical query deadline exceeded"); + } + if (reads >= limits.maxReads) { + throw new HistoricalQueryBudgetException("Historical query read budget exceeded"); + } + reads++; + } + + private synchronized void accountBytes(long additionalBytes) { + if (additionalBytes > limits.maxBytes - bytes) { + throw new HistoricalQueryBudgetException("Historical query byte budget exceeded"); + } + bytes += additionalBytes; + } + + private static Map> rawAdapters() { + Map> adapters = new LinkedHashMap<>(); + for (String dbName : ArchiveStoreScope.getStateDatabases()) { + adapters.put(dbName, StoreAdapter.define(dbName, HistoricalQuerySession::copyValue)); + } + return Collections.unmodifiableMap(adapters); + } + + private static List> exactAdapters() { + return Collections.unmodifiableList(new ArrayList<>(RAW_ADAPTERS.values())); + } + + private static Account decodeAccount(byte[] value) { + try { + return Account.parseFrom(value); + } catch (InvalidProtocolBufferException failure) { + throw new ArchivePersistenceException("Historical Account cannot be decoded", failure); + } + } + + private static SmartContract decodeContract(byte[] value) { + try { + return SmartContract.parseFrom(value); + } catch (InvalidProtocolBufferException failure) { + throw new ArchivePersistenceException("Historical Contract cannot be decoded", failure); + } + } + + private static ContractState decodeContractState(byte[] value) { + try { + return ContractState.parseFrom(value); + } catch (InvalidProtocolBufferException failure) { + throw new ArchivePersistenceException( + "Historical ContractState cannot be decoded", failure); + } + } + + private static byte[] copyHash(byte[] hash) { + if (hash == null || hash.length != 32) { + throw new IllegalArgumentException("targetBlockHash must be exactly 32 bytes"); + } + return Arrays.copyOf(hash, hash.length); + } + + private static byte[] copyKey(byte[] value, String name) { + return Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + } + + private static byte[] copyValue(byte[] value) { + return Arrays.copyOf(value, value.length); + } +} diff --git a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index 0f74f20d379..ef1ee26ad84 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -8,7 +8,11 @@ import com.google.protobuf.ByteString; import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Optional; +import java.util.function.LongSupplier; import java.util.stream.IntStream; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -29,6 +33,62 @@ @Component public class DynamicPropertiesStore extends TronStoreWithRevoking { + private static final Map LONG_PROPERTY_DEFAULTS = + createLongPropertyDefaults(); + + private static Map createLongPropertyDefaults() { + // Keep this registry aligned with every typed long getter below that uses Optional.orElse. + Map defaults = new LinkedHashMap<>(); + defaults.put("WITNESS_127_PAY_PER_BLOCK", () -> 16_000_000L); + defaults.put("CURRENT_CYCLE_NUMBER", () -> 0L); + defaults.put("ALLOW_TVM_SHANGHAI", + () -> CommonParameter.getInstance().getAllowTvmShangHai()); + defaults.put("ALLOW_CANCEL_ALL_UNFREEZE_V2", + () -> CommonParameter.getInstance().getAllowCancelAllUnfreezeV2()); + defaults.put("MAX_DELEGATE_LOCK_PERIOD", + () -> DELEGATE_PERIOD / BLOCK_PRODUCED_INTERVAL); + defaults.put("ALLOW_OLD_REWARD_OPT", + () -> CommonParameter.getInstance().getAllowOldRewardOpt()); + defaults.put("ALLOW_ENERGY_ADJUSTMENT", + () -> CommonParameter.getInstance().getAllowEnergyAdjustment()); + defaults.put("MAX_CREATE_ACCOUNT_TX_SIZE", + () -> CommonParameter.getInstance().getMaxCreateAccountTxSize()); + defaults.put("ALLOW_STRICT_MATH", + () -> CommonParameter.getInstance().getAllowStrictMath()); + defaults.put("CONSENSUS_LOGIC_OPTIMIZATION", + () -> CommonParameter.getInstance().getConsensusLogicOptimization()); + defaults.put("ALLOW_TVM_CANCUN", + () -> CommonParameter.getInstance().getAllowTvmCancun()); + defaults.put("ALLOW_TVM_BLOB", + () -> CommonParameter.getInstance().getAllowTvmBlob()); + defaults.put("ALLOW_TVM_SELFDESTRUCT_RESTRICTION", () -> 0L); + defaults.put("PROPOSAL_EXPIRE_TIME", + () -> CommonParameter.getInstance().getProposalExpireTime()); + defaults.put("ALLOW_TVM_OSAKA", () -> 0L); + defaults.put("ALLOW_TVM_PRAGUE", () -> 0L); + defaults.put("BLOCK_HASH_HISTORY_INSTALLED", () -> 0L); + defaults.put("ALLOW_HARDEN_RESOURCE_CALCULATION", () -> 0L); + defaults.put("ALLOW_HARDEN_EXCHANGE_CALCULATION", () -> 0L); + defaults.put("TURKISH_KEY_MIGRATION_DONE", () -> 0L); + return Collections.unmodifiableMap(defaults); + } + + /** + * Resolves the missing-key defaults defined by the typed long getters in this Store. + * Existing values always remain authoritative; callers use this map only for an absent key. + */ + public static Long getLongPropertyDefault(String key) { + LongSupplier supplier = LONG_PROPERTY_DEFAULTS.get(key); + return supplier == null ? null : supplier.getAsLong(); + } + + /** Returns a resolved, immutable snapshot for auditing and completeness tests. */ + public static Map getLongPropertyDefaults() { + Map resolved = new LinkedHashMap<>(); + LONG_PROPERTY_DEFAULTS.forEach((key, supplier) -> resolved.put(key, supplier.getAsLong())); + return Collections.unmodifiableMap(resolved); + } + private static final byte[] LATEST_BLOCK_HEADER_TIMESTAMP = "latest_block_header_timestamp" .getBytes(); private static final byte[] LATEST_BLOCK_HEADER_NUMBER = "latest_block_header_number".getBytes(); diff --git a/common/src/main/java/org/tron/core/vm/config/VMConfig.java b/common/src/main/java/org/tron/core/vm/config/VMConfig.java index 304ced33698..129f774c920 100644 --- a/common/src/main/java/org/tron/core/vm/config/VMConfig.java +++ b/common/src/main/java/org/tron/core/vm/config/VMConfig.java @@ -46,6 +46,7 @@ public static class Snapshot { public boolean allowTvmSelfdestructRestriction; public boolean allowTvmOsaka; public boolean allowHardenResourceCalculation; + public boolean energyLimitHardFork; } // HEAD / block-processing config, written by the consensus path; read by everyone with no @@ -95,6 +96,7 @@ public static boolean vmTraceCompressed() { public static void initVmHardFork(boolean pass) { CommonParameter.ENERGY_LIMIT_HARD_FORK = pass; + globalSnapshot.energyLimitHardFork = pass; } // The init* setters below mutate the global (HEAD) config in place. They are kept for tests and @@ -205,7 +207,9 @@ public static void initAllowHardenResourceCalculation(long allow) { } public static boolean getEnergyLimitHardFork() { - return CommonParameter.ENERGY_LIMIT_HARD_FORK; + Snapshot local = localSnapshot.get(); + return local == null ? CommonParameter.ENERGY_LIMIT_HARD_FORK + : local.energyLimitHardFork; } public static boolean allowTvmTransferTrc10() { diff --git a/framework/src/main/java/org/tron/common/runtime/RuntimeImpl.java b/framework/src/main/java/org/tron/common/runtime/RuntimeImpl.java index 3dccfc5d146..8091d7109c5 100644 --- a/framework/src/main/java/org/tron/common/runtime/RuntimeImpl.java +++ b/framework/src/main/java/org/tron/common/runtime/RuntimeImpl.java @@ -12,6 +12,7 @@ import org.tron.core.db.TransactionContext; import org.tron.core.exception.ContractExeException; import org.tron.core.exception.ContractValidateException; +import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program; import org.tron.core.vm.program.Program.BadJumpDestinationException; import org.tron.core.vm.program.Program.IllegalOperationException; @@ -22,6 +23,7 @@ import org.tron.core.vm.program.Program.PrecompiledContractException; import org.tron.core.vm.program.Program.StackTooLargeException; import org.tron.core.vm.program.Program.StackTooSmallException; +import org.tron.core.vm.repository.HistoricalRepositoryProvider; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result.contractResult; @@ -44,14 +46,24 @@ public void execute(TransactionContext context) switch (contractType.getNumber()) { case ContractType.TriggerSmartContract_VALUE: case ContractType.CreateSmartContract_VALUE: - actuator2 = new VMActuator(context.isStatic()); + actuator2 = context.getExecutionMode() + == TransactionContext.ExecutionMode.HISTORICAL_CONSTANT + ? new VMActuator(true, HistoricalRepositoryProvider.INSTANCE) + : new VMActuator(context.isStatic()); break; default: actuatorList = ActuatorCreator.getINSTANCE().createActuator(context.getTrxCap()); } if (actuator2 != null) { - actuator2.validate(context); - actuator2.execute(context); + try { + actuator2.validate(context); + actuator2.execute(context); + } finally { + if (context.getExecutionMode() + == TransactionContext.ExecutionMode.HISTORICAL_CONSTANT) { + VMConfig.clearLocalSnapshot(); + } + } } else { for (Actuator act : actuatorList) { act.validate(); @@ -133,4 +145,3 @@ private void setResultCode(ProgramResult result) { } } - diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index b99142e1d43..dd6efa16075 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -166,6 +166,8 @@ import org.tron.core.db.EnergyProcessor; import org.tron.core.db.Manager; import org.tron.core.db.TransactionContext; +import org.tron.core.db.TransactionContext.ExecutionMode; +import org.tron.core.db2.archive.HistoricalQuerySession; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.Chainbase.Cursor; import org.tron.core.exception.AccountResourceInsufficientException; @@ -204,6 +206,7 @@ import org.tron.core.utils.TransactionUtil; import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program; +import org.tron.core.vm.repository.HistoricalRepositoryProvider; import org.tron.core.zen.ShieldedTRC20ParametersBuilder; import org.tron.core.zen.ShieldedTRC20ParametersBuilder.ShieldedTRC20ParametersType; import org.tron.core.zen.ZenTransactionBuilder; @@ -3153,10 +3156,40 @@ public Transaction callConstantContract(TransactionCapsule trxCap, headBlock = blockCapsuleList.get(0).getInstance(); } - BlockCapsule headBlockCapsule = new BlockCapsule(headBlock); - TransactionContext context = new TransactionContext(headBlockCapsule, trxCap, - StoreFactory.getInstance(), true, false); - VMActuator vmActuator = new VMActuator(true); + return executeConstantContract(trxCap, builder, retBuilder, isEstimating, + new BlockCapsule(headBlock), null); + } + + public Transaction callHistoricalConstantContract(TransactionCapsule trxCap, + Builder builder, Return.Builder retBuilder, HistoricalQuerySession session) + throws ContractValidateException, ContractExeException, HeaderNotFound, VMIllegalException { + if (!Args.getInstance().isSupportConstant()) { + throw new ContractValidateException("this node does not support constant"); + } + BlockCapsule targetBlock; + try { + targetBlock = chainBaseManager.getBlockByNum(session.getTargetBlock()); + } catch (BadItemException | ItemNotFoundException failure) { + throw new HeaderNotFound("historical block not found"); + } + if (targetBlock == null + || !Arrays.equals(targetBlock.getBlockId().getBytes(), session.getTargetBlockHash())) { + throw new HeaderNotFound("historical block identity changed"); + } + session.requirePinnedIdentity(); + return executeConstantContract(trxCap, builder, retBuilder, false, targetBlock, session); + } + + private Transaction executeConstantContract(TransactionCapsule trxCap, + Builder builder, Return.Builder retBuilder, boolean isEstimating, + BlockCapsule blockCapsule, HistoricalQuerySession session) + throws ContractValidateException, ContractExeException, VMIllegalException { + TransactionContext context = session == null + ? new TransactionContext(blockCapsule, trxCap, StoreFactory.getInstance(), true, false) + : new TransactionContext(blockCapsule, trxCap, StoreFactory.getInstance(), true, false, + ExecutionMode.HISTORICAL_CONSTANT, session); + VMActuator vmActuator = session == null + ? new VMActuator(true) : new VMActuator(true, HistoricalRepositoryProvider.INSTANCE); try { vmActuator.validate(context); diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 150525d5752..46c7c5f4ec4 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -124,6 +124,7 @@ import org.tron.core.db2.archive.HistoricalAccountAssetBalanceResolver; import org.tron.core.db2.archive.HistoricalAccountAssetPrefixResolver; import org.tron.core.db2.archive.HistoricalAccountBalanceReader; +import org.tron.core.db2.archive.HistoricalQuerySession; import org.tron.core.db2.archive.OldValue; import org.tron.core.db2.archive.SnapshotOldValueCollector; import org.tron.core.db2.archive.SnapshotPathStateTransitionCollector; @@ -928,6 +929,52 @@ public HistoricalAccountBalanceReader.Result getArchiveAccountBalance(long block } } + public boolean isArchiveHistoricalQueryEnabled() { + return stateArchiveRuntime != null; + } + + /** Opens one canonical request-owned exact-27 historical query view. */ + public HistoricalQuerySession openArchiveHistoricalQuery(long blockNumber, + byte[] expectedBlockHash) throws ItemNotFoundException, BadItemException { + Objects.requireNonNull(expectedBlockHash, "expectedBlockHash"); + if (expectedBlockHash.length != 32) { + throw new IllegalArgumentException("expectedBlockHash must be exactly 32 bytes"); + } + StateArchiveRuntimeOwner runtime = stateArchiveRuntime; + if (runtime == null) { + throw new IllegalStateException("Experimental state archive is disabled"); + } + + BlockCapsule beforePin = chainBaseManager.getBlockByNum(blockNumber); + byte[] canonicalHash = beforePin.getBlockId().getBytes(); + if (!Arrays.equals(expectedBlockHash, canonicalHash)) { + throw new IllegalArgumentException("Historical block number and hash do not match"); + } + + HistoricalQuerySession session; + try { + session = HistoricalQuerySession.open(runtime.pinHistoricalState(blockNumber), + canonicalHash); + } catch (java.io.IOException failure) { + throw new org.tron.core.db2.archive.ArchivePersistenceException( + "Failed to open request-owned historical query session", failure); + } + + try { + BlockCapsule afterPin = chainBaseManager.getBlockByNum(blockNumber); + if (!Arrays.equals(canonicalHash, afterPin.getBlockId().getBytes())) { + session.close(); + throw new org.tron.core.db2.archive.ArchivePersistenceException( + "Canonical historical block changed while opening query session"); + } + session.requirePinnedIdentity(); + return session; + } catch (ItemNotFoundException | BadItemException | RuntimeException failure) { + session.close(); + throw failure; + } + } + /** Measures the current experimental exact-only serving index at its readable fixed point. */ public StateArchiveRuntimeOwner.ServingIndexInspection inspectArchiveServingIndex() { StateArchiveRuntimeOwner runtime = stateArchiveRuntime; diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index 6be47886117..70ef1026aad 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -63,6 +63,7 @@ import org.tron.core.capsule.TransactionCapsule; import org.tron.core.config.args.Args; import org.tron.core.db.Manager; +import org.tron.core.db2.archive.HistoricalQuerySession; import org.tron.core.db2.core.Chainbase; import org.tron.core.exception.BadItemException; import org.tron.core.exception.ContractExeException; @@ -389,6 +390,28 @@ private void requireLatestBlockTag(String blockNumOrTag) throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); } + private HistoricalQuerySession openHistoricalQuery(String blockNumOrTag) + throws JsonRpcInvalidParamsException { + if (JsonRpcApiUtil.isBlockTag(blockNumOrTag)) { + throw new JsonRpcInvalidParamsException(TAG_NOT_SUPPORT_ERROR); + } + long blockNumber = parseBlockNumber(blockNumOrTag); + if (manager == null || !manager.isArchiveHistoricalQueryEnabled()) { + throw new JsonRpcInvalidParamsException(QUANTITY_NOT_SUPPORT_ERROR); + } + Block block = wallet.getBlockByNum(blockNumber); + if (block == null) { + throw new JsonRpcInvalidParamsException(NO_BLOCK_HEADER); + } + byte[] blockHash = new BlockCapsule(block).getBlockId().getBytes(); + try { + return manager.openArchiveHistoricalQuery(blockNumber, blockHash); + } catch (ItemNotFoundException | BadItemException | RuntimeException failure) { + throw new JsonRpcInvalidParamsException( + "historical state unavailable: " + failure.getMessage(), failure); + } + } + private Block getBlockByJsonHash(String blockHash) throws JsonRpcInvalidParamsException { byte[] bHash = hashToByteArray(blockHash); return wallet.getBlockById(ByteString.copyFrom(bHash)); @@ -449,18 +472,20 @@ public String getLatestBlockNum() { @Override public String getTrxBalance(String address, String blockNumOrTag) throws JsonRpcInvalidParamsException { - requireLatestBlockTag(blockNumOrTag); - - byte[] addressData = addressCompatibleToByteArray(address); - - Account account = Account.newBuilder().setAddress(ByteString.copyFrom(addressData)).build(); - Account reply = wallet.getAccount(account); - long balance = 0; - - if (reply != null) { - balance = reply.getBalance(); + if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { + byte[] addressData = addressCompatibleToByteArray(address); + Account account = Account.newBuilder().setAddress(ByteString.copyFrom(addressData)).build(); + Account reply = wallet.getAccount(account); + return ByteArray.toJsonHex(reply == null ? 0 : reply.getBalance()); + } + try (HistoricalQuerySession session = openHistoricalQuery(blockNumOrTag)) { + byte[] addressData = addressCompatibleToByteArray(address); + return ByteArray.toJsonHex(session.getAccount(addressData) + .map(Account::getBalance).orElse(0L)); + } catch (RuntimeException failure) { + throw new JsonRpcInvalidParamsException( + "historical state unavailable: " + failure.getMessage(), failure); } - return ByteArray.toJsonHex(balance); } private void callTriggerConstantContract(byte[] ownerAddressByte, byte[] contractAddressByte, @@ -488,6 +513,23 @@ private void callTriggerConstantContract(byte[] ownerAddressByte, byte[] contrac retBuilder.setResult(true).setCode(response_code.SUCCESS); } + private void callTriggerHistoricalConstantContract(byte[] ownerAddressByte, + byte[] contractAddressByte, long value, byte[] data, + TransactionExtention.Builder trxExtBuilder, Return.Builder retBuilder, + HistoricalQuerySession session) + throws ContractValidateException, ContractExeException, HeaderNotFound, VMIllegalException { + TriggerSmartContract triggerContract = triggerCallContract(ownerAddressByte, + contractAddressByte, value, data, 0, null); + TransactionCapsule trxCap = wallet.createTransactionCapsule(triggerContract, + ContractType.TriggerSmartContract); + Transaction trx = wallet.callHistoricalConstantContract(trxCap, trxExtBuilder, retBuilder, + session); + trxExtBuilder.setTransaction(trx); + trxExtBuilder.setTxid(trxCap.getTransactionId().getByteString()); + trxExtBuilder.setResult(retBuilder); + retBuilder.setResult(true).setCode(response_code.SUCCESS); + } + private void estimateEnergy(byte[] ownerAddressByte, byte[] contractAddressByte, long value, byte[] data, TransactionExtention.Builder trxExtBuilder, Return.Builder retBuilder, EstimateEnergyMessage.Builder estimateBuilder) @@ -549,14 +591,25 @@ static String tryDecodeRevertReason(byte[] resData) { */ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long value, byte[] data) throws JsonRpcInvalidRequestException, JsonRpcInternalException { + return call(ownerAddressByte, contractAddressByte, value, data, null); + } + + private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long value, + byte[] data, HistoricalQuerySession historicalSession) + throws JsonRpcInvalidRequestException, JsonRpcInternalException { TransactionExtention.Builder trxExtBuilder = TransactionExtention.newBuilder(); Return.Builder retBuilder = Return.newBuilder(); TransactionExtention trxExt; try { - callTriggerConstantContract(ownerAddressByte, contractAddressByte, value, data, - trxExtBuilder, retBuilder); + if (historicalSession == null) { + callTriggerConstantContract(ownerAddressByte, contractAddressByte, value, data, + trxExtBuilder, retBuilder); + } else { + callTriggerHistoricalConstantContract(ownerAddressByte, contractAddressByte, value, data, + trxExtBuilder, retBuilder, historicalSession); + } } catch (ContractValidateException | VMIllegalException e) { String errString = CONTRACT_VALIDATE_ERROR; @@ -603,20 +656,23 @@ private String call(byte[] ownerAddressByte, byte[] contractAddressByte, long va @Override public String getStorageAt(String address, String storageIdx, String blockNumOrTag) throws JsonRpcInvalidParamsException { - requireLatestBlockTag(blockNumOrTag); - - if (storageIdx == null || storageIdx.length() > MAX_STORAGE_KEY_HEX_LEN) { - throw new JsonRpcInvalidParamsException("invalid storage key value"); + if (!LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { + try (HistoricalQuerySession session = openHistoricalQuery(blockNumOrTag)) { + byte[] addressByte = addressCompatibleToByteArray(address); + DataWord index = parseStorageIndex(storageIdx); + if (!session.getContract(addressByte).isPresent()) { + return ByteArray.toJsonHex(new byte[32]); + } + byte[] value = session.getStorage(addressByte, index.getData()).orElse(new byte[32]); + return ByteArray.toJsonHex(new DataWord(value).getData()); + } catch (RuntimeException failure) { + throw new JsonRpcInvalidParamsException( + "historical state unavailable: " + failure.getMessage(), failure); + } } byte[] addressByte = addressCompatibleToByteArray(address); - - DataWord index; - try { - index = new DataWord(ByteArray.fromHexString(storageIdx)); - } catch (Exception e) { - throw new JsonRpcInvalidParamsException("invalid storage key value"); - } + DataWord index = parseStorageIndex(storageIdx); // get contract from contractStore BytesMessage.Builder build = BytesMessage.newBuilder(); @@ -635,10 +691,29 @@ public String getStorageAt(String address, String storageIdx, String blockNumOrT return ByteArray.toJsonHex(value == null ? new byte[32] : value.getData()); } + private DataWord parseStorageIndex(String storageIdx) throws JsonRpcInvalidParamsException { + if (storageIdx == null || storageIdx.length() > MAX_STORAGE_KEY_HEX_LEN) { + throw new JsonRpcInvalidParamsException("invalid storage key value"); + } + try { + return new DataWord(ByteArray.fromHexString(storageIdx)); + } catch (Exception failure) { + throw new JsonRpcInvalidParamsException("invalid storage key value", failure); + } + } + @Override public String getABIOfSmartContract(String contractAddress, String blockNumOrTag) throws JsonRpcInvalidParamsException { - requireLatestBlockTag(blockNumOrTag); + if (!LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { + try (HistoricalQuerySession session = openHistoricalQuery(blockNumOrTag)) { + byte[] addressData = addressCompatibleToByteArray(contractAddress); + return session.getCode(addressData).map(ByteArray::toJsonHex).orElse("0x"); + } catch (RuntimeException failure) { + throw new JsonRpcInvalidParamsException( + "historical state unavailable: " + failure.getMessage(), failure); + } + } byte[] addressData = addressCompatibleToByteArray(contractAddress); @@ -1003,6 +1078,7 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) JsonRpcInternalException { String blockNumOrTag; + byte[] requestedBlockHash = null; if (blockParamObj instanceof HashMap) { HashMap paramMap; paramMap = (HashMap) blockParamObj; @@ -1014,6 +1090,11 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidParamsException(JSON_ERROR); } + if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { + return call(addressCompatibleToByteArray(transactionCall.getFrom()), + addressCompatibleToByteArray(transactionCall.getTo()), transactionCall.parseValue(), + ByteArray.fromHexString(transactionCall.resolveData())); + } long blockNumber = parseBlockNumber(blockNumOrTag); if (wallet.getBlockByNum(blockNumber) == null) { @@ -1027,27 +1108,42 @@ public String getCall(CallArguments transactionCall, Object blockParamObj) throw new JsonRpcInvalidParamsException(JSON_ERROR); } - if (getBlockByJsonHash(blockNumOrTag) == null) { + Block block = getBlockByJsonHash(blockNumOrTag); + if (block == null) { throw new JsonRpcInternalException(NO_BLOCK_HEADER_BY_HASH); } + requestedBlockHash = new BlockCapsule(block).getBlockId().getBytes(); + blockNumOrTag = ByteArray.toJsonHex( + block.getBlockHeader().getRawData().getNumber()); } else { throw new JsonRpcInvalidRequestException(JSON_ERROR); } - blockNumOrTag = LATEST_STR; } else if (blockParamObj instanceof String) { blockNumOrTag = (String) blockParamObj; } else { throw new JsonRpcInvalidRequestException(JSON_ERROR); } - requireLatestBlockTag(blockNumOrTag); - - byte[] addressData = addressCompatibleToByteArray(transactionCall.getFrom()); - byte[] contractAddressData = addressCompatibleToByteArray(transactionCall.getTo()); - - return call(addressData, contractAddressData, transactionCall.parseValue(), - ByteArray.fromHexString(transactionCall.resolveData())); + if (LATEST_STR.equalsIgnoreCase(blockNumOrTag)) { + return call(addressCompatibleToByteArray(transactionCall.getFrom()), + addressCompatibleToByteArray(transactionCall.getTo()), transactionCall.parseValue(), + ByteArray.fromHexString(transactionCall.resolveData())); + } + try (HistoricalQuerySession session = openHistoricalQuery(blockNumOrTag)) { + if (requestedBlockHash != null + && !Arrays.equals(requestedBlockHash, session.getTargetBlockHash())) { + throw new JsonRpcInvalidParamsException("historical block is not canonical"); + } + return call(addressCompatibleToByteArray(transactionCall.getFrom()), + addressCompatibleToByteArray(transactionCall.getTo()), transactionCall.parseValue(), + ByteArray.fromHexString(transactionCall.resolveData()), session); + } catch (JsonRpcInvalidParamsException failure) { + throw failure; + } catch (RuntimeException failure) { + throw new JsonRpcInvalidParamsException( + "historical state unavailable: " + failure.getMessage(), failure); + } } @Override diff --git a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java index 11f14051030..adf6aafd7b4 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/ArchiveReadSnapshotTest.java @@ -5,6 +5,10 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.google.protobuf.ByteString; import java.io.IOException; @@ -17,12 +21,21 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; import org.bouncycastle.util.encoders.Hex; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.ByteArray; +import org.tron.core.actuator.VMActuator; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.db.TransactionContext; +import org.tron.core.db.TransactionContext.ExecutionMode; import org.tron.core.db2.archive.ArchiveReadContext.HistoricalStore; import org.tron.core.db2.archive.ArchiveReadContext.StoreAdapter; import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; @@ -32,9 +45,25 @@ import org.tron.core.db2.archive.HistoricalRangeOverlay.Limits; import org.tron.core.db2.archive.HistoryIndexRecord.KeyGroup; import org.tron.core.db2.archive.P66AccountAssetCodec.Phase; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.core.store.StorageRowKeyCodec; +import org.tron.core.vm.HistoricalCapabilityException; +import org.tron.core.vm.HistoricalExecutionGuard; +import org.tron.core.vm.OperationActions; +import org.tron.core.vm.PrecompiledContracts; +import org.tron.core.vm.config.ConfigLoader; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.program.Program; +import org.tron.core.vm.repository.HistoricalRepositoryProvider; +import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.repository.RepositoryImpl; +import org.tron.protos.Protocol; import org.tron.protos.Protocol.Account; import org.tron.protos.Protocol.AccountType; +import org.tron.protos.contract.SmartContractOuterClass.CreateSmartContract; import org.tron.protos.contract.SmartContractOuterClass.SmartContract; +import org.tron.protos.contract.SmartContractOuterClass.TriggerSmartContract; public class ArchiveReadSnapshotTest { @@ -348,6 +377,292 @@ public void resolvesLogicalStorageWithHistoricalContractFromTheSameContext() thr } } + @Test + public void historicalQuerySessionOwnsExactTypedViewAndReleasesGateLease() throws Exception { + byte[] address = address(51); + byte[] slot = Hex.decode( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + byte[] transactionHash = hash(91); + SmartContract contract = SmartContract.newBuilder().setVersion(1) + .setTrxHash(ByteString.copyFrom(transactionHash)).build(); + byte[] physicalKey = StorageRowKeyCodec.physicalKey(address, slot, + contract.getVersion(), transactionHash); + byte[] historicalAccount = optimizedAccount(address, 77L); + byte[] historicalCode = bytes("historical-code"); + byte[] historicalStorage = hash(17); + + try (Fixture fixture = new Fixture( + temporaryFolder.newFolder("historical-query-session").toPath())) { + fixture.append(diff(1, + new DbGroup("account", Collections.singletonList( + new Entry(address, OldValue.present(historicalAccount)))), + new DbGroup("contract", Collections.singletonList( + new Entry(address, OldValue.present(contract.toByteArray())))), + new DbGroup("code", Collections.singletonList( + new Entry(address, OldValue.present(historicalCode)))), + new DbGroup("storage-row", Collections.singletonList( + new Entry(physicalKey, OldValue.present(historicalStorage)))), + new DbGroup("properties", historicalVmProperties()))); + fixture.sync(); + InMemoryLatest latest = InMemoryLatest.scoped(1, hash(1), Collections.emptyMap()); + ArchiveRuntimeQueryGate gate = new ArchiveRuntimeQueryGate( + target -> fixture.snapshot(target, latest)); + HistoricalQuerySession session = HistoricalQuerySession.open(gate.pin(0), hash(0)); + + assertEquals(1, gate.getActiveLeaseCount()); + assertEquals(0L, session.getTargetBlock()); + assertArrayEquals(hash(0), session.getTargetBlockHash()); + assertEquals(77L, session.getAccount(address).orElseThrow(AssertionError::new) + .getBalance()); + assertEquals(contract, session.getContract(address).orElseThrow(AssertionError::new)); + assertArrayEquals(historicalCode, + session.getCode(address).orElseThrow(AssertionError::new)); + assertArrayEquals(historicalStorage, + session.getStorage(address, slot).orElseThrow(AssertionError::new)); + + session.close(); + assertEquals(0, gate.getActiveLeaseCount()); + assertTrue(latest.closed); + assertThrows(IllegalStateException.class, session::getTargetBlock); + gate.close(); + } + } + + @Test + public void historicalQuerySessionFailsClosedWhenReadBudgetIsExceeded() throws Exception { + byte[] address = address(55); + try (Fixture fixture = new Fixture( + temporaryFolder.newFolder("historical-query-budget").toPath())) { + fixture.append(diff(1, new DbGroup("code", Collections.singletonList( + new Entry(address, OldValue.present(bytes("old-code"))))))); + fixture.sync(); + InMemoryLatest latest = InMemoryLatest.scoped(1, hash(1), Collections.emptyMap()); + ArchiveRuntimeQueryGate gate = new ArchiveRuntimeQueryGate( + target -> fixture.snapshot(target, latest)); + try (HistoricalQuerySession session = HistoricalQuerySession.open(gate.pin(0), hash(0), + new HistoricalQuerySession.Limits(1, 1024, 10_000))) { + assertArrayEquals(bytes("old-code"), + session.getCode(address).orElseThrow(AssertionError::new)); + assertThrows(HistoricalQueryBudgetException.class, () -> session.getCode(address)); + } + assertTrue(latest.closed); + gate.close(); + } + } + + @Test + public void historicalRepositoryUsesOneSourceAndKeepsWritesInOverlay() throws Exception { + byte[] address = address(52); + byte[] slot = hash(31); + byte[] historicalStorage = hash(32); + byte[] overlayStorage = hash(33); + byte[] historicalCode = bytes("historical-repository-code"); + SmartContract contract = SmartContract.newBuilder().setVersion(0).build(); + byte[] physicalKey = StorageRowKeyCodec.physicalKey(address, slot, 0, null); + + try (Fixture fixture = new Fixture( + temporaryFolder.newFolder("historical-repository").toPath())) { + fixture.append(diff(1, + new DbGroup("account", Collections.singletonList( + new Entry(address, OldValue.present(optimizedAccount(address, 81L))))), + new DbGroup("contract", Collections.singletonList( + new Entry(address, OldValue.present(contract.toByteArray())))), + new DbGroup("code", Collections.singletonList( + new Entry(address, OldValue.present(historicalCode)))), + new DbGroup("storage-row", Collections.singletonList( + new Entry(physicalKey, OldValue.present(historicalStorage)))), + new DbGroup("properties", historicalVmProperties()))); + fixture.sync(); + InMemoryLatest latest = InMemoryLatest.scoped(1, hash(1), Collections.emptyMap()); + ArchiveRuntimeQueryGate gate = new ArchiveRuntimeQueryGate( + target -> fixture.snapshot(target, latest)); + + try (HistoricalQuerySession session = HistoricalQuerySession.open(gate.pin(0), hash(0))) { + Repository root = RepositoryImpl.createHistoricalRoot(null, session); + assertTrue(root.isHistorical()); + assertEquals(81L, root.getBalance(address)); + assertArrayEquals(historicalCode, root.getCode(address)); + assertEquals(new DataWord(historicalStorage), + root.getStorageValue(address, new DataWord(slot))); + + Repository child = root.newRepositoryChild(); + child.putStorageValue(address, new DataWord(slot), new DataWord(overlayStorage)); + assertEquals(new DataWord(overlayStorage), + child.getStorageValue(address, new DataWord(slot))); + child.commit(); + assertEquals(new DataWord(overlayStorage), + root.getStorageValue(address, new DataWord(slot))); + ConfigLoader.load(root); + assertTrue(VMConfig.allowTvmConstantinople()); + assertTrue(VMConfig.getEnergyLimitHardFork()); + VMConfig.clearLocalSnapshot(); + assertThrows(HistoricalCapabilityException.class, + root::getDynamicPropertiesStore); + + Program program = mock(Program.class); + when(program.getContractState()).thenReturn(root); + assertThrows(HistoricalCapabilityException.class, + () -> OperationActions.suicideAction(program)); + verify(program, never()).stackPop(); + HistoricalExecutionGuard.requirePrecompileAllowed( + new PrecompiledContracts.Identity()); + assertThrows(HistoricalCapabilityException.class, + () -> HistoricalExecutionGuard.requirePrecompileAllowed( + new PrecompiledContracts.RewardBalance())); + assertThrows(IllegalStateException.class, root::commit); + } + assertTrue(latest.closed); + gate.close(); + } + } + + private static List historicalVmProperties() { + List properties = new ArrayList<>(); + String[] enabled = { + "ALLOW_MULTI_SIGN", "ALLOW_TVM_TRANSFER_TRC10", "ALLOW_TVM_CONSTANTINOPLE", + "ALLOW_TVM_SOLIDITY_059", "ALLOW_SHIELDED_TRC20_TRANSACTION", + "ALLOW_TVM_ISTANBUL", "ALLOW_TVM_FREEZE", "ALLOW_TVM_VOTE", "ALLOW_TVM_LONDON", + "ALLOW_TVM_COMPATIBLE_EVM", "ALLOW_HIGHER_LIMIT_FOR_MAX_CPU_TIME_OF_ONE_TX", + "ALLOW_OPTIMIZED_RETURN_VALUE_OF_CHAIN_ID", "ALLOW_DYNAMIC_ENERGY", + "ALLOW_TVM_SHANGHAI", "ALLOW_ENERGY_ADJUSTMENT", "ALLOW_STRICT_MATH", + "ALLOW_TVM_CANCUN", "CONSENSUS_LOGIC_OPTIMIZATION", "ALLOW_TVM_BLOB", + "ALLOW_TVM_SELFDESTRUCT_RESTRICTION", "ALLOW_TVM_OSAKA", + "ALLOW_HARDEN_RESOURCE_CALCULATION", "ALLOW_CREATION_OF_CONTRACTS" + }; + for (String key : enabled) { + properties.add(new Entry(bytes(key), OldValue.present(ByteArray.fromLong(1L)))); + } + properties.add(new Entry(bytes("latest_block_header_number"), + OldValue.present(ByteArray.fromLong(Long.MAX_VALUE)))); + String[] values = { + "UNFREEZE_DELAY_DAYS", "DYNAMIC_ENERGY_THRESHOLD", + "DYNAMIC_ENERGY_INCREASE_FACTOR", "DYNAMIC_ENERGY_MAX_FACTOR", "ENERGY_FEE", + "MAX_FEE_LIMIT", "MAX_CPU_TIME_OF_ONE_TX", "CURRENT_CYCLE_NUMBER" + }; + for (String key : values) { + properties.add(new Entry(bytes(key), OldValue.present(ByteArray.fromLong(100L)))); + } + return properties; + } + + @Test + public void historicalRepositoryAppliesEveryTypedDynamicPropertyDefault() throws Exception { + Map defaults = DynamicPropertiesStore.getLongPropertyDefaults(); + Set expectedKeys = new java.util.HashSet<>(Arrays.asList( + "WITNESS_127_PAY_PER_BLOCK", "CURRENT_CYCLE_NUMBER", "ALLOW_TVM_SHANGHAI", + "ALLOW_CANCEL_ALL_UNFREEZE_V2", "MAX_DELEGATE_LOCK_PERIOD", + "ALLOW_OLD_REWARD_OPT", "ALLOW_ENERGY_ADJUSTMENT", "MAX_CREATE_ACCOUNT_TX_SIZE", + "ALLOW_STRICT_MATH", "CONSENSUS_LOGIC_OPTIMIZATION", "ALLOW_TVM_CANCUN", + "ALLOW_TVM_BLOB", "ALLOW_TVM_SELFDESTRUCT_RESTRICTION", "PROPOSAL_EXPIRE_TIME", + "ALLOW_TVM_OSAKA", "ALLOW_TVM_PRAGUE", "BLOCK_HASH_HISTORY_INSTALLED", + "ALLOW_HARDEN_RESOURCE_CALCULATION", "ALLOW_HARDEN_EXCHANGE_CALCULATION", + "TURKISH_KEY_MIGRATION_DONE")); + assertEquals(expectedKeys, defaults.keySet()); + + List requiredProperties = historicalVmProperties().stream() + .filter(entry -> !defaults.containsKey( + new String(entry.getKey(), StandardCharsets.UTF_8))) + .collect(Collectors.toList()); + try (Fixture fixture = new Fixture( + temporaryFolder.newFolder("historical-dynamic-defaults").toPath())) { + fixture.append(diff(1, new DbGroup("properties", requiredProperties))); + fixture.sync(); + InMemoryLatest latest = InMemoryLatest.scoped(1, hash(1), Collections.emptyMap()); + ArchiveRuntimeQueryGate gate = new ArchiveRuntimeQueryGate( + target -> fixture.snapshot(target, latest)); + + try (HistoricalQuerySession session = HistoricalQuerySession.open(gate.pin(0), hash(0))) { + Repository root = RepositoryImpl.createHistoricalRoot(null, session); + for (Map.Entry defaultEntry : defaults.entrySet()) { + assertEquals(defaultEntry.getKey(), defaultEntry.getValue().longValue(), + root.getDynamicPropertyLong(defaultEntry.getKey())); + } + assertThrows(IllegalArgumentException.class, + () -> root.getDynamicPropertyLong("UNKNOWN_REQUIRED_PROPERTY")); + + ConfigLoader.load(root); + assertEquals(defaults.get("ALLOW_TVM_OSAKA").longValue(), + VMConfig.allowTvmOsaka() ? 1L : 0L); + assertEquals(defaults.get("ALLOW_HARDEN_RESOURCE_CALCULATION").longValue(), + VMConfig.allowHardenResourceCalculation() ? 1L : 0L); + VMConfig.clearLocalSnapshot(); + } + assertTrue(latest.closed); + gate.close(); + } + } + + @Test + public void historicalVmExecutesTriggerAgainstPinnedState() throws Exception { + byte[] owner = address(53); + byte[] contractAddress = address(54); + byte[] code = Hex.decode("60005460005260206000f3"); + byte[] storageValue = new DataWord(7).getData(); + byte[] physicalStorageKey = StorageRowKeyCodec.physicalKey( + contractAddress, new byte[32], 0, null); + SmartContract contract = SmartContract.newBuilder().setVersion(0).build(); + + try (Fixture fixture = new Fixture( + temporaryFolder.newFolder("historical-vm-trigger").toPath())) { + fixture.append(diff(1, + new DbGroup("account", Collections.singletonList( + new Entry(contractAddress, + OldValue.present(optimizedAccount(contractAddress, 0L))))), + new DbGroup("contract", Collections.singletonList( + new Entry(contractAddress, OldValue.present(contract.toByteArray())))), + new DbGroup("code", Collections.singletonList( + new Entry(contractAddress, OldValue.present(code)))), + new DbGroup("storage-row", Collections.singletonList( + new Entry(physicalStorageKey, OldValue.present(storageValue)))), + new DbGroup("properties", historicalVmProperties()))); + fixture.sync(); + InMemoryLatest latest = InMemoryLatest.scoped(1, hash(1), Collections.emptyMap()); + ArchiveRuntimeQueryGate gate = new ArchiveRuntimeQueryGate( + target -> fixture.snapshot(target, latest)); + + try (HistoricalQuerySession session = HistoricalQuerySession.open(gate.pin(0), hash(0))) { + TriggerSmartContract trigger = TriggerSmartContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(owner)) + .setContractAddress(ByteString.copyFrom(contractAddress)).build(); + TransactionCapsule transaction = new TransactionCapsule(trigger, + Protocol.Transaction.Contract.ContractType.TriggerSmartContract); + TransactionContext context = new TransactionContext( + new BlockCapsule(Protocol.Block.newBuilder().build()), transaction, null, true, false, + ExecutionMode.HISTORICAL_CONSTANT, session); + VMActuator actuator = new VMActuator(true, HistoricalRepositoryProvider.INSTANCE); + long previousConstantCallTimeoutMs = + CommonParameter.getInstance().getConstantCallTimeoutMs(); + CommonParameter.getInstance().setConstantCallTimeoutMs(5_000L); + try { + actuator.validate(context); + actuator.execute(context); + if (context.getProgramResult().getException() != null) { + throw context.getProgramResult().getException(); + } + assertEquals(new DataWord(7), new DataWord(context.getProgramResult().getHReturn())); + + TransactionCapsule createTransaction = new TransactionCapsule( + CreateSmartContract.getDefaultInstance(), + Protocol.Transaction.Contract.ContractType.CreateSmartContract); + TransactionContext createContext = new TransactionContext( + new BlockCapsule(Protocol.Block.newBuilder().build()), createTransaction, null, + true, false, ExecutionMode.HISTORICAL_CONSTANT, session); + VMActuator createActuator = new VMActuator( + true, HistoricalRepositoryProvider.INSTANCE); + ContractValidateException rejected = assertThrows(ContractValidateException.class, + () -> createActuator.validate(createContext)); + assertEquals("Historical execution only supports TriggerSmartContract", + rejected.getMessage()); + } finally { + CommonParameter.getInstance().setConstantCallTimeoutMs(previousConstantCallTimeoutMs); + VMConfig.clearLocalSnapshot(); + } + } + assertTrue(latest.closed); + gate.close(); + } + } + @Test public void logicalStorageFailsClosedForMissingCorruptOrClosedContractContext() throws Exception { diff --git a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java index e8d14ace060..f83d5c308da 100644 --- a/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/jsonrpc/JsonrpcServiceTest.java @@ -541,6 +541,11 @@ public void testGetTrxBalance() { () -> tronJsonRpc.getTrxBalance("", "safe")); Assert.assertEquals(TAG_NOT_SUPPORT_ERROR, e4.getMessage()); + Exception e5 = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getTrxBalance("", "0x1")); + Assert.assertEquals( + "QUANTITY not supported, just support TAG as latest", e5.getMessage()); + try { balance = tronJsonRpc.getTrxBalance("0xabd4b9367799eaa3197fecb144eb71de1e049abc", "latest"); @@ -811,6 +816,13 @@ public void testGetCallWithBlockObject() { Exception missingHashEx = Assert.assertThrows(Exception.class, () -> tronJsonRpc.getCall(null, missingHashParams)); Assert.assertEquals("header for hash not found", missingHashEx.getMessage()); + + HashMap historicalParams = new HashMap<>(); + historicalParams.put("blockNumber", ByteArray.toJsonHex(blockCapsule1.getNum())); + Exception historicalEx = Assert.assertThrows(Exception.class, + () -> tronJsonRpc.getCall(null, historicalParams)); + Assert.assertEquals( + "QUANTITY not supported, just support TAG as latest", historicalEx.getMessage()); } /** From 024025cb2a1e319d783f7dbeedc8767b27699f58 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 2 Sep 2026 14:01:33 +0800 Subject: [PATCH 108/161] feat(trie): add physical state root oracle --- .../PathStatePhysicalOracleWindow.java | 86 +++++ .../stateroot/PathStatePhysicalStoreSet.java | 41 ++ .../stateroot/PathStatePhysicalOracle.java | 359 ++++++++++++++++++ .../PathStatePhysicalOracleTool.java | 94 +++++ .../PathStateNativeNodeStoreTest.java | 120 ++++++ 5 files changed, 700 insertions(+) create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracleWindow.java create mode 100644 framework/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracle.java create mode 100644 framework/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracleTool.java diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracleWindow.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracleWindow.java new file mode 100644 index 00000000000..5a33735b4f5 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracleWindow.java @@ -0,0 +1,86 @@ +package org.tron.core.db2.stateroot; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Validated, read-only input for one fixed physical-store oracle window. + * + *

The window is ordered from the current child toward its oldest retained parent. It does not + * expose a historical root lookup API and does not mutate {@code F/N/M}, {@code INTENT}, + * {@code CURRENT}, or reverse journals. A caller must open an offline coherent checkpoint before + * loading the window; this class only proves that the supplied CURRENT and journals form one exact + * direct-parent chain. + */ +final class PathStatePhysicalOracleWindow { + + private final byte[] currentTarget; + private final byte[] oldestTarget; + private final List encodedJournals; + + PathStatePhysicalOracleWindow(byte[] currentTarget, + List journals) { + this.currentTarget = owned(currentTarget, "currentTarget"); + List supplied = new ArrayList<>( + Objects.requireNonNull(journals, "journals")); + if (supplied.isEmpty()) { + throw new IllegalArgumentException("physical oracle window must contain at least one block"); + } + PathStatePhysicalGlobalIntent cursor = PathStatePhysicalGlobalIntent.decode( + this.currentTarget); + List encoded = new ArrayList<>(supplied.size()); + for (PathStatePhysicalReverseJournal journal : supplied) { + PathStatePhysicalReverseJournal present = Objects.requireNonNull(journal, "journal"); + if (!Arrays.equals(cursor.encode(), present.getChildTarget())) { + throw new IllegalArgumentException( + "physical oracle window journal does not extend its current child"); + } + byte[] journalBytes = present.encode(); + encoded.add(Arrays.copyOf(journalBytes, journalBytes.length)); + cursor = PathStatePhysicalGlobalIntent.decode(present.getParentTarget()); + } + this.oldestTarget = cursor.encode(); + this.encodedJournals = Collections.unmodifiableList(encoded); + } + + int getBlockCount() { + return encodedJournals.size(); + } + + PathStateRootMetadata getCurrentMetadata() { + return PathStatePhysicalGlobalIntent.decode(currentTarget).getMetadata(); + } + + PathStateRootMetadata getOldestMetadata() { + return PathStatePhysicalGlobalIntent.decode(oldestTarget).getMetadata(); + } + + List journals() { + List copies = new ArrayList<>(encodedJournals.size()); + for (byte[] encoded : encodedJournals) { + copies.add(PathStatePhysicalReverseJournal.decode(encoded)); + } + return Collections.unmodifiableList(copies); + } + + List targets() { + List targets = new ArrayList<>(encodedJournals.size() + 1); + PathStatePhysicalGlobalIntent cursor = PathStatePhysicalGlobalIntent.decode(currentTarget); + targets.add(cursor); + for (byte[] encoded : encodedJournals) { + PathStatePhysicalReverseJournal journal = PathStatePhysicalReverseJournal.decode(encoded); + cursor = PathStatePhysicalGlobalIntent.decode(journal.getParentTarget()); + targets.add(cursor); + } + return Collections.unmodifiableList(targets); + } + + private static byte[] owned(byte[] value, String name) { + byte[] supplied = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + PathStatePhysicalGlobalIntent.decode(supplied); + return supplied; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java index 0899ff63855..0bab1ea922b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java @@ -177,6 +177,10 @@ public byte[] getFormatDigest() { return manifest.getIdentityDigest(); } + PathStateParticipantScope participantScope() { + return scope; + } + synchronized void saveIngestCheckpoint(String dbName, PathStatePhysicalIngestCheckpoint value) { participant(dbName).putMetadata(FLAT_INGEST_CHECKPOINT, Objects.requireNonNull(value, "value").encode()); @@ -526,6 +530,43 @@ synchronized void verifyReverseJournals(PathStateLayerLimits limits) throws IOEx reverseJournalIndex = loadReverseJournalIndex(Objects.requireNonNull(limits, "limits")); } + /** + * Loads one exact canonical reverse window without changing physical storage authority. + * + *

This diagnostic entry requires an offline coherent checkpoint. It deliberately reloads and + * validates every retained journal instead of trusting the steady-state in-memory index. Missing + * ancestry fails rather than returning a shorter window. + */ + synchronized PathStatePhysicalOracleWindow loadOracleWindow(int blockCount, + PathStateLayerLimits limits) throws IOException { + requireOpen(); + if (blockCount <= 0) { + throw new IllegalArgumentException("physical oracle block count must be positive"); + } + if (Files.exists(directory.resolve(INTENT_FILE), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("physical oracle requires a settled CURRENT without INTENT"); + } + PathStatePhysicalGlobalIntent current = currentTarget(); + Map indexed = loadReverseJournalIndex( + Objects.requireNonNull(limits, "limits")); + List journals = new ArrayList<>(blockCount); + PathStatePhysicalGlobalIntent cursor = current; + for (int index = 0; index < blockCount; index++) { + ReverseJournalIndexEntry entry = indexed.get(new BytesKey(cursor.encode())); + if (entry == null) { + throw new IOException("physical oracle fixed window ancestry is missing at height " + + cursor.getMetadata().getBlockNumber()); + } + PathStatePhysicalReverseJournal journal = loadReverseJournal(entry.path); + if (!Arrays.equals(cursor.encode(), journal.getChildTarget())) { + throw new IOException("physical oracle journal child differs from canonical cursor"); + } + journals.add(journal); + cursor = PathStatePhysicalGlobalIntent.decode(journal.getParentTarget()); + } + return new PathStatePhysicalOracleWindow(current.encode(), journals); + } + /** Computes one exact child target without changing F/N/M, INTENT, or CURRENT. */ public synchronized PathStateRootMetadata previewTransition(PathStateBlockTransition transition) throws IOException { diff --git a/framework/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracle.java b/framework/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracle.java new file mode 100644 index 00000000000..1700c9e375a --- /dev/null +++ b/framework/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracle.java @@ -0,0 +1,359 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.tron.core.capsule.BytesCapsule; +import org.tron.core.db2.common.DB; +import org.tron.core.trie.TrieImpl; + +/** Independent {@link TrieImpl} verifier for one offline physical-store oracle window. */ +final class PathStatePhysicalOracle { + + private static final int HASH_BATCH_ENTRIES = 4096; + + private PathStatePhysicalOracle() { + } + + static Result verify(PathStatePhysicalStoreSet stores, + PathStatePhysicalOracleWindow window, Path scratchDirectory, int rowsPerFlush) + throws IOException { + PathStatePhysicalStoreSet source = Objects.requireNonNull(stores, "stores"); + PathStatePhysicalOracleWindow input = Objects.requireNonNull(window, "window"); + if (rowsPerFlush <= 0) { + throw new IllegalArgumentException("physical oracle rows per flush must be positive"); + } + Path scratch = Objects.requireNonNull(scratchDirectory, "scratchDirectory") + .toAbsolutePath().normalize(); + if (Files.exists(scratch, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("physical oracle scratch directory must be fresh: " + scratch); + } + Files.createDirectories(scratch); + + List targets = input.targets(); + List journals = input.journals(); + Map> rootsByStore = new LinkedHashMap<>(); + long totalRows = 0; + boolean complete = false; + try { + for (PathStateParticipantDescriptor.StoreIdentity identity + : PathStateParticipantDescriptor.current().getStores()) { + int storeId = identity.getStoreId(); + Path participantScratch = scratch.resolve(String.format("%02d", storeId)); + List roots = new ArrayList<>(targets.size()); + boolean participantComplete = false; + try (NativeTrieDatabase database = new NativeTrieDatabase(participantScratch)) { + TrieImpl trie = new TrieImpl(database); + trie.setAsync(false); + long[] rows = new long[1]; + source.participant(identity.getDbName()).scanFlat(entry -> { + byte[] storedKey = entry.getKey(); + if (storedKey.length != PathStateCommitmentCodec.ROOT_LENGTH + 1 + || storedKey[0] != 'F') { + throw new IOException("physical oracle F key is corrupt: " + + identity.getDbName()); + } + trie.put(Arrays.copyOfRange(storedKey, 1, storedKey.length), entry.getValue()); + rows[0]++; + if (rows[0] % rowsPerFlush == 0) { + flush(trie, database); + } + }); + byte[] currentRoot = rootAndFlush(trie, database); + requireRoot(participantTarget(targets.get(0), storeId).getStoreRoot(), currentRoot, + identity.getDbName(), targets.get(0).getMetadata().getBlockNumber()); + roots.add(currentRoot); + + for (int index = 0; index < journals.size(); index++) { + PathStatePhysicalReverseJournal.StoreReverse reverse = reverseFor( + journals.get(index), storeId); + if (reverse != null) { + for (PathStatePhysicalReverseJournal.Entry entry : reverse.getFlatEntries()) { + byte[] oldValue = entry.getOldValue(); + if (oldValue == null) { + trie.delete(entry.getKey()); + } else { + trie.put(entry.getKey(), oldValue); + } + } + } + byte[] parentRoot = rootAndFlush(trie, database); + PathStatePhysicalGlobalIntent parent = targets.get(index + 1); + requireRoot(participantTarget(parent, storeId).getStoreRoot(), parentRoot, + identity.getDbName(), parent.getMetadata().getBlockNumber()); + roots.add(parentRoot); + } + totalRows = Math.addExact(totalRows, rows[0]); + participantComplete = true; + } catch (ArithmeticException overflow) { + throw new IOException("physical oracle row count overflow", overflow); + } finally { + if (participantComplete) { + deleteOwnedTree(scratch, participantScratch); + } + } + rootsByStore.put(storeId, immutableRoots(roots)); + } + + for (int targetIndex = 0; targetIndex < targets.size(); targetIndex++) { + TrieImpl superTrie = new TrieImpl(); + superTrie.setAsync(false); + for (PathStateParticipantDescriptor.StoreIdentity identity + : PathStateParticipantDescriptor.current().getStores()) { + PathStateParticipant participant = source.participantScope().require( + identity.getDbName()); + byte[] storeRoot = rootsByStore.get(identity.getStoreId()).get(targetIndex); + superTrie.put(PathStateCommitmentCodec.superLeafKey(identity.getStoreId()), + PathStateCommitmentCodec.superLeafValue(identity.getStoreId(), + identity.getDbName(), participant.getStoreFormatVersion(), storeRoot)); + } + PathStatePhysicalGlobalIntent target = targets.get(targetIndex); + requireRoot(target.getSuperRoot(), superTrie.getRootHash(), "super", + target.getMetadata().getBlockNumber()); + } + complete = true; + return new Result(input.getBlockCount(), totalRows, + input.getCurrentMetadata(), input.getOldestMetadata()); + } finally { + if (complete) { + deleteOwnedTree(scratch.getParent(), scratch); + } + } + } + + private static PathStatePhysicalReverseJournal.StoreReverse reverseFor( + PathStatePhysicalReverseJournal journal, int storeId) { + for (PathStatePhysicalReverseJournal.StoreReverse reverse : journal.getStores()) { + if (reverse.getStoreId() == storeId) { + return reverse; + } + } + return null; + } + + private static PathStatePhysicalGlobalIntent.ParticipantTarget participantTarget( + PathStatePhysicalGlobalIntent target, int storeId) throws IOException { + for (PathStatePhysicalGlobalIntent.ParticipantTarget participant + : target.getParticipants()) { + if (participant.getStoreId() == storeId) { + return participant; + } + } + throw new IOException("physical oracle target Store is absent: " + storeId); + } + + private static byte[] rootAndFlush(TrieImpl trie, NativeTrieDatabase database) { + flush(trie, database); + byte[] root = trie.getRootHash(); + return Arrays.copyOf(root, root.length); + } + + private static void flush(TrieImpl trie, NativeTrieDatabase database) { + trie.flush(); + database.flush(); + } + + private static void requireRoot(byte[] expected, byte[] actual, String store, + long blockNumber) throws IOException { + if (!Arrays.equals(expected, actual)) { + throw new IOException("physical oracle root differs: store=" + store + ", block=" + + blockNumber); + } + } + + private static List immutableRoots(List roots) { + List copies = new ArrayList<>(roots.size()); + for (byte[] root : roots) { + copies.add(Arrays.copyOf(root, root.length)); + } + return Collections.unmodifiableList(copies); + } + + private static void deleteOwnedTree(Path owner, Path target) throws IOException { + Path parent = Objects.requireNonNull(owner, "owner").toAbsolutePath().normalize(); + Path child = Objects.requireNonNull(target, "target").toAbsolutePath().normalize(); + if (child.equals(parent) || !child.startsWith(parent) || Files.isSymbolicLink(child)) { + throw new IOException("physical oracle refuses to delete unowned scratch: " + child); + } + if (!Files.exists(child, LinkOption.NOFOLLOW_LINKS)) { + return; + } + Files.walkFileTree(child, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path directory, IOException failure) + throws IOException { + if (failure != null) { + throw failure; + } + Files.delete(directory); + return FileVisitResult.CONTINUE; + } + }); + } + + static final class Result { + + private final int blockCount; + private final long rowCount; + private final PathStateRootMetadata current; + private final PathStateRootMetadata oldest; + + private Result(int blockCount, long rowCount, PathStateRootMetadata current, + PathStateRootMetadata oldest) { + this.blockCount = blockCount; + this.rowCount = rowCount; + this.current = current; + this.oldest = oldest; + } + + int getBlockCount() { + return blockCount; + } + + long getRowCount() { + return rowCount; + } + + PathStateRootMetadata getCurrent() { + return PathStateRootMetadata.decode(current.encode()); + } + + PathStateRootMetadata getOldest() { + return PathStateRootMetadata.decode(oldest.encode()); + } + } + + private static final class NativeTrieDatabase implements DB, + AutoCloseable { + + private final PathStateNativeNodeStore store; + private final Map pending = new LinkedHashMap<>(); + + private NativeTrieDatabase(Path directory) throws IOException { + store = PathStateNativeNodeStore.open(directory, PathStateStoreManifest.Engine.ROCKSDB); + } + + @Override + public BytesCapsule get(byte[] key) { + Key owned = new Key(key); + if (pending.containsKey(owned)) { + byte[] value = pending.get(owned); + return value == null ? null : new BytesCapsule(value); + } + byte[] value = store.get(key); + return value == null ? null : new BytesCapsule(value); + } + + @Override + public void put(byte[] key, BytesCapsule value) { + pending.put(new Key(key), Arrays.copyOf(Objects.requireNonNull(value, "value").getData(), + value.getData().length)); + flushIfFull(); + } + + @Override + public void remove(byte[] key) { + pending.put(new Key(key), null); + flushIfFull(); + } + + private void flushIfFull() { + if (pending.size() >= HASH_BATCH_ENTRIES) { + flush(); + } + } + + private void flush() { + if (pending.isEmpty()) { + return; + } + List mutations = new ArrayList<>(pending.size()); + for (Map.Entry entry : pending.entrySet()) { + mutations.add(entry.getValue() == null + ? PathStateNativeNodeStore.BatchMutation.delete(entry.getKey().value) + : PathStateNativeNodeStore.BatchMutation.put(entry.getKey().value, + entry.getValue())); + } + store.writeBatchUnsynced(mutations); + pending.clear(); + } + + @Override + public long size() { + return -1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public Iterator> iterator() { + return Collections.>emptyList().iterator(); + } + + @Override + public void close() { + flush(); + try { + store.close(); + } catch (IOException failure) { + throw new IllegalStateException("failed to close physical oracle scratch", failure); + } + } + + @Override + public String getDbName() { + return "path-state-physical-oracle"; + } + + @Override + public void stat() { + } + + @Override + public DB newInstance() { + return null; + } + } + + private static final class Key { + + private final byte[] value; + + private Key(byte[] value) { + this.value = Arrays.copyOf(Objects.requireNonNull(value, "value"), value.length); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof Key + && Arrays.equals(value, ((Key) other).value); + } + + @Override + public int hashCode() { + return Arrays.hashCode(value); + } + } +} diff --git a/framework/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracleTool.java b/framework/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracleTool.java new file mode 100644 index 00000000000..891e06dd8b7 --- /dev/null +++ b/framework/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOracleTool.java @@ -0,0 +1,94 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Command-line entry for verifying one offline physical-store reverse window. */ +public final class PathStatePhysicalOracleTool { + + private static final int DEFAULT_ROWS_PER_FLUSH = 100_000; + + private PathStatePhysicalOracleTool() { + } + + public static void main(String[] args) throws Exception { + PathStatePhysicalOracle.Result result = run(args); + System.out.println("PATH_STATE_ORACLE_OK" + + " current=" + result.getCurrent().getBlockNumber() + + " oldest=" + result.getOldest().getBlockNumber() + + " blocks=" + result.getBlockCount() + + " rows=" + result.getRowCount()); + } + + static PathStatePhysicalOracle.Result run(String[] args) throws IOException { + Map options = parse(args); + Path root = Paths.get(require(options, "--root")); + Path scratch = Paths.get(require(options, "--scratch")); + int blocks = positiveInt(require(options, "--blocks"), "--blocks"); + int rowsPerFlush = options.containsKey("--rows-per-flush") + ? positiveInt(options.get("--rows-per-flush"), "--rows-per-flush") + : DEFAULT_ROWS_PER_FLUSH; + Engine engine; + try { + engine = Engine.valueOf(options.getOrDefault("--engine", Engine.ROCKSDB.name())); + } catch (IllegalArgumentException invalid) { + throw new IllegalArgumentException("unsupported physical oracle engine", invalid); + } + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.openExisting(root, scope, + engine)) { + PathStatePhysicalOracleWindow window = stores.loadOracleWindow(blocks, + PathStateLayerLimits.defaults()); + return PathStatePhysicalOracle.verify(stores, window, scratch, rowsPerFlush); + } + } + + private static Map parse(String[] args) { + if (args == null || args.length == 0 || args.length % 2 != 0) { + throw usage(); + } + Map options = new LinkedHashMap<>(); + for (int index = 0; index < args.length; index += 2) { + String name = args[index]; + if (!"--root".equals(name) && !"--scratch".equals(name) + && !"--blocks".equals(name) && !"--rows-per-flush".equals(name) + && !"--engine".equals(name)) { + throw usage(); + } + if (options.put(name, args[index + 1]) != null) { + throw new IllegalArgumentException("duplicate physical oracle option: " + name); + } + } + return options; + } + + private static String require(Map options, String name) { + String value = options.get(name); + if (value == null || value.isEmpty()) { + throw usage(); + } + return value; + } + + private static int positiveInt(String value, String name) { + try { + int parsed = Integer.parseInt(value); + if (parsed <= 0) { + throw new NumberFormatException("not positive"); + } + return parsed; + } catch (NumberFormatException invalid) { + throw new IllegalArgumentException(name + " must be a positive integer", invalid); + } + } + + private static IllegalArgumentException usage() { + return new IllegalArgumentException("usage: PathStatePhysicalOracleTool" + + " --root --scratch --blocks " + + " [--rows-per-flush ] [--engine ROCKSDB|LEVELDB]"); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index b3820e0026c..3b296919834 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -865,6 +865,126 @@ public void physicalSteadyTransitionDoesNotRereadIndexedReverseJournal() throws () -> PathStatePhysicalSnapshotHead.open(root, Engine.ROCKSDB)); } + @Test + public void physicalOracleWindowLoadsOneExactReadOnlyAncestorChain() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-oracle-window").toPath(); + preparePublishedPhysicalTarget(root, scope); + PathStateLayerLimits limits = new PathStateLayerLimits(4, 1L << 20); + byte[] key = new byte[]{1, 2}; + + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + stores.applyAndPublish(new PathStateBlockTransition(1, bytes(94), new byte[32], 3, + P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", key, new byte[]{3}))), limits); + stores.applyAndPublish(new PathStateBlockTransition(2, bytes(95), bytes(94), 6, + P66Phase.P66_ON, Arrays.asList( + PathStateMutation.put("code", key, new byte[]{4}), + PathStateMutation.put("proposal", new byte[]{5}, new byte[]{6}))), limits); + stores.applyAndPublish(new PathStateBlockTransition(3, bytes(96), bytes(95), 9, + P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.delete("code", key))), limits); + + byte[] currentBefore = Files.readAllBytes(root.resolve( + PathStatePhysicalStoreSet.CURRENT_FILE)); + PathStatePhysicalOracleWindow window = stores.loadOracleWindow(3, limits); + assertEquals(3, window.getBlockCount()); + assertEquals(3, window.getCurrentMetadata().getBlockNumber()); + assertEquals(0, window.getOldestMetadata().getBlockNumber()); + assertArrayEquals(currentBefore, Files.readAllBytes(root.resolve( + PathStatePhysicalStoreSet.CURRENT_FILE))); + assertNull(stores.participant("code").getFlat( + PathStateCommitmentCodec.storeLeafKey(scope.require("code").getStoreId(), key))); + + List journals = window.journals(); + assertEquals(3, journals.size()); + for (int index = 0; index < journals.size(); index++) { + PathStatePhysicalGlobalIntent child = PathStatePhysicalGlobalIntent.decode( + journals.get(index).getChildTarget()); + PathStatePhysicalGlobalIntent parent = PathStatePhysicalGlobalIntent.decode( + journals.get(index).getParentTarget()); + assertEquals(3 - index, child.getMetadata().getBlockNumber()); + assertEquals(2 - index, parent.getMetadata().getBlockNumber()); + } + + } + + Path scratch = new File(temporaryFolder.getRoot(), "physical-oracle-scratch").toPath(); + PathStatePhysicalOracle.Result result = PathStatePhysicalOracleTool.run(new String[]{ + "--root", root.toString(), "--scratch", scratch.toString(), "--blocks", "3", + "--rows-per-flush", "2", "--engine", "ROCKSDB"}); + assertEquals(3, result.getBlockCount()); + assertEquals(3, result.getRowCount()); + assertEquals(3, result.getCurrent().getBlockNumber()); + assertEquals(0, result.getOldest().getBlockNumber()); + assertFalse(Files.exists(scratch)); + } + + @Test + public void physicalOracleToolRejectsUnknownDuplicateAndInvalidOptions() { + assertThrows(IllegalArgumentException.class, + () -> PathStatePhysicalOracleTool.run(new String[]{"--unknown", "value"})); + assertThrows(IllegalArgumentException.class, + () -> PathStatePhysicalOracleTool.run(new String[]{ + "--root", "one", "--root", "two", "--scratch", "scratch", "--blocks", "1"})); + assertThrows(IllegalArgumentException.class, + () -> PathStatePhysicalOracleTool.run(new String[]{ + "--root", "root", "--scratch", "scratch", "--blocks", "0"})); + } + + @Test + public void physicalOracleDetectsFlatValueDriftAndPreservesFailureScratch() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-oracle-flat-drift").toPath(); + preparePublishedPhysicalTarget(root, scope); + PathStateLayerLimits limits = new PathStateLayerLimits(2, 1L << 20); + + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + stores.applyAndPublish(new PathStateBlockTransition(1, bytes(99), new byte[32], 3, + P66Phase.P66_ON, Collections.emptyList()), limits); + PathStatePhysicalOracleWindow window = stores.loadOracleWindow(1, limits); + byte[] accountKey = PathStateCommitmentCodec.storeLeafKey( + scope.require("account").getStoreId(), new byte[]{1, 2}); + stores.participant("account").putFlat(accountKey, + PathStateCommitmentCodec.presentLeafValue(new byte[]{9, 9})); + + Path scratch = new File(temporaryFolder.getRoot(), + "physical-oracle-failure-scratch").toPath(); + java.io.IOException failure = assertThrows(java.io.IOException.class, + () -> PathStatePhysicalOracle.verify(stores, window, scratch, 2)); + assertTrue(failure.getMessage().contains("physical oracle root differs")); + assertTrue(Files.isDirectory(scratch)); + } + } + + @Test + public void physicalOracleWindowRejectsPartialUnsettledOrOverLimitInput() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-oracle-window-invalid").toPath(); + preparePublishedPhysicalTarget(root, scope); + PathStateLayerLimits limits = new PathStateLayerLimits(4, 1L << 20); + + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + stores.applyAndPublish(new PathStateBlockTransition(1, bytes(97), new byte[32], 3, + P66Phase.P66_ON, Collections.emptyList()), limits); + stores.applyAndPublish(new PathStateBlockTransition(2, bytes(98), bytes(97), 6, + P66Phase.P66_ON, Collections.emptyList()), limits); + + assertThrows(java.io.IOException.class, () -> stores.loadOracleWindow(3, limits)); + assertThrows(java.io.IOException.class, + () -> stores.loadOracleWindow(2, new PathStateLayerLimits(1, 1L << 20))); + assertThrows(IllegalArgumentException.class, + () -> stores.loadOracleWindow(0, limits)); + + Files.write(root.resolve(PathStatePhysicalStoreSet.INTENT_FILE), + Files.readAllBytes(root.resolve(PathStatePhysicalStoreSet.CURRENT_FILE))); + assertThrows(java.io.IOException.class, () -> stores.loadOracleWindow(1, limits)); + } + } + @Test public void parallelParticipantWritesStartTogetherAndWaitForEveryCompletion() throws Exception { ExecutorService executor = Executors.newFixedThreadPool(2); From 42ce4513916bd6066227f3da52bae614bb3f803e Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 4 Sep 2026 15:00:07 +0800 Subject: [PATCH 109/161] feat(chainbase): add checkpointed state archive Capture block-scoped archive and path-state changes in snapshots, then materialize Chainbase, trie, and archive stores through a shared redo checkpoint. Add overlay-based path-state preparation, serving-index recovery, configuration, and integration coverage. --- .../java/org/tron/core/db/TronDatabase.java | 7 + .../archive/AccountAssetArchiveProjector.java | 33 +- .../core/db2/archive/BlockChangeView.java | 33 + .../core/db2/archive/BlockReverseDiff.java | 24 + .../db2/archive/P66AccountAssetCodec.java | 9 +- .../archive/SnapshotOldValueCollector.java | 18 +- .../SnapshotPathStateTransitionCollector.java | 76 +- .../StateArchiveCheckpointMaterializer.java | 490 +++++++++++++ .../StateArchiveCheckpointReadAdapter.java | 85 +++ .../StateArchiveCheckpointReadSnapshot.java | 181 +++++ .../StateArchiveCheckpointServingIndex.java | 449 ++++++++++++ .../db2/archive/StateArchiveRuntimeOwner.java | 16 +- .../org/tron/core/db2/common/Flusher.java | 5 + .../org/tron/core/db2/common/LevelDB.java | 14 +- .../org/tron/core/db2/common/RocksDB.java | 14 +- .../core/ChainbaseCheckpointMaterializer.java | 358 ++++++++++ .../core/db2/core/CommonCheckpointFile.java | 162 +++++ .../core/CommonCheckpointMaterializer.java | 34 + .../db2/core/CommonCheckpointPayload.java | 337 +++++++++ .../core/CommonCheckpointPayloadCodec.java | 286 ++++++++ .../core/CommonCheckpointPayloadFactory.java | 151 ++++ .../core/CommonCheckpointRedoCoordinator.java | 166 +++++ .../db2/core/CommonCheckpointRuntime.java | 76 ++ .../CommonCheckpointRuntimeAttachment.java | 101 +++ .../core/CommonCheckpointRuntimeOwner.java | 145 ++++ .../core/CommonCheckpointSnapshotRebaser.java | 110 +++ .../core/db2/core/CommonCheckpointTarget.java | 114 +++ .../org/tron/core/db2/core/SnapshotImpl.java | 30 +- .../tron/core/db2/core/SnapshotManager.java | 26 +- .../org/tron/core/db2/core/SnapshotRoot.java | 26 +- .../core/db2/stateroot/PathMerkleTrie.java | 500 +++++++++++-- .../stateroot/PathStateAsyncPrepareHead.java | 200 ++++++ .../stateroot/PathStateBaseCompaction.java | 6 +- .../stateroot/PathStateBlockTransition.java | 18 +- .../PathStateCheckpointMaterializer.java | 273 ++++++++ .../db2/stateroot/PathStateCurrentStore.java | 3 +- .../db2/stateroot/PathStateFlushTarget.java | 288 ++++++++ .../core/db2/stateroot/PathStateHead.java | 7 + .../stateroot/PathStateLayerPublication.java | 3 +- .../core/db2/stateroot/PathStateMutation.java | 29 + .../db2/stateroot/PathStateNodeStoreSet.java | 9 +- .../PathStatePhysicalOverlayHead.java | 565 +++++++++++++++ .../PathStatePhysicalSnapshotHead.java | 24 +- .../stateroot/PathStatePhysicalStoreSet.java | 283 +++++++- .../core/db2/stateroot/PathStateRoot.java | 214 +++++- .../stateroot/PathStateRuntimeAttachment.java | 169 ++++- .../db2/stateroot/PathStateSnapshotDelta.java | 267 +++++++ .../db2/stateroot/PathStateSnapshotHead.java | 7 + .../PreparedPathStateTransition.java | 81 +++ .../org/tron/core/config/args/Storage.java | 22 + .../tron/core/config/args/StorageConfig.java | 17 +- common/src/main/resources/reference.conf | 6 + .../core/config/args/StorageConfigTest.java | 21 +- .../java/org/tron/core/config/args/Args.java | 10 + .../main/java/org/tron/core/db/Manager.java | 84 ++- framework/src/main/resources/config.conf | 6 + .../org/tron/core/config/args/ArgsTest.java | 10 + .../org/tron/core/db2/SnapshotImplTest.java | 73 ++ .../SnapshotOldValueCollectorTest.java | 107 ++- ...tateArchiveCheckpointMaterializerTest.java | 261 +++++++ ...tateArchiveCheckpointReadSnapshotTest.java | 189 +++++ ...eArchiveManagerStartupIntegrationTest.java | 26 +- .../ChainbaseCheckpointMaterializerTest.java | 662 ++++++++++++++++++ .../db2/core/CommonCheckpointFileTest.java | 137 ++++ .../CommonCheckpointRedoCoordinatorTest.java | 279 ++++++++ ...CommonCheckpointRuntimeAttachmentTest.java | 112 +++ .../db2/stateroot/PathMerkleTrieTest.java | 167 +++++ .../PathStateBlockTransitionTest.java | 17 + .../PathStateCheckpointMaterializerTest.java | 220 ++++++ .../db2/stateroot/PathStateLayerTest.java | 32 + .../PathStateNativeNodeStoreTest.java | 138 +++- .../core/db2/stateroot/PathStateRootTest.java | 18 +- .../stateroot/PathStateSnapshotHeadTest.java | 151 ++++ 73 files changed, 9104 insertions(+), 183 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFile.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachment.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointSnapshotRebaser.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointTarget.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateAsyncPrepareHead.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateFlushTarget.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshotTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/core/CommonCheckpointFileTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializerTest.java diff --git a/chainbase/src/main/java/org/tron/core/db/TronDatabase.java b/chainbase/src/main/java/org/tron/core/db/TronDatabase.java index 0a78570b8ed..705b4149523 100644 --- a/chainbase/src/main/java/org/tron/core/db/TronDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db/TronDatabase.java @@ -64,6 +64,13 @@ public void updateByBatch(Map rows) { this.dbSource.updateByBatch(rows, writeOptions); } + /** Writes one checkpoint batch with sync enabled regardless of the ordinary DB setting. */ + public void updateByBatchSynced(Map rows) { + try (WriteOptionsWrapper synced = WriteOptionsWrapper.getInstance().sync(true)) { + this.dbSource.updateByBatch(rows, synced); + } + } + /** * reset the database. */ diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java index 19ff7a77088..ee50d2d0717 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/AccountAssetArchiveProjector.java @@ -30,6 +30,28 @@ Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue r Map oldPhysicalAssetsForAddress) { Account oldAccount = parse(rawOld); Account postAccount = rawPost.isPresent() ? parse(rawPost.getValue()) : null; + return projectParsed(accountKey, rawOld, rawPost, targetAssetOptimizationEnabled, + oldPhysicalAssetsForAddress, oldAccount, postAccount); + } + + Projection projectWithOldPhysicalAssetsSource(byte[] accountKey, byte[] rawOld, + BlockChangeView.PostValue rawPost, boolean targetAssetOptimizationEnabled, + AccountAssetOldPhysicalAssetsSource oldPhysicalAssetsSource) { + Account oldAccount = parse(rawOld); + Account postAccount = rawPost.isPresent() ? parse(rawPost.getValue()) : null; + Map oldPhysicalAssets = Collections.emptyMap(); + if (requiresOldPhysicalAssets(oldAccount, postAccount)) { + oldPhysicalAssets = AccountAssetOldPhysicalAssetsSource.captureRequired( + oldPhysicalAssetsSource, accountKey); + } + return projectParsed(accountKey, rawOld, rawPost, targetAssetOptimizationEnabled, + oldPhysicalAssets, oldAccount, postAccount); + } + + private Projection projectParsed(byte[] accountKey, byte[] rawOld, + BlockChangeView.PostValue rawPost, boolean targetAssetOptimizationEnabled, + Map oldPhysicalAssetsForAddress, Account oldAccount, + Account postAccount) { boolean projectPost = postAccount != null && (postAccount.getAssetOptimized() || targetAssetOptimizationEnabled); @@ -71,11 +93,12 @@ Projection project(byte[] accountKey, byte[] rawOld, BlockChangeView.PostValue r } OldValue canonicalOld = oldAccount == null ? OldValue.absent() - : OldValue.present(canonicalAccount(accountKey, oldAccount, + : OldValue.present(canonicalAccount(accountKey, oldAccount, rawOld, oldAccount.getAssetOptimized())); BlockChangeView.PostValue canonicalPost = postAccount == null ? BlockChangeView.PostValue.absent() - : BlockChangeView.PostValue.present(canonicalAccount(accountKey, postAccount, projectPost)); + : BlockChangeView.PostValue.present(canonicalAccount(accountKey, postAccount, + rawPost.getValue(), projectPost)); if (canonicalPost.isPresent()) { codec.requireCanonicalLayout(phase(postAccount, projectPost), accountKey, canonicalPost.getValue(), changedAssetRows); @@ -135,9 +158,9 @@ private Map physicalAssets(byte[] accountKey, Account return result; } - private byte[] canonicalAccount(byte[] accountKey, Account account, boolean projected) { - return codec.canonicalizeAccount(phase(account, projected), accountKey, - account.toByteArray()); + private byte[] canonicalAccount(byte[] accountKey, Account account, byte[] rawAccount, + boolean projected) { + return codec.canonicalizeAccount(phase(account, projected), accountKey, account, rawAccount); } private Phase phase(Account account, boolean projected) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java b/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java index 0e7e11ebfbb..e900c117e1b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java @@ -1,5 +1,8 @@ package org.tron.core.db2.archive; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -19,12 +22,18 @@ */ public final class BlockChangeView { + private static final byte[] DIGEST_DOMAIN = + "java-tron/block-change-view".getBytes(StandardCharsets.US_ASCII); + private static final int DIGEST_VERSION = 1; + private final BlockSnapshotMeta meta; private final List databases; + private final byte[] mutationViewDigest; private BlockChangeView(BlockSnapshotMeta meta, List databases) { this.meta = Objects.requireNonNull(meta, "meta"); this.databases = Collections.unmodifiableList(new ArrayList<>(databases)); + this.mutationViewDigest = digest(meta, this.databases); } public static BlockChangeView capture(BlockSnapshotMeta meta, List databases) { @@ -58,6 +67,30 @@ public List getDatabases() { return databases; } + /** Canonical identity of the exact block-final database/key/post-value view. */ + public byte[] getMutationViewDigest() { + return Arrays.copyOf(mutationViewDigest, mutationViewDigest.length); + } + + private static byte[] digest(BlockSnapshotMeta meta, List databases) { + Hasher digest = Hashing.sha256().newHasher(); + digest.putInt(DIGEST_DOMAIN.length).putBytes(DIGEST_DOMAIN).putInt(DIGEST_VERSION) + .putLong(meta.getEpoch()).putLong(meta.getBlockNumber()).putBytes(meta.getBlockHash()) + .putBytes(meta.getParentHash()).putLong(meta.getTimestamp()).putInt(databases.size()); + for (DatabaseChanges database : databases) { + byte[] dbName = database.dbName.getBytes(StandardCharsets.UTF_8); + digest.putInt(dbName.length).putBytes(dbName).putInt(database.changes.size()); + for (Change change : database.changes) { + digest.putInt(change.key.length).putBytes(change.key) + .putBoolean(change.postValue.present); + if (change.postValue.present) { + digest.putInt(change.postValue.value.length).putBytes(change.postValue.value); + } + } + } + return digest.hash().asBytes(); + } + public static final class DatabaseChanges { private final String dbName; private final Snapshot previous; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java b/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java index 30f94e77d67..538441a4db0 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java @@ -13,12 +13,19 @@ public final class BlockReverseDiff { private final BlockSnapshotMeta meta; private final List groups; + private final byte[] mutationViewDigest; public BlockReverseDiff(BlockSnapshotMeta meta, List groups) { + this(meta, groups, null); + } + + public BlockReverseDiff(BlockSnapshotMeta meta, List groups, + byte[] mutationViewDigest) { this.meta = Objects.requireNonNull(meta, "meta"); List sorted = new ArrayList<>(groups); sorted.sort(Comparator.comparing(DbGroup::getDbName)); this.groups = Collections.unmodifiableList(sorted); + this.mutationViewDigest = optionalDigest(mutationViewDigest); } public BlockSnapshotMeta getMeta() { @@ -29,6 +36,23 @@ public List getGroups() { return groups; } + /** Returns the block-final mutation-view identity, or null for decoded legacy payloads. */ + public byte[] getMutationViewDigest() { + return mutationViewDigest == null ? null + : Arrays.copyOf(mutationViewDigest, mutationViewDigest.length); + } + + private static byte[] optionalDigest(byte[] supplied) { + if (supplied == null) { + return null; + } + byte[] copy = Arrays.copyOf(supplied, supplied.length); + if (copy.length != 32) { + throw new IllegalArgumentException("mutationViewDigest must contain exactly 32 bytes"); + } + return copy; + } + public static final class DbGroup { private final String dbName; private final List entries; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java index c1da86adc6e..5507b0852ce 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/P66AccountAssetCodec.java @@ -31,10 +31,17 @@ private boolean directAssetsEnabled() { /** Validates and converts one raw execution Account into its target archive representation. */ public byte[] canonicalizeAccount(Phase phase, byte[] physicalAccountKey, byte[] rawAccountValue) { + Objects.requireNonNull(rawAccountValue, "rawAccountValue"); + return canonicalizeAccount(phase, physicalAccountKey, parseAccount(rawAccountValue), + rawAccountValue); + } + + byte[] canonicalizeAccount(Phase phase, byte[] physicalAccountKey, Account account, + byte[] rawAccountValue) { Objects.requireNonNull(phase, "phase"); byte[] accountKey = requireAddress(physicalAccountKey); Objects.requireNonNull(rawAccountValue, "rawAccountValue"); - Account account = parseAccount(rawAccountValue); + Objects.requireNonNull(account, "account"); requireAccountAddress(accountKey, account); if (!phase.directAssetsEnabled()) { if (account.getAssetOptimized()) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java index b7292e5c697..7843cbb5b3f 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java @@ -3,12 +3,9 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.function.BooleanSupplier; -import org.tron.core.db2.common.WrappedByteArray; /** Scheme 2 collector: read old values from the completed block layer's previous view. */ public final class SnapshotOldValueCollector implements OldValueCollector { @@ -54,15 +51,10 @@ public BlockReverseDiff collect(BlockChangeView view) { BlockChangeView.PostValue postValue = change.getPostValue(); if (accountAssetProjector != null && AccountAssetArchiveProjector.ACCOUNT_DB.equals(database.getDbName())) { - Map oldPhysicalAssets = Collections.emptyMap(); - if (accountAssetProjector.requiresOldPhysicalAssets( - oldValue.isPresent() ? oldValue.getValue() : null, postValue)) { - oldPhysicalAssets = AccountAssetOldPhysicalAssetsSource.captureRequired( - oldPhysicalAssetsSource, key); - } - AccountAssetArchiveProjector.Projection projection = accountAssetProjector.project( - key, oldValue.isPresent() ? oldValue.getValue() : null, postValue, - targetAssetOptimizationEnabled, oldPhysicalAssets); + AccountAssetArchiveProjector.Projection projection = + accountAssetProjector.projectWithOldPhysicalAssetsSource( + key, oldValue.isPresent() ? oldValue.getValue() : null, postValue, + targetAssetOptimizationEnabled, oldPhysicalAssetsSource); oldValue = projection.oldAccount; postValue = projection.postAccount; accountAssetEntries.addAll(projection.reverseAssets); @@ -79,7 +71,7 @@ public BlockReverseDiff collect(BlockChangeView view) { groups.add(new BlockReverseDiff.DbGroup( AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, accountAssetEntries)); } - return new BlockReverseDiff(view.getMeta(), groups); + return new BlockReverseDiff(view.getMeta(), groups, view.getMutationViewDigest()); } /** Resolves proposal 66 from the same immutable target block view being projected. */ diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java index 532d4b18657..15dbb42d20c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -69,26 +68,26 @@ public PathStateBlockTransition collect(BlockChangeView view) throws IOException } BlockSnapshotMeta meta = admitted.getMeta(); return new PathStateBlockTransition(meta.getBlockNumber(), meta.getBlockHash(), - meta.getParentHash(), meta.getTimestamp(), phase, mutations.values()); + meta.getParentHash(), meta.getTimestamp(), phase, mutations.values(), + admitted.getMutationViewDigest()); } private void collectActivationAccounts( Map mutations) throws IOException { activationAccountSource.scan((key, rawPost) -> { BlockChangeView.PostValue postValue = BlockChangeView.PostValue.present(rawPost); - Map oldAssets = Collections.emptyMap(); - if (accountAssetProjector.requiresOldPhysicalAssets(null, postValue)) { - oldAssets = AccountAssetOldPhysicalAssetsSource.captureRequired( - oldPhysicalAssetsSource, key); - } - AccountAssetArchiveProjector.Projection projection = accountAssetProjector.project( - key, null, postValue, true, oldAssets); + AccountAssetArchiveProjector.Projection projection = + accountAssetProjector.projectWithOldPhysicalAssetsSource( + key, null, postValue, true, oldPhysicalAssetsSource); addCanonical(AccountAssetArchiveProjector.ACCOUNT_DB, key, projection.oldAccount, projection.postAccount, P66Phase.P66_ACTIVATION, mutations); + Map reverseOldAssets = oldAssets(projection); for (AssetRow asset : projection.changedAssetRows) { + OldValue oldValue = requireOldAsset(reverseOldAssets, asset.getPhysicalRawKey()); addPhysical(P66Phase.P66_ACTIVATION, AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, - asset.getPhysicalRawKey(), null, asset.getPostValue(), mutations); + asset.getPhysicalRawKey(), nullable(oldValue), asset.getPostValue(), false, + mutations); } }); } @@ -97,23 +96,28 @@ private void collectAccount(P66Phase phase, BlockChangeView.DatabaseChanges data BlockChangeView.Change change, Map mutations) { byte[] key = change.getKey(); byte[] rawOld = database.getPrevious(key); - Map oldAssets = Collections.emptyMap(); - if (accountAssetProjector.requiresOldPhysicalAssets(rawOld, change.getPostValue())) { - oldAssets = AccountAssetOldPhysicalAssetsSource.captureRequired( - oldPhysicalAssetsSource, key); - } - AccountAssetArchiveProjector.Projection projection = accountAssetProjector.project( - key, rawOld, change.getPostValue(), phase != P66Phase.P66_OFF, oldAssets); + AccountAssetArchiveProjector.Projection projection = + accountAssetProjector.projectWithOldPhysicalAssetsSource( + key, rawOld, change.getPostValue(), phase != P66Phase.P66_OFF, + oldPhysicalAssetsSource); addCanonical(AccountAssetArchiveProjector.ACCOUNT_DB, key, projection.oldAccount, projection.postAccount, phase, mutations); + Map reverseOldAssets = oldAssets(projection); for (AssetRow asset : projection.changedAssetRows) { + OldValue oldValue = requireOldAsset(reverseOldAssets, asset.getPhysicalRawKey()); addPhysical(phase, AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, - asset.getPhysicalRawKey(), null, asset.getPostValue(), mutations); + asset.getPhysicalRawKey(), nullable(oldValue), asset.getPostValue(), false, mutations); } } private void addPhysical(P66Phase phase, String dbName, byte[] key, byte[] rawOld, BlockChangeView.PostValue rawPost, Map mutations) { + addPhysical(phase, dbName, key, rawOld, rawPost, true, mutations); + } + + private void addPhysical(P66Phase phase, String dbName, byte[] key, byte[] rawOld, + BlockChangeView.PostValue rawPost, boolean physicalPreviousAuthoritative, + Map mutations) { PathStateMutation oldMutation = rawOld == null ? null : canonicalizer.put(phase, dbName, key, rawOld); PathStateMutation postMutation = rawPost.isPresent() @@ -122,7 +126,9 @@ private void addPhysical(P66Phase phase, String dbName, byte[] key, byte[] rawOl if (oldMutation != null && same(oldMutation, postMutation)) { return; } - add(postMutation, mutations); + add(physicalPreviousAuthoritative + ? postMutation.withPreviousPhysicalValue( + oldMutation == null ? null : oldMutation.getPhysicalValue()) : postMutation, mutations); } private void addCanonical(String dbName, byte[] key, OldValue oldValue, @@ -136,14 +142,42 @@ private void addCanonical(String dbName, byte[] key, OldValue oldValue, if (oldMutation != null && same(oldMutation, postMutation)) { return; } - add(postMutation, mutations); + add(postMutation.withPreviousPhysicalValue( + oldMutation == null ? null : oldMutation.getPhysicalValue()), mutations); + } + + private static Map oldAssets( + AccountAssetArchiveProjector.Projection projection) { + Map result = new LinkedHashMap<>(); + for (org.tron.core.db2.archive.BlockReverseDiff.Entry entry : projection.reverseAssets) { + result.put(WrappedByteArray.copyOf(entry.getKey()), entry.getOldValue()); + } + return result; + } + + private static OldValue requireOldAsset(Map oldAssets, + byte[] physicalKey) { + OldValue oldValue = oldAssets.get(WrappedByteArray.copyOf(physicalKey)); + if (oldValue == null) { + throw new ArchivePersistenceException( + "Changed AccountAsset row has no matching pre-state value"); + } + return oldValue; + } + + private static byte[] nullable(OldValue oldValue) { + return oldValue.isPresent() ? oldValue.getValue() : null; } private void add(PathStateMutation mutation, Map mutations) { MutationKey key = new MutationKey(mutation.getDbName(), mutation.getCanonicalKey()); PathStateMutation previous = mutations.putIfAbsent(key, mutation); - if (previous != null && !same(previous, mutation)) { + if (previous != null && (!same(previous, mutation) + || previous.isPreviousValueKnown() != mutation.isPreviousValueKnown() + || previous.isPreviousValueKnown() + && !Arrays.equals(previous.getPreviousPhysicalValue(), + mutation.getPreviousPhysicalValue()))) { throw new ArchivePersistenceException("Conflicting path-state block mutation"); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java new file mode 100644 index 00000000000..ce6da0d7bd4 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java @@ -0,0 +1,490 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import org.tron.core.db2.core.CommonCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointTarget; + +/** Next-format State Archive participant for the common-checkpoint two-barrier protocol. */ +public final class StateArchiveCheckpointMaterializer implements CommonCheckpointMaterializer { + + static final String READABLE_FILE = "READABLE"; + static final String TARGET_DIRECTORY = "checkpoint-targets"; + static final String BLOCK_DIRECTORY = "blocks"; + static final String MATERIALIZED_FILE = "MATERIALIZED"; + + private static final int TARGET_MAGIC = 0x53414354; // SACT + private static final short TARGET_VERSION = 2; + private static final int BLOCK_MAGIC = 0x53414342; // SACB + private static final short BLOCK_VERSION = 1; + private static final int DIGEST_LENGTH = 32; + private static final int META_LENGTH = 3 * Long.BYTES + 2 * DIGEST_LENGTH; + private static final int TARGET_LENGTH = Integer.BYTES + 2 * Short.BYTES + + 4 * DIGEST_LENGTH + 2 * META_LENGTH + DIGEST_LENGTH; + private static final int BLOCK_FIXED_LENGTH = Integer.BYTES + 2 * Short.BYTES + + DIGEST_LENGTH + Integer.BYTES + DIGEST_LENGTH; + private static final long MAX_BLOCK_LENGTH = BlockHistoryCodec.DEFAULT_MAX_RECORD_LENGTH + + (long) BLOCK_FIXED_LENGTH; + + private final Path directory; + private final byte[] formatIdentity; + private final BlockHistoryCodec historyCodec = new BlockHistoryCodec(); + private final FaultHook faultHook; + + public StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity) { + this(directory, formatIdentity, (stage, blockIndex) -> { }); + } + + StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, + FaultHook faultHook) { + this.directory = Objects.requireNonNull(directory, "directory"); + this.formatIdentity = digest(formatIdentity, "formatIdentity"); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + @Override + public Authority authority() { + return Authority.STATE_ARCHIVE; + } + + @Override + public synchronized Status inspect(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = requireTarget(target); + byte[] expected = encodeTarget(admitted); + Path readable = directory.resolve(READABLE_FILE); + if (Files.exists(readable, LinkOption.NOFOLLOW_LINKS)) { + TargetMarker current = loadTarget(readable); + if (Arrays.equals(current.encoded, expected)) { + requireExact(materializedPath(admitted), expected); + requireServingIndex(admitted); + return Status.PUBLISHED; + } + requireParent(current, admitted); + } + Path materialized = materializedPath(admitted); + if (!Files.exists(materialized, LinkOption.NOFOLLOW_LINKS)) { + return Status.NEEDS_MATERIALIZATION; + } + requireExact(materialized, expected); + requireServingIndex(admitted); + return Status.MATERIALIZED; + } + + /** Loads and fully validates the target currently published by Archive READABLE. */ + public static CommonCheckpointTarget loadPublishedTarget(Path directory, + byte[] expectedFormatIdentity) throws IOException { + StateArchiveCheckpointMaterializer materializer = + new StateArchiveCheckpointMaterializer(directory, expectedFormatIdentity); + Path readable = directory.resolve(READABLE_FILE); + if (!Files.exists(readable, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("State Archive READABLE target is missing"); + } + CommonCheckpointTarget target = loadTarget(readable).target; + materializer.requireTarget(target); + if (materializer.inspect(target) != Status.PUBLISHED) { + throw new IOException("State Archive READABLE target is not fully published"); + } + return target; + } + + @Override + public synchronized void materialize(CommonCheckpointPayload payload, + CommonCheckpointTarget target) throws IOException { + CommonCheckpointPayload admittedPayload = Objects.requireNonNull(payload, "payload"); + CommonCheckpointTarget admittedTarget = requireTarget(target); + if (!admittedTarget.equals(CommonCheckpointTarget.from(admittedPayload))) { + throw new IOException("State Archive checkpoint payload and target differ"); + } + Status status = inspect(admittedTarget); + if (status != Status.NEEDS_MATERIALIZATION) { + return; + } + + Path blocks = blocksPath(admittedTarget); + createDirectory(blocks); + Set expectedNames = new HashSet<>(); + for (int index = 0; index < admittedPayload.getBlocks().size(); index++) { + CommonCheckpointPayload.BlockPayload block = admittedPayload.getBlocks().get(index); + String name = blockFileName(index, block.getMeta()); + expectedNames.add(name); + byte[] encoded = encodeBlock(block); + publishImmutable(blocks.resolve(name), encoded); + faultHook.after(Stage.AFTER_BLOCK_FILE, index); + } + requireExactBlockSet(blocks, expectedNames); + StateArchiveCheckpointServingIndex.apply(directory, admittedPayload, admittedTarget); + faultHook.after(Stage.AFTER_SERVING_INDEX_BATCH, -1); + publishImmutable(materializedPath(admittedTarget), encodeTarget(admittedTarget)); + faultHook.after(Stage.AFTER_MATERIALIZED_TARGET, -1); + } + + @Override + public synchronized void publish(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = requireTarget(target); + Status status = inspect(admitted); + if (status == Status.PUBLISHED) { + return; + } + if (status != Status.MATERIALIZED) { + throw new IOException("State Archive checkpoint target is not fully materialized"); + } + replace(directory.resolve(READABLE_FILE), encodeTarget(admitted)); + faultHook.after(Stage.AFTER_READABLE, -1); + } + + BlockReverseDiff loadBlock(CommonCheckpointTarget target, int index) throws IOException { + CommonCheckpointTarget admitted = requireTarget(target); + Path blocks = blocksPath(admitted); + String prefix = String.format("%08d-", index); + Path found = null; + try (DirectoryStream paths = Files.newDirectoryStream(blocks, prefix + "*.diff")) { + for (Path path : paths) { + if (found != null) { + throw new IOException("State Archive checkpoint block index is ambiguous"); + } + found = path; + } + } + if (found == null) { + throw new IOException("State Archive checkpoint block is missing"); + } + return loadCheckpointBlock(found); + } + + private CommonCheckpointTarget requireTarget(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + if (!Arrays.equals(formatIdentity, admitted.getFormatIdentity())) { + throw new IOException("State Archive checkpoint format identity differs"); + } + return admitted; + } + + private void requireParent(TargetMarker current, CommonCheckpointTarget target) + throws IOException { + BlockSnapshotMeta first = target.getFirstBlock(); + CommonCheckpointTarget published = current.target; + if (!Arrays.equals(published.getFormatIdentity(), target.getFormatIdentity()) + || published.getLastBlock().getEpoch() + 1 != first.getEpoch() + || published.getLastBlock().getBlockNumber() + 1 != first.getBlockNumber() + || !Arrays.equals(published.getLastBlock().getBlockHash(), first.getParentHash()) + || !Arrays.equals(published.getStateRoot(), target.getParentStateRoot())) { + throw new IOException("State Archive READABLE is not the checkpoint parent target"); + } + } + + private void requireServingIndex(CommonCheckpointTarget target) throws IOException { + if (StateArchiveCheckpointServingIndex.inspect(directory, target) + != StateArchiveCheckpointServingIndex.Status.EXACT) { + throw new IOException("State Archive checkpoint serving index target differs"); + } + } + + private Path targetPath(CommonCheckpointTarget target) { + return directory.resolve(TARGET_DIRECTORY).resolve(hex(target.getPayloadDigest())); + } + + private Path blocksPath(CommonCheckpointTarget target) { + return targetPath(target).resolve(BLOCK_DIRECTORY); + } + + private Path materializedPath(CommonCheckpointTarget target) { + return targetPath(target).resolve(MATERIALIZED_FILE); + } + + private byte[] encodeBlock(CommonCheckpointPayload.BlockPayload block) { + try { + byte[] history = historyCodec.encode(block.getArchiveDiff()); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(BLOCK_FIXED_LENGTH + history.length); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(BLOCK_MAGIC); + output.writeShort(BLOCK_VERSION); + output.writeShort(0); + output.write(block.getMutationViewDigest()); + output.writeInt(history.length); + output.write(history); + output.flush(); + byte[] body = bytes.toByteArray(); + output.write(Hashing.sha256().hashBytes(body).asBytes()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory State Archive block encoding failed", impossible); + } + } + + static BlockReverseDiff loadCheckpointBlock(Path path) throws IOException { + return decodeBlock(readBounded(path, MAX_BLOCK_LENGTH)); + } + + private static BlockReverseDiff decodeBlock(byte[] encoded) throws IOException { + if (encoded.length < BLOCK_FIXED_LENGTH) { + throw new IOException("State Archive checkpoint block is truncated"); + } + int bodyLength = encoded.length - DIGEST_LENGTH; + byte[] body = Arrays.copyOf(encoded, bodyLength); + byte[] checksum = Arrays.copyOfRange(encoded, bodyLength, encoded.length); + if (!Arrays.equals(checksum, Hashing.sha256().hashBytes(body).asBytes())) { + throw new IOException("State Archive checkpoint block checksum differs"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(body))) { + if (input.readInt() != BLOCK_MAGIC || input.readShort() != BLOCK_VERSION + || input.readShort() != 0) { + throw new IOException("State Archive checkpoint block format is unsupported"); + } + byte[] viewDigest = readDigest(input); + int historyLength = input.readInt(); + if (historyLength <= 0 || historyLength != input.available()) { + throw new IOException("State Archive checkpoint history length is invalid"); + } + byte[] history = new byte[historyLength]; + input.readFully(history); + BlockReverseDiff decoded; + try { + decoded = new BlockHistoryCodec().decode(history); + } catch (IllegalArgumentException invalid) { + throw new IOException("State Archive checkpoint history is corrupt", invalid); + } + return new BlockReverseDiff(decoded.getMeta(), decoded.getGroups(), viewDigest); + } catch (EOFException truncated) { + throw new IOException("State Archive checkpoint block is truncated", truncated); + } + } + + static String blockFileName(int index, BlockSnapshotMeta meta) { + return String.format("%08d-%020d-%s.diff", index, meta.getEpoch(), hex(meta.getBlockHash())); + } + + private static byte[] encodeTarget(CommonCheckpointTarget target) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(TARGET_LENGTH); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(TARGET_MAGIC); + output.writeShort(TARGET_VERSION); + output.writeShort(0); + output.write(target.getFormatIdentity()); + output.write(target.getPayloadDigest()); + writeMeta(output, target.getFirstBlock()); + writeMeta(output, target.getLastBlock()); + output.write(target.getParentStateRoot()); + output.write(target.getStateRoot()); + output.flush(); + byte[] body = bytes.toByteArray(); + output.write(Hashing.sha256().hashBytes(body).asBytes()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory State Archive target encoding failed", impossible); + } + } + + private static TargetMarker loadTarget(Path path) throws IOException { + byte[] encoded = readBounded(path, TARGET_LENGTH); + if (encoded.length != TARGET_LENGTH) { + throw new IOException("State Archive checkpoint target length is invalid"); + } + int bodyLength = encoded.length - DIGEST_LENGTH; + byte[] body = Arrays.copyOf(encoded, bodyLength); + byte[] checksum = Arrays.copyOfRange(encoded, bodyLength, encoded.length); + if (!Arrays.equals(checksum, Hashing.sha256().hashBytes(body).asBytes())) { + throw new IOException("State Archive checkpoint target checksum differs"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(body))) { + if (input.readInt() != TARGET_MAGIC || input.readShort() != TARGET_VERSION + || input.readShort() != 0) { + throw new IOException("State Archive checkpoint target format is unsupported"); + } + byte[] formatIdentity = readDigest(input); + byte[] payloadDigest = readDigest(input); + BlockSnapshotMeta first = readMeta(input); + BlockSnapshotMeta last = readMeta(input); + byte[] parentStateRoot = readDigest(input); + byte[] stateRoot = readDigest(input); + CommonCheckpointTarget target; + try { + target = CommonCheckpointTarget.restore(formatIdentity, payloadDigest, first, last, + parentStateRoot, stateRoot); + } catch (IllegalArgumentException invalid) { + throw new IOException("State Archive checkpoint target identity is invalid", invalid); + } + return new TargetMarker(encoded, target); + } catch (EOFException truncated) { + throw new IOException("State Archive checkpoint target is truncated", truncated); + } + } + + private static void requireExact(Path path, byte[] expected) throws IOException { + if (!Arrays.equals(loadTarget(path).encoded, expected)) { + throw new IOException("State Archive checkpoint target identity differs"); + } + } + + private static void requireExactBlockSet(Path directory, Set expected) + throws IOException { + Set actual = new HashSet<>(); + try (DirectoryStream paths = Files.newDirectoryStream(directory)) { + for (Path path : paths) { + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) + || !actual.add(path.getFileName().toString())) { + throw new IOException("State Archive checkpoint block directory is ambiguous"); + } + } + } + if (!actual.equals(expected)) { + throw new IOException("State Archive checkpoint block set differs"); + } + } + + private static void createDirectory(Path path) throws IOException { + Files.createDirectories(path); + if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("State Archive checkpoint path is not a directory"); + } + } + + private static void publishImmutable(Path path, byte[] bytes) throws IOException { + createDirectory(path.getParent()); + if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + if (!Arrays.equals(readBounded(path, bytes.length), bytes)) { + throw new IOException("State Archive immutable checkpoint file differs"); + } + return; + } + Path temporary = path.resolveSibling(path.getFileName() + ".tmp-" + UUID.randomUUID()); + try { + writeForced(temporary, bytes); + try { + Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("State Archive filesystem lacks atomic immutable publication", + unsupported); + } catch (java.nio.file.FileAlreadyExistsException raced) { + if (!Arrays.equals(readBounded(path, bytes.length), bytes)) { + throw new IOException("State Archive immutable checkpoint publication raced", raced); + } + } + HistorySegmentStore.syncDirectory(path.getParent()); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static void replace(Path path, byte[] bytes) throws IOException { + createDirectory(path.getParent()); + Path temporary = path.resolveSibling(path.getFileName() + ".tmp-" + UUID.randomUUID()); + try { + writeForced(temporary, bytes); + try { + Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("State Archive filesystem lacks atomic READABLE replacement", + unsupported); + } + HistorySegmentStore.syncDirectory(path.getParent()); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static void writeForced(Path path, byte[] bytes) throws IOException { + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + } + + private static byte[] readBounded(Path path, long maximum) throws IOException { + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("State Archive checkpoint path is not a regular file"); + } + long size = Files.size(path); + if (size <= 0 || size > maximum) { + throw new IOException("State Archive checkpoint file length is invalid"); + } + return Files.readAllBytes(path); + } + + private static byte[] readDigest(DataInputStream input) throws IOException { + byte[] value = new byte[DIGEST_LENGTH]; + input.readFully(value); + return value; + } + + private static void writeMeta(DataOutputStream output, BlockSnapshotMeta meta) + throws IOException { + output.writeLong(meta.getEpoch()); + output.writeLong(meta.getBlockNumber()); + output.write(meta.getBlockHash()); + output.write(meta.getParentHash()); + output.writeLong(meta.getTimestamp()); + } + + private static BlockSnapshotMeta readMeta(DataInputStream input) throws IOException { + return new BlockSnapshotMeta(input.readLong(), input.readLong(), readDigest(input), + readDigest(input), input.readLong()); + } + + private static byte[] digest(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length != DIGEST_LENGTH) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return copy; + } + + static String hex(byte[] value) { + StringBuilder encoded = new StringBuilder(value.length * 2); + for (byte current : value) { + encoded.append(Character.forDigit(current >>> 4 & 0xf, 16)); + encoded.append(Character.forDigit(current & 0xf, 16)); + } + return encoded.toString(); + } + + enum Stage { + AFTER_BLOCK_FILE, + AFTER_SERVING_INDEX_BATCH, + AFTER_MATERIALIZED_TARGET, + AFTER_READABLE + } + + @FunctionalInterface + interface FaultHook { + void after(Stage stage, int blockIndex) throws IOException; + } + + private static final class TargetMarker { + + private final byte[] encoded; + private final CommonCheckpointTarget target; + + private TargetMarker(byte[] encoded, CommonCheckpointTarget target) { + this.encoded = encoded; + this.target = target; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java new file mode 100644 index 00000000000..03677c16419 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java @@ -0,0 +1,85 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalLong; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Status; +import org.tron.core.db2.core.CommonCheckpointTarget; + +/** Request-owned exact-key reader for one published next-format checkpoint target. */ +public final class StateArchiveCheckpointReadAdapter implements AutoCloseable { + + private final CommonCheckpointTarget target; + private final StateArchiveCheckpointServingIndex.Reader reader; + private boolean closed; + + private StateArchiveCheckpointReadAdapter(CommonCheckpointTarget target, + StateArchiveCheckpointServingIndex.Reader reader) { + this.target = target; + this.reader = reader; + } + + /** Opens only an exact target whose Archive READABLE and serving-index markers are published. */ + public static StateArchiveCheckpointReadAdapter open(Path archiveDirectory, + CommonCheckpointTarget target) throws IOException { + Path directory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + StateArchiveCheckpointMaterializer materializer = + new StateArchiveCheckpointMaterializer(directory, admitted.getFormatIdentity()); + if (materializer.inspect(admitted) != Status.PUBLISHED) { + throw new IOException("State Archive checkpoint target is not published for reading"); + } + return new StateArchiveCheckpointReadAdapter(admitted, + StateArchiveCheckpointServingIndex.openReader(directory, admitted)); + } + + /** Reconstructs the published target from disk before opening the exact-point reader. */ + public static StateArchiveCheckpointReadAdapter open(Path archiveDirectory, + byte[] expectedFormatIdentity) throws IOException { + Path directory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + return open(directory, StateArchiveCheckpointMaterializer.loadPublishedTarget(directory, + expectedFormatIdentity)); + } + + /** + * Returns the old value from the first change in {@code (targetBlock, publishedHead]}, or empty + * when the caller must use its pinned latest-state value. + */ + public synchronized Optional findOldValueAfter(String dbName, byte[] rawKey, + long targetBlock) throws IOException { + ensureOpen(); + OptionalLong first = reader.firstChangeAfter(dbName, rawKey, targetBlock, + target.getLastBlock().getBlockNumber()); + return first.isPresent() + ? Optional.of(reader.readOldValue(dbName, rawKey, first.getAsLong())) + : Optional.empty(); + } + + public long getIndexedFrom() { + return reader.getIndexedFrom(); + } + + public long getIndexedThrough() { + return reader.getIndexedThrough(); + } + + public byte[] getHeadHash() { + return reader.getHeadHash(); + } + + @Override + public synchronized void close() { + if (!closed) { + closed = true; + reader.close(); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("State Archive checkpoint read adapter is closed"); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java new file mode 100644 index 00000000000..19fdb997cc9 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java @@ -0,0 +1,181 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.core.CommonCheckpointRuntimeOwner; + +/** Request-owned, point-only view over one published next-format checkpoint head. */ +public final class StateArchiveCheckpointReadSnapshot implements Closeable { + + private final long targetBlock; + private final long pinnedBlock; + private final byte[] pinnedHash; + private final CommonCheckpointRuntimeOwner.ReadLease lease; + private final StateArchiveCheckpointReadAdapter archive; + private final PinnedLatestState latest; + private boolean closed; + + private StateArchiveCheckpointReadSnapshot(long targetBlock, + CommonCheckpointRuntimeOwner.ReadLease lease, + StateArchiveCheckpointReadAdapter archive, PinnedLatestState latest) { + this.targetBlock = targetBlock; + this.pinnedBlock = archive.getIndexedThrough(); + this.pinnedHash = archive.getHeadHash(); + this.lease = lease; + this.archive = archive; + this.latest = latest; + validateIdentity(); + } + + /** Pins the publication gate, Archive index, and latest engine head as one request unit. */ + public static StateArchiveCheckpointReadSnapshot pin(long targetBlock, + CommonCheckpointRuntimeOwner owner, Path archiveDirectory, byte[] expectedFormatIdentity, + PinnedLatestStateFactory latestFactory) throws IOException { + CommonCheckpointRuntimeOwner admittedOwner = Objects.requireNonNull(owner, "owner"); + CommonCheckpointRuntimeOwner.ReadLease lease = admittedOwner.acquireReadLease(); + StateArchiveCheckpointReadAdapter archive = null; + PinnedLatestState latest = null; + try { + archive = StateArchiveCheckpointReadAdapter.open(archiveDirectory, + expectedFormatIdentity); + if (targetBlock < archive.getIndexedFrom() || targetBlock > archive.getIndexedThrough()) { + throw new IllegalArgumentException("checkpoint target block is outside indexed coverage"); + } + latest = Objects.requireNonNull(latestFactory, "latestFactory").pin( + archive.getIndexedThrough(), archive.getHeadHash()); + return new StateArchiveCheckpointReadSnapshot(targetBlock, lease, archive, + Objects.requireNonNull(latest, "pinned latest state")); + } catch (IOException | RuntimeException failure) { + closeAfterFailedPin(lease, archive, latest, failure); + throw failure; + } + } + + /** Returns the first reverse-diff old value, or the same-request pinned latest value. */ + public synchronized OldValue get(String dbName, byte[] physicalRawKey) throws IOException { + ensureOpen(); + Optional historical = archive.findOldValueAfter(dbName, physicalRawKey, + targetBlock); + OldValue value = historical.isPresent() + ? historical.get() : latest.get(dbName, physicalRawKey); + if (value == null) { + throw new IllegalStateException("Pinned latest state returned null"); + } + return value; + } + + public long getTargetBlock() { + return targetBlock; + } + + public long getPinnedBlock() { + return pinnedBlock; + } + + public byte[] getPinnedHash() { + return Arrays.copyOf(pinnedHash, pinnedHash.length); + } + + /** Revalidates the request-owned history and latest head identity. */ + public synchronized void requirePinnedIdentity() { + ensureOpen(); + validateIdentity(); + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + IOException failure = null; + try { + latest.close(); + } catch (IOException e) { + failure = e; + } + try { + archive.close(); + } catch (RuntimeException e) { + if (failure == null) { + failure = new IOException("Failed to close checkpoint Archive reader", e); + } else { + failure.addSuppressed(e); + } + } finally { + lease.close(); + } + if (failure != null) { + throw failure; + } + } + + private void validateIdentity() { + if (pinnedBlock != archive.getIndexedThrough() + || !Arrays.equals(pinnedHash, archive.getHeadHash()) + || latest.getBlockNumber() != pinnedBlock + || !Arrays.equals(pinnedHash, latest.getBlockHash())) { + throw new IllegalArgumentException("checkpoint Archive read snapshot identity mismatch"); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("checkpoint Archive read snapshot is closed"); + } + } + + private static void closeAfterFailedPin(CommonCheckpointRuntimeOwner.ReadLease lease, + StateArchiveCheckpointReadAdapter archive, PinnedLatestState latest, + Exception failure) { + IOException closeFailure = closeResources(lease, archive, latest); + if (closeFailure != null) { + failure.addSuppressed(closeFailure); + } + } + + private static IOException closeResources(CommonCheckpointRuntimeOwner.ReadLease lease, + StateArchiveCheckpointReadAdapter archive, PinnedLatestState latest) { + IOException failure = null; + if (latest != null) { + try { + latest.close(); + } catch (IOException e) { + failure = e; + } + } + if (archive != null) { + try { + archive.close(); + } catch (RuntimeException e) { + failure = append(failure, new IOException( + "Failed to close checkpoint Archive reader", e)); + } + } + try { + lease.close(); + } catch (RuntimeException e) { + failure = append(failure, new IOException( + "Failed to release common checkpoint read lease", e)); + } + return failure; + } + + private static IOException append(IOException failure, IOException addition) { + if (failure == null) { + return addition; + } + failure.addSuppressed(addition); + return failure; + } + + @FunctionalInterface + public interface PinnedLatestStateFactory { + PinnedLatestState pin(long blockNumber, byte[] blockHash) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java new file mode 100644 index 00000000000..3e037623062 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java @@ -0,0 +1,449 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Objects; +import java.util.OptionalLong; +import org.rocksdb.Options; +import org.rocksdb.ReadOptions; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointTarget; + +/** Persistent exact-key locator for next-format per-block checkpoint history files. */ +final class StateArchiveCheckpointServingIndex { + + static final String DIRECTORY = "checkpoint-serving-index"; + private static final String DATABASE = "keys"; + private static final byte[] MARKER_KEY = new byte[]{0}; + private static final byte CHANGE_PREFIX = 1; + private static final byte BLOCK_PREFIX = 2; + private static final int MARKER_MAGIC = 0x53414349; // SACI + private static final short MARKER_VERSION = 1; + private static final int DIGEST_LENGTH = 32; + private static final int MARKER_LENGTH = Integer.BYTES + 2 * Short.BYTES + + 5 * DIGEST_LENGTH + 3 * Long.BYTES + DIGEST_LENGTH; + private static final int LOCATION_LENGTH = DIGEST_LENGTH + Integer.BYTES + Long.BYTES + + DIGEST_LENGTH; + + static { + RocksDB.loadLibrary(); + } + + private StateArchiveCheckpointServingIndex() { + } + + static Status inspect(Path archiveDirectory, CommonCheckpointTarget target) + throws IOException { + Path databasePath = databasePath(archiveDirectory); + if (!Files.exists(databasePath, LinkOption.NOFOLLOW_LINKS)) { + return Status.ABSENT; + } + try (Options options = new Options().setCreateIfMissing(false); + RocksDB database = RocksDB.openReadOnly(options, databasePath.toString())) { + byte[] encoded = database.get(MARKER_KEY); + if (encoded == null) { + throw new IOException("State Archive checkpoint serving marker is missing"); + } + Marker marker = decodeMarker(encoded); + if (Arrays.equals(encoded, encodeMarker(target, marker.baseBlockNumber, + marker.baseBlockHash))) { + return Status.EXACT; + } + requireParent(marker, target); + return Status.PARENT; + } catch (RocksDBException failure) { + throw new IOException("Failed to inspect State Archive checkpoint serving index", failure); + } + } + + static void apply(Path archiveDirectory, CommonCheckpointPayload payload, + CommonCheckpointTarget target) throws IOException { + Status status = inspect(archiveDirectory, target); + if (status == Status.EXACT) { + return; + } + Path indexDirectory = archiveDirectory.resolve(DIRECTORY); + Files.createDirectories(indexDirectory); + if (!Files.isDirectory(indexDirectory, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("State Archive checkpoint serving path is not a directory"); + } + long baseBlockNumber = target.getFirstBlock().getBlockNumber() - 1; + byte[] baseBlockHash = target.getFirstBlock().getParentHash(); + Path databasePath = databasePath(archiveDirectory); + try (Options options = new Options().setCreateIfMissing(true); + RocksDB database = RocksDB.open(options, databasePath.toString()); + WriteBatch batch = new WriteBatch(); + WriteOptions writes = new WriteOptions().setSync(true)) { + byte[] existing = database.get(MARKER_KEY); + if (existing != null) { + Marker parent = decodeMarker(existing); + requireParent(parent, target); + baseBlockNumber = parent.baseBlockNumber; + baseBlockHash = parent.baseBlockHash; + } + for (int index = 0; index < payload.getBlocks().size(); index++) { + CommonCheckpointPayload.BlockPayload block = payload.getBlocks().get(index); + long blockNumber = block.getMeta().getBlockNumber(); + for (DbGroup group : block.getArchiveDiff().getGroups()) { + requireStateDatabase(group.getDbName()); + for (Entry entry : group.getEntries()) { + batch.put(changeKey(group.getDbName(), entry.getKey(), blockNumber), new byte[]{1}); + } + } + batch.put(blockKey(blockNumber), encodeLocation(target.getPayloadDigest(), index, + block.getMeta())); + } + batch.put(MARKER_KEY, encodeMarker(target, baseBlockNumber, baseBlockHash)); + database.write(writes, batch); + } catch (RocksDBException failure) { + throw new IOException("Failed to materialize State Archive checkpoint serving index", + failure); + } + HistorySegmentStore.syncDirectory(indexDirectory); + } + + static Reader openReader(Path archiveDirectory, CommonCheckpointTarget target) + throws IOException { + return new Reader(archiveDirectory, target); + } + + private static void requireParent(Marker marker, CommonCheckpointTarget target) + throws IOException { + BlockSnapshotMeta first = target.getFirstBlock(); + if (!Arrays.equals(marker.formatIdentity, target.getFormatIdentity()) + || marker.lastEpoch + 1 != first.getEpoch() + || marker.lastBlockNumber + 1 != first.getBlockNumber() + || !Arrays.equals(marker.lastBlockHash, first.getParentHash()) + || !Arrays.equals(marker.stateRoot, target.getParentStateRoot())) { + throw new IOException("State Archive checkpoint serving index is not the target parent"); + } + } + + private static byte[] changeKey(String dbName, byte[] rawKey, long blockNumber) { + int storeId = ArchiveParticipantDescriptor.current().getStoreId(dbName); + byte[] key = Objects.requireNonNull(rawKey, "rawKey"); + return ByteBuffer.allocate(1 + Short.BYTES + Integer.BYTES + key.length + Long.BYTES) + .put(CHANGE_PREFIX).putShort((short) storeId).putInt(key.length).put(key) + .putLong(blockNumber).array(); + } + + private static byte[] changePrefix(String dbName, byte[] rawKey) { + int storeId = ArchiveParticipantDescriptor.current().getStoreId(dbName); + byte[] key = Objects.requireNonNull(rawKey, "rawKey"); + return ByteBuffer.allocate(1 + Short.BYTES + Integer.BYTES + key.length) + .put(CHANGE_PREFIX).putShort((short) storeId).putInt(key.length).put(key).array(); + } + + private static byte[] blockKey(long blockNumber) { + return ByteBuffer.allocate(1 + Long.BYTES).put(BLOCK_PREFIX).putLong(blockNumber).array(); + } + + private static byte[] encodeLocation(byte[] targetDigest, int index, BlockSnapshotMeta meta) { + return ByteBuffer.allocate(LOCATION_LENGTH).put(targetDigest).putInt(index) + .putLong(meta.getEpoch()).put(meta.getBlockHash()).array(); + } + + private static Location decodeLocation(byte[] encoded) throws IOException { + if (encoded == null || encoded.length != LOCATION_LENGTH) { + throw new IOException("State Archive checkpoint block location is missing or corrupt"); + } + ByteBuffer input = ByteBuffer.wrap(encoded); + byte[] targetDigest = new byte[DIGEST_LENGTH]; + input.get(targetDigest); + int index = input.getInt(); + long epoch = input.getLong(); + byte[] blockHash = new byte[DIGEST_LENGTH]; + input.get(blockHash); + if (index < 0 || epoch < 0) { + throw new IOException("State Archive checkpoint block location is invalid"); + } + return new Location(targetDigest, index, epoch, blockHash); + } + + private static byte[] encodeMarker(CommonCheckpointTarget target, long baseBlockNumber, + byte[] baseBlockHash) { + if (baseBlockNumber < 0) { + throw new IllegalArgumentException("checkpoint serving base block must not be negative"); + } + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(MARKER_LENGTH); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MARKER_MAGIC); + output.writeShort(MARKER_VERSION); + output.writeShort(0); + output.write(target.getFormatIdentity()); + output.write(target.getPayloadDigest()); + output.writeLong(baseBlockNumber); + output.write(baseBlockHash); + output.writeLong(target.getLastBlock().getEpoch()); + output.writeLong(target.getLastBlock().getBlockNumber()); + output.write(target.getLastBlock().getBlockHash()); + output.write(target.getStateRoot()); + output.flush(); + byte[] body = bytes.toByteArray(); + output.write(Hashing.sha256().hashBytes(body).asBytes()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory checkpoint serving marker encoding failed", + impossible); + } + } + + private static Marker decodeMarker(byte[] encoded) throws IOException { + if (encoded == null || encoded.length != MARKER_LENGTH) { + throw new IOException("State Archive checkpoint serving marker length is invalid"); + } + int bodyLength = encoded.length - DIGEST_LENGTH; + byte[] body = Arrays.copyOf(encoded, bodyLength); + if (!Arrays.equals(Arrays.copyOfRange(encoded, bodyLength, encoded.length), + Hashing.sha256().hashBytes(body).asBytes())) { + throw new IOException("State Archive checkpoint serving marker checksum differs"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(body))) { + if (input.readInt() != MARKER_MAGIC || input.readShort() != MARKER_VERSION + || input.readShort() != 0) { + throw new IOException("State Archive checkpoint serving marker format is unsupported"); + } + byte[] formatIdentity = readDigest(input); + byte[] payloadDigest = readDigest(input); + long baseBlockNumber = input.readLong(); + byte[] baseBlockHash = readDigest(input); + long lastEpoch = input.readLong(); + long lastBlockNumber = input.readLong(); + byte[] lastBlockHash = readDigest(input); + byte[] stateRoot = readDigest(input); + if (baseBlockNumber < 0 || lastEpoch < 0 || lastBlockNumber <= baseBlockNumber) { + throw new IOException("State Archive checkpoint serving marker range is invalid"); + } + return new Marker(formatIdentity, payloadDigest, baseBlockNumber, baseBlockHash, lastEpoch, + lastBlockNumber, lastBlockHash, stateRoot); + } catch (EOFException truncated) { + throw new IOException("State Archive checkpoint serving marker is truncated", truncated); + } + } + + private static Path databasePath(Path archiveDirectory) { + return archiveDirectory.resolve(DIRECTORY).resolve(DATABASE); + } + + private static void requireStateDatabase(String dbName) throws IOException { + if (!ArchiveStoreScope.isStateDatabase(dbName)) { + throw new IOException("State Archive checkpoint contains a non-state Store: " + dbName); + } + } + + private static byte[] readDigest(DataInputStream input) throws IOException { + byte[] value = new byte[DIGEST_LENGTH]; + input.readFully(value); + return value; + } + + private static boolean startsWith(byte[] value, byte[] prefix) { + if (value.length < prefix.length) { + return false; + } + for (int index = 0; index < prefix.length; index++) { + if (value[index] != prefix[index]) { + return false; + } + } + return true; + } + + enum Status { + ABSENT, + PARENT, + EXACT + } + + static final class Reader implements AutoCloseable { + + private final Path archiveDirectory; + private final Options options; + private final RocksDB database; + private final Marker marker; + private boolean closed; + + private Reader(Path archiveDirectory, CommonCheckpointTarget target) throws IOException { + this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + Objects.requireNonNull(target, "target"); + this.options = new Options().setCreateIfMissing(false); + RocksDB opened; + try { + opened = RocksDB.openReadOnly(options, databasePath(archiveDirectory).toString()); + } catch (RocksDBException | RuntimeException failure) { + options.close(); + throw new IOException("Failed to open State Archive checkpoint serving reader", failure); + } + this.database = opened; + Marker loaded; + try { + byte[] encoded = opened.get(MARKER_KEY); + loaded = decodeMarker(encoded); + if (!Arrays.equals(encoded, encodeMarker(target, loaded.baseBlockNumber, + loaded.baseBlockHash))) { + throw new IOException("State Archive checkpoint reader target differs"); + } + } catch (IOException | RocksDBException | RuntimeException failure) { + opened.close(); + options.close(); + if (failure instanceof IOException) { + throw (IOException) failure; + } + throw new IOException("Failed to validate State Archive checkpoint serving reader", + failure); + } + this.marker = loaded; + } + + OptionalLong firstChangeAfter(String dbName, byte[] rawKey, long targetBlock, + long upperBound) throws IOException { + ensureOpen(); + requireStateDatabase(dbName); + if (targetBlock < marker.baseBlockNumber || upperBound > marker.lastBlockNumber + || targetBlock > upperBound) { + throw new IllegalArgumentException("checkpoint serving query is outside coverage"); + } + if (targetBlock == Long.MAX_VALUE) { + return OptionalLong.empty(); + } + byte[] prefix = changePrefix(dbName, rawKey); + byte[] seek = ByteBuffer.allocate(prefix.length + Long.BYTES).put(prefix) + .putLong(targetBlock + 1).array(); + try (ReadOptions reads = new ReadOptions(); + RocksIterator iterator = database.newIterator(reads)) { + iterator.seek(seek); + if (!iterator.isValid()) { + return OptionalLong.empty(); + } + byte[] found = iterator.key(); + if (found.length != prefix.length + Long.BYTES || !startsWith(found, prefix)) { + return OptionalLong.empty(); + } + long blockNumber = ByteBuffer.wrap(found, prefix.length, Long.BYTES).getLong(); + return blockNumber <= upperBound ? OptionalLong.of(blockNumber) : OptionalLong.empty(); + } + } + + OldValue readOldValue(String dbName, byte[] rawKey, long blockNumber) throws IOException { + ensureOpen(); + requireStateDatabase(dbName); + if (blockNumber <= marker.baseBlockNumber || blockNumber > marker.lastBlockNumber) { + throw new IllegalArgumentException("checkpoint history block is outside coverage"); + } + Location location; + try { + location = decodeLocation(database.get(blockKey(blockNumber))); + } catch (RocksDBException failure) { + throw new IOException("Failed to read State Archive checkpoint block location", failure); + } + String fileName = StateArchiveCheckpointMaterializer.blockFileName(location.index, + new BlockSnapshotMeta(location.epoch, blockNumber, location.blockHash, + new byte[DIGEST_LENGTH], 0)); + Path path = archiveDirectory.resolve(StateArchiveCheckpointMaterializer.TARGET_DIRECTORY) + .resolve(StateArchiveCheckpointMaterializer.hex(location.targetDigest)) + .resolve(StateArchiveCheckpointMaterializer.BLOCK_DIRECTORY).resolve(fileName); + BlockReverseDiff diff = StateArchiveCheckpointMaterializer.loadCheckpointBlock(path); + if (diff.getMeta().getBlockNumber() != blockNumber + || diff.getMeta().getEpoch() != location.epoch + || !Arrays.equals(diff.getMeta().getBlockHash(), location.blockHash)) { + throw new IOException("State Archive checkpoint block location identity differs"); + } + for (DbGroup group : diff.getGroups()) { + if (group.getDbName().equals(dbName)) { + for (Entry entry : group.getEntries()) { + if (Arrays.equals(entry.getKey(), rawKey)) { + return entry.getOldValue(); + } + } + } + } + throw new IOException("State Archive checkpoint index references a missing key"); + } + + long getIndexedFrom() { + return marker.baseBlockNumber; + } + + long getIndexedThrough() { + return marker.lastBlockNumber; + } + + byte[] getHeadHash() { + return Arrays.copyOf(marker.lastBlockHash, marker.lastBlockHash.length); + } + + @Override + public void close() { + if (!closed) { + closed = true; + database.close(); + options.close(); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("State Archive checkpoint serving reader is closed"); + } + } + } + + private static final class Marker { + + private final byte[] formatIdentity; + private final byte[] payloadDigest; + private final long baseBlockNumber; + private final byte[] baseBlockHash; + private final long lastEpoch; + private final long lastBlockNumber; + private final byte[] lastBlockHash; + private final byte[] stateRoot; + + private Marker(byte[] formatIdentity, byte[] payloadDigest, long baseBlockNumber, + byte[] baseBlockHash, long lastEpoch, long lastBlockNumber, byte[] lastBlockHash, + byte[] stateRoot) { + this.formatIdentity = formatIdentity; + this.payloadDigest = payloadDigest; + this.baseBlockNumber = baseBlockNumber; + this.baseBlockHash = baseBlockHash; + this.lastEpoch = lastEpoch; + this.lastBlockNumber = lastBlockNumber; + this.lastBlockHash = lastBlockHash; + this.stateRoot = stateRoot; + } + } + + private static final class Location { + + private final byte[] targetDigest; + private final int index; + private final long epoch; + private final byte[] blockHash; + + private Location(byte[] targetDigest, int index, long epoch, byte[] blockHash) { + this.targetDigest = targetDigest; + this.index = index; + this.epoch = epoch; + this.blockHash = blockHash; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java index f9a63291766..2ed92e30b0b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java @@ -554,19 +554,15 @@ private void validateReadableState(PersistentServingKeyIndexCatalog catalog, private ArchiveProgressEnvelope readReadableAuthority() throws IOException { BlockSnapshotMeta head = readableHead; - ArchiveHistoryWriter writer = historyWriter; PersistentServingKeyIndexCatalog catalog = servingIndexCatalog; LatestStateGenerationCoordinator latest = latestStateCoordinator; - ArchiveWalBinding binding = snapshotManager.getLatestArchiveWalBinding(); - if (binding == null) { - binding = snapshotManager.getRecoveredArchiveWalBinding(); - } - BlockSnapshotMeta persisted = binding == null ? recoveredHead : binding.getLast(); - if (head == null || writer == null || catalog == null || latest == null - || snapshotManager.getArchiveReadableEpoch() != head.getEpoch() - || !head.equals(writer.committedHeadMeta()) || !head.equals(persisted)) { + // H and the WAL binding may advance before the next reader generation is published. Queries + // continue to pin the previous immutable serving/latest generation under this owner's lock; + // write/recovery callers retain the stricter P/H/I/latest/R check above. + if (head == null || catalog == null || latest == null + || snapshotManager.getArchiveReadableEpoch() != head.getEpoch()) { throw new ArchivePersistenceException( - "Archive historical query is outside the P/H/I/latest/R fixed point"); + "Archive historical query has no published reader generation"); } try (PersistentServingKeyIndexGeneration serving = catalog.pin()) { if (!serving.isLatestSourceIdentityBound() diff --git a/chainbase/src/main/java/org/tron/core/db2/common/Flusher.java b/chainbase/src/main/java/org/tron/core/db2/common/Flusher.java index 1faf35c4809..76beb62b08d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/common/Flusher.java +++ b/chainbase/src/main/java/org/tron/core/db2/common/Flusher.java @@ -6,6 +6,11 @@ public interface Flusher { void flush(Map batch); + /** Flushes one checkpoint batch with an explicit durability barrier. */ + default void flushSynced(Map batch) { + throw new UnsupportedOperationException("Synchronous checkpoint flush is not supported"); + } + void close(); void reset(); diff --git a/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java b/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java index 5fdf3835a7d..5842cb322da 100644 --- a/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java +++ b/chainbase/src/main/java/org/tron/core/db2/common/LevelDB.java @@ -113,10 +113,22 @@ public DBIterator iterator() { @Override public void flush(Map batch) { + flush(batch, writeOptions); + } + + @Override + public void flushSynced(Map batch) { + try (WriteOptionsWrapper synced = WriteOptionsWrapper.getInstance().sync(true)) { + flush(batch, synced); + } + } + + private void flush(Map batch, + WriteOptionsWrapper options) { Map rows = batch.entrySet().stream() .map(e -> Maps.immutableEntry(e.getKey().getBytes(), e.getValue().getBytes())) .collect(HashMap::new, (m, k) -> m.put(k.getKey(), k.getValue()), HashMap::putAll); - db.updateByBatch(rows, writeOptions); + db.updateByBatch(rows, options); } @Override diff --git a/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java b/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java index 91118c3fd29..8fa39ae5906 100644 --- a/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java +++ b/chainbase/src/main/java/org/tron/core/db2/common/RocksDB.java @@ -114,10 +114,22 @@ public DBIterator iterator() { @Override public void flush(Map batch) { + flush(batch, writeOptions); + } + + @Override + public void flushSynced(Map batch) { + try (WriteOptionsWrapper synced = WriteOptionsWrapper.getInstance().sync(true)) { + flush(batch, synced); + } + } + + private void flush(Map batch, + WriteOptionsWrapper options) { Map rows = batch.entrySet().stream() .map(e -> Maps.immutableEntry(e.getKey().getBytes(), e.getValue().getBytes())) .collect(HashMap::new, (m, k) -> m.put(k.getKey(), k.getValue()), HashMap::putAll); - db.updateByBatch(rows, writeOptions); + db.updateByBatch(rows, options); } @Override diff --git a/chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java new file mode 100644 index 00000000000..975f5321d70 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java @@ -0,0 +1,358 @@ +package org.tron.core.db2.core; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.common.WrappedByteArray; + +/** Chainbase participant for common-checkpoint idempotent materialization and publication. */ +public final class ChainbaseCheckpointMaterializer implements CommonCheckpointMaterializer { + + static final String CURRENT_FILE = "CHAINBASE_CURRENT"; + static final String MATERIALIZED_DIRECTORY = "chainbase-checkpoint-materialized"; + private static final int MAGIC = 0x43424354; // CBCT + private static final short VERSION = 1; + private static final int DIGEST_LENGTH = 32; + private static final int RECORD_LENGTH = Integer.BYTES + 2 * Short.BYTES + + 4 * DIGEST_LENGTH + 2 * Long.BYTES + DIGEST_LENGTH; + + private final Path directory; + private final byte[] formatIdentity; + private final Map databases; + private final FaultHook faultHook; + + public ChainbaseCheckpointMaterializer(Path directory, byte[] formatIdentity, + List databases) { + this(directory, formatIdentity, databases, (stage, dbName) -> { }); + } + + ChainbaseCheckpointMaterializer(Path directory, byte[] formatIdentity, + List databases, FaultHook faultHook) { + this.directory = Objects.requireNonNull(directory, "directory"); + this.formatIdentity = digest(formatIdentity, "formatIdentity"); + this.databases = index(databases); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + @Override + public Authority authority() { + return Authority.CHAINBASE; + } + + @Override + public synchronized Status inspect(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = requireTarget(target); + byte[] expected = encode(admitted); + Path currentPath = directory.resolve(CURRENT_FILE); + if (Files.exists(currentPath, LinkOption.NOFOLLOW_LINKS)) { + Marker current = load(currentPath); + if (Arrays.equals(current.encoded, expected)) { + requireExact(materializedPath(admitted), expected); + return Status.PUBLISHED; + } + requireParent(current, admitted); + } + Path materialized = materializedPath(admitted); + if (!Files.exists(materialized, LinkOption.NOFOLLOW_LINKS)) { + return Status.NEEDS_MATERIALIZATION; + } + requireExact(materialized, expected); + return Status.MATERIALIZED; + } + + @Override + public synchronized void materialize(CommonCheckpointPayload payload, + CommonCheckpointTarget target) throws IOException { + CommonCheckpointPayload admittedPayload = Objects.requireNonNull(payload, "payload"); + CommonCheckpointTarget admittedTarget = requireTarget(target); + if (!admittedTarget.equals(CommonCheckpointTarget.from(admittedPayload))) { + throw new IOException("Chainbase checkpoint payload and target differ"); + } + Status status = inspect(admittedTarget); + if (status != Status.NEEDS_MATERIALIZATION) { + return; + } + for (CommonCheckpointPayload.StoreMutations store + : admittedPayload.getChainbaseStores()) { + Chainbase database = databases.get(store.getDbName()); + if (database == null) { + throw new IOException("Chainbase checkpoint Store is not registered: " + + store.getDbName()); + } + Snapshot root = database.getHead().getRoot(); + if (!(root instanceof SnapshotRoot)) { + throw new IOException("Chainbase checkpoint Store has no SnapshotRoot: " + + store.getDbName()); + } + ((SnapshotRoot) root).applyCheckpointMutations(batch(store)); + faultHook.after(Stage.AFTER_STORE_BATCH, store.getDbName()); + } + publishImmutable(materializedPath(admittedTarget), encode(admittedTarget)); + faultHook.after(Stage.AFTER_MATERIALIZED_TARGET, null); + } + + @Override + public synchronized void publish(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = requireTarget(target); + Status status = inspect(admitted); + if (status == Status.PUBLISHED) { + return; + } + if (status != Status.MATERIALIZED) { + throw new IOException("Chainbase checkpoint target is not fully materialized"); + } + replace(directory.resolve(CURRENT_FILE), encode(admitted)); + faultHook.after(Stage.AFTER_CURRENT, null); + } + + private CommonCheckpointTarget requireTarget(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + if (!Arrays.equals(formatIdentity, admitted.getFormatIdentity())) { + throw new IOException("Chainbase checkpoint format identity differs"); + } + return admitted; + } + + private void requireParent(Marker current, CommonCheckpointTarget target) throws IOException { + BlockSnapshotMeta first = target.getFirstBlock(); + if (!Arrays.equals(current.formatIdentity, target.getFormatIdentity()) + || current.lastEpoch + 1 != first.getEpoch() + || current.lastBlockNumber + 1 != first.getBlockNumber() + || !Arrays.equals(current.lastBlockHash, first.getParentHash()) + || !Arrays.equals(current.stateRoot, target.getParentStateRoot())) { + throw new IOException("Chainbase CURRENT is not the checkpoint parent target"); + } + } + + private Path materializedPath(CommonCheckpointTarget target) { + return directory.resolve(MATERIALIZED_DIRECTORY).resolve(hex(target.getPayloadDigest())); + } + + private static Map batch( + CommonCheckpointPayload.StoreMutations store) { + Map batch = new LinkedHashMap<>(); + for (CommonCheckpointPayload.Mutation mutation : store.getMutations()) { + batch.put(WrappedByteArray.of(mutation.getKey()), + WrappedByteArray.of(mutation.getValue())); + } + return batch; + } + + private static Map index(List supplied) { + Map indexed = new LinkedHashMap<>(); + for (Chainbase database : Objects.requireNonNull(supplied, "databases")) { + Chainbase admitted = Objects.requireNonNull(database, "database"); + if (indexed.putIfAbsent(admitted.getDbName(), admitted) != null) { + throw new IllegalArgumentException("duplicate Chainbase checkpoint Store: " + + admitted.getDbName()); + } + } + return indexed; + } + + private static byte[] encode(CommonCheckpointTarget target) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(RECORD_LENGTH); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.write(target.getFormatIdentity()); + output.write(target.getPayloadDigest()); + output.writeLong(target.getLastBlock().getEpoch()); + output.writeLong(target.getLastBlock().getBlockNumber()); + output.write(target.getLastBlock().getBlockHash()); + output.write(target.getStateRoot()); + output.flush(); + byte[] body = bytes.toByteArray(); + output.write(Hashing.sha256().hashBytes(body).asBytes()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory Chainbase target encoding failed", impossible); + } + } + + private static Marker load(Path path) throws IOException { + byte[] encoded = readBounded(path, RECORD_LENGTH); + if (encoded.length != RECORD_LENGTH) { + throw new IOException("Chainbase checkpoint target length is invalid"); + } + int bodyLength = encoded.length - DIGEST_LENGTH; + byte[] body = Arrays.copyOf(encoded, bodyLength); + byte[] checksum = Arrays.copyOfRange(encoded, bodyLength, encoded.length); + if (!Arrays.equals(checksum, Hashing.sha256().hashBytes(body).asBytes())) { + throw new IOException("Chainbase checkpoint target checksum differs"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(body))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0) { + throw new IOException("Chainbase checkpoint target format is unsupported"); + } + return new Marker(encoded, readDigest(input), readDigest(input), input.readLong(), + input.readLong(), readDigest(input), readDigest(input)); + } catch (EOFException truncated) { + throw new IOException("Chainbase checkpoint target is truncated", truncated); + } + } + + private static void requireExact(Path path, byte[] expected) throws IOException { + if (!Arrays.equals(load(path).encoded, expected)) { + throw new IOException("Chainbase checkpoint target identity differs"); + } + } + + private static void publishImmutable(Path path, byte[] bytes) throws IOException { + createDirectory(path.getParent()); + if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + if (!Arrays.equals(readBounded(path, bytes.length), bytes)) { + throw new IOException("Chainbase immutable checkpoint target differs"); + } + return; + } + Path temporary = path.resolveSibling(path.getFileName() + ".tmp-" + UUID.randomUUID()); + try { + writeForced(temporary, bytes); + try { + Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("Chainbase filesystem lacks atomic target publication", + unsupported); + } catch (java.nio.file.FileAlreadyExistsException raced) { + if (!Arrays.equals(readBounded(path, bytes.length), bytes)) { + throw new IOException("Chainbase immutable target publication raced", raced); + } + } + syncDirectory(path.getParent()); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static void replace(Path path, byte[] bytes) throws IOException { + createDirectory(path.getParent()); + Path temporary = path.resolveSibling(path.getFileName() + ".tmp-" + UUID.randomUUID()); + try { + writeForced(temporary, bytes); + try { + Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("Chainbase filesystem lacks atomic CURRENT replacement", + unsupported); + } + syncDirectory(path.getParent()); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static void writeForced(Path path, byte[] bytes) throws IOException { + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + } + + private static byte[] readBounded(Path path, long maximum) throws IOException { + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Chainbase checkpoint path is not a regular file"); + } + long size = Files.size(path); + if (size <= 0 || size > maximum) { + throw new IOException("Chainbase checkpoint target length is invalid"); + } + return Files.readAllBytes(path); + } + + private static void createDirectory(Path path) throws IOException { + Files.createDirectories(path); + if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Chainbase checkpoint path is not a directory"); + } + } + + private static void syncDirectory(Path path) throws IOException { + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { + channel.force(true); + } + } + + private static byte[] readDigest(DataInputStream input) throws IOException { + byte[] value = new byte[DIGEST_LENGTH]; + input.readFully(value); + return value; + } + + private static byte[] digest(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length != DIGEST_LENGTH) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return copy; + } + + private static String hex(byte[] value) { + StringBuilder encoded = new StringBuilder(value.length * 2); + for (byte current : value) { + encoded.append(Character.forDigit(current >>> 4 & 0xf, 16)); + encoded.append(Character.forDigit(current & 0xf, 16)); + } + return encoded.toString(); + } + + enum Stage { + AFTER_STORE_BATCH, + AFTER_MATERIALIZED_TARGET, + AFTER_CURRENT + } + + @FunctionalInterface + interface FaultHook { + void after(Stage stage, String dbName) throws IOException; + } + + private static final class Marker { + + private final byte[] encoded; + private final byte[] formatIdentity; + private final byte[] payloadDigest; + private final long lastEpoch; + private final long lastBlockNumber; + private final byte[] lastBlockHash; + private final byte[] stateRoot; + + private Marker(byte[] encoded, byte[] formatIdentity, byte[] payloadDigest, long lastEpoch, + long lastBlockNumber, byte[] lastBlockHash, byte[] stateRoot) { + this.encoded = encoded; + this.formatIdentity = formatIdentity; + this.payloadDigest = payloadDigest; + this.lastEpoch = lastEpoch; + this.lastBlockNumber = lastBlockNumber; + this.lastBlockHash = lastBlockHash; + this.stateRoot = stateRoot; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFile.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFile.java new file mode 100644 index 00000000000..5afe0b24e82 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFile.java @@ -0,0 +1,162 @@ +package org.tron.core.db2.core; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Objects; + +/** Standalone durable file lifecycle for one immutable common-checkpoint redo payload. */ +public final class CommonCheckpointFile { + + static final String FILE_NAME = "COMMON_CHECKPOINT"; + static final String TEMPORARY_FILE_NAME = ".COMMON_CHECKPOINT.tmp"; + + private final Path directory; + private final Path checkpoint; + private final Path temporary; + private final int maxEncodedLength; + private final CommonCheckpointPayloadCodec codec; + private final FaultHook faultHook; + + public CommonCheckpointFile(Path directory) { + this(directory, CommonCheckpointPayloadCodec.DEFAULT_MAX_ENCODED_LENGTH, + (stage, path) -> { }); + } + + CommonCheckpointFile(Path directory, int maxEncodedLength, FaultHook faultHook) { + this.directory = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); + this.maxEncodedLength = maxEncodedLength; + this.codec = new CommonCheckpointPayloadCodec(maxEncodedLength); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + this.checkpoint = this.directory.resolve(FILE_NAME); + this.temporary = this.directory.resolve(TEMPORARY_FILE_NAME); + } + + /** Publishes once; an exact existing payload is an idempotent retry and seals its directory. */ + public synchronized void publish(CommonCheckpointPayload payload) throws IOException { + byte[] encoded = codec.encode(Objects.requireNonNull(payload, "payload")); + requireDirectory(); + if (Files.exists(checkpoint, LinkOption.NOFOLLOW_LINKS)) { + requireExact(encoded); + syncDirectory(); + return; + } + if (Files.deleteIfExists(temporary)) { + syncDirectory(); + } + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + writeFully(channel, ByteBuffer.wrap(encoded)); + channel.force(true); + } + faultHook.after(Stage.AFTER_TEMPORARY_FORCE, temporary); + try { + Files.move(temporary, checkpoint, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("common checkpoint requires atomic publication", unsupported); + } + faultHook.after(Stage.AFTER_ATOMIC_PUBLISH, checkpoint); + syncDirectory(); + faultHook.after(Stage.AFTER_DIRECTORY_FORCE, checkpoint); + } + + public synchronized CommonCheckpointPayload loadRequired() throws IOException { + if (!Files.isRegularFile(checkpoint, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("common checkpoint is missing or not a regular file"); + } + long length = Files.size(checkpoint); + if (length < CommonCheckpointPayloadCodec.HEADER_LENGTH || length > maxEncodedLength) { + throw new IOException("common checkpoint file length is invalid"); + } + try { + return codec.decode(Files.readAllBytes(checkpoint)); + } catch (IllegalArgumentException invalid) { + throw new IOException("common checkpoint file is corrupt", invalid); + } + } + + public synchronized CommonCheckpointPayload loadIfPresent() throws IOException { + if (!Files.exists(checkpoint, LinkOption.NOFOLLOW_LINKS)) { + return null; + } + return loadRequired(); + } + + /** Retires only this checkpoint and its non-authoritative temporary file. */ + public synchronized void retire() throws IOException { + requireDirectory(); + boolean changed = Files.deleteIfExists(checkpoint); + changed |= Files.deleteIfExists(temporary); + if (changed) { + faultHook.after(Stage.AFTER_RETIRE_DELETE, checkpoint); + } + syncDirectory(); + faultHook.after(Stage.AFTER_RETIRE_DIRECTORY_FORCE, directory); + } + + Path getCheckpointPath() { + return checkpoint; + } + + Path getTemporaryPath() { + return temporary; + } + + private void requireExact(byte[] expected) throws IOException { + if (!Files.isRegularFile(checkpoint, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("common checkpoint is not a regular file"); + } + long length = Files.size(checkpoint); + if (length != expected.length || length > maxEncodedLength + || !Arrays.equals(expected, Files.readAllBytes(checkpoint))) { + throw new IOException("immutable common checkpoint identity mismatch"); + } + try { + codec.decode(expected); + } catch (IllegalArgumentException invalid) { + throw new IOException("common checkpoint file is corrupt", invalid); + } + } + + private void requireDirectory() throws IOException { + if (Files.isSymbolicLink(directory)) { + throw new IOException("common checkpoint directory must not be a symbolic link"); + } + Files.createDirectories(directory); + if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("common checkpoint parent is not a direct directory"); + } + } + + private void syncDirectory() throws IOException { + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } + + enum Stage { + AFTER_TEMPORARY_FORCE, + AFTER_ATOMIC_PUBLISH, + AFTER_DIRECTORY_FORCE, + AFTER_RETIRE_DELETE, + AFTER_RETIRE_DIRECTORY_FORCE + } + + @FunctionalInterface + interface FaultHook { + void after(Stage stage, Path path) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java new file mode 100644 index 00000000000..d3c63886305 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java @@ -0,0 +1,34 @@ +package org.tron.core.db2.core; + +import java.io.IOException; + +/** One idempotent authority participant in common-checkpoint redo and publication. */ +public interface CommonCheckpointMaterializer { + + Authority authority(); + + /** + * Returns only an exact state for {@code target}. Implementations must throw when durable state + * is corrupt, ambiguous, or belongs to a different target. + */ + Status inspect(CommonCheckpointTarget target) throws IOException; + + /** Idempotently writes and forces this authority's data without publishing its public marker. */ + void materialize(CommonCheckpointPayload payload, CommonCheckpointTarget target) + throws IOException; + + /** Idempotently publishes this authority's already-materialized exact target. */ + void publish(CommonCheckpointTarget target) throws IOException; + + enum Authority { + CHAINBASE, + PATH_STATE, + STATE_ARCHIVE + } + + enum Status { + NEEDS_MATERIALIZATION, + MATERIALIZED, + PUBLISHED + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java new file mode 100644 index 00000000000..79133112479 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java @@ -0,0 +1,337 @@ +package org.tron.core.db2.core; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.stateroot.PathStateFlushTarget; +import org.tron.core.db2.stateroot.PathStateSnapshotDelta; + +/** Complete immutable redo input for one future cross-authority checkpoint. */ +public final class CommonCheckpointPayload { + + public static final int FORMAT_VERSION = 1; + private static final int DIGEST_LENGTH = 32; + private static final Comparator MUTATION_ORDER = + (left, right) -> compareUnsigned(left.key, right.key); + + private final byte[] formatIdentity; + private final List blocks; + private final byte[] parentStateRoot; + private final byte[] stateRoot; + private final List chainbaseStores; + private final List pathStores; + private final List superNodeMutations; + + private CommonCheckpointPayload(byte[] formatIdentity, List blocks, + byte[] parentStateRoot, byte[] stateRoot, List chainbaseStores, + List pathStores, List superNodeMutations) { + this.formatIdentity = digest(formatIdentity, "formatIdentity"); + if (blocks.isEmpty()) { + throw new IllegalArgumentException("common checkpoint must contain at least one block"); + } + this.parentStateRoot = digest(parentStateRoot, "parentStateRoot"); + this.stateRoot = digest(stateRoot, "stateRoot"); + List admittedBlocks = new ArrayList<>(blocks); + validateBlocks(admittedBlocks, this.parentStateRoot, this.stateRoot); + this.blocks = Collections.unmodifiableList(admittedBlocks); + this.chainbaseStores = immutableStores(chainbaseStores); + this.pathStores = immutablePathStores(pathStores); + this.superNodeMutations = immutableMutations(superNodeMutations); + } + + public static CommonCheckpointPayload create(byte[] formatIdentity, + PathStateFlushTarget pathState, List archiveBlocks, + List chainbaseStores) { + PathStateFlushTarget path = Objects.requireNonNull(pathState, "pathState"); + List archives = new ArrayList<>(Objects.requireNonNull(archiveBlocks, + "archiveBlocks")); + if (path.getBlocks().size() != archives.size()) { + throw new IllegalArgumentException("common checkpoint block payload count differs"); + } + List blocks = new ArrayList<>(); + for (int index = 0; index < archives.size(); index++) { + PathStateFlushTarget.BlockBinding binding = path.getBlocks().get(index); + BlockReverseDiff archive = Objects.requireNonNull(archives.get(index), "archiveBlock"); + if (!binding.getMeta().equals(archive.getMeta()) + || archive.getMutationViewDigest() == null + || !Arrays.equals(binding.getMutationViewDigest(), + archive.getMutationViewDigest())) { + throw new IllegalArgumentException( + "common checkpoint Archive and PathState block identity differs"); + } + blocks.add(new BlockPayload(binding.getMeta(), binding.getParentStateRoot(), + binding.getStateRoot(), binding.getTransitionPayloadDigest(), + binding.getMutationViewDigest(), archive)); + } + List pathStores = new ArrayList<>(); + for (PathStateFlushTarget.StoreTarget store : path.getStores()) { + pathStores.add(new PathStoreTarget(store.getStoreId(), store.getDbName(), + store.getStoreRoot(), mutations(store.getFlatMutations()), + mutations(store.getNodeMutations()))); + } + return new CommonCheckpointPayload(formatIdentity, blocks, path.getParentStateRoot(), + path.getStateRoot(), chainbaseStores, pathStores, + mutations(path.getSuperNodeMutations())); + } + + static CommonCheckpointPayload restore(byte[] formatIdentity, List blocks, + byte[] parentStateRoot, byte[] stateRoot, List chainbaseStores, + List pathStores, List superNodeMutations) { + return new CommonCheckpointPayload(formatIdentity, blocks, parentStateRoot, stateRoot, + chainbaseStores, pathStores, superNodeMutations); + } + + public byte[] getFormatIdentity() { + return copy(formatIdentity); + } + + public List getBlocks() { + return blocks; + } + + public byte[] getParentStateRoot() { + return copy(parentStateRoot); + } + + public byte[] getStateRoot() { + return copy(stateRoot); + } + + public List getChainbaseStores() { + return chainbaseStores; + } + + public List getPathStores() { + return pathStores; + } + + public List getSuperNodeMutations() { + return superNodeMutations; + } + + private static List immutableStores(List supplied) { + List stores = new ArrayList<>(Objects.requireNonNull(supplied, + "chainbaseStores")); + stores.sort(Comparator.comparing(StoreMutations::getDbName)); + requireUniqueStoreNames(stores); + return Collections.unmodifiableList(stores); + } + + private static void validateBlocks(List blocks, byte[] parentStateRoot, + byte[] stateRoot) { + BlockPayload previous = null; + for (BlockPayload block : blocks) { + BlockPayload current = Objects.requireNonNull(block, "block"); + byte[] archiveView = current.archiveDiff.getMutationViewDigest(); + if (archiveView == null || !Arrays.equals(archiveView, current.mutationViewDigest)) { + throw new IllegalArgumentException("checkpoint block mutation-view identity differs"); + } + if (previous != null + && (current.meta.getEpoch() != previous.meta.getEpoch() + 1 + || current.meta.getBlockNumber() != previous.meta.getBlockNumber() + 1 + || !Arrays.equals(current.meta.getParentHash(), previous.meta.getBlockHash()) + || !Arrays.equals(current.parentStateRoot, previous.stateRoot))) { + throw new IllegalArgumentException("common checkpoint block chain is not consecutive"); + } + previous = current; + } + if (!Arrays.equals(blocks.get(0).parentStateRoot, parentStateRoot) + || !Arrays.equals(blocks.get(blocks.size() - 1).stateRoot, stateRoot)) { + throw new IllegalArgumentException("common checkpoint target root range differs"); + } + } + + private static List immutablePathStores(List supplied) { + List stores = new ArrayList<>(Objects.requireNonNull(supplied, + "pathStores")); + stores.sort(Comparator.comparingInt(PathStoreTarget::getStoreId)); + for (int index = 1; index < stores.size(); index++) { + if (stores.get(index - 1).storeId == stores.get(index).storeId) { + throw new IllegalArgumentException("duplicate checkpoint path-state Store ID"); + } + } + return Collections.unmodifiableList(stores); + } + + private static void requireUniqueStoreNames(List stores) { + for (int index = 1; index < stores.size(); index++) { + if (stores.get(index - 1).dbName.equals(stores.get(index).dbName)) { + throw new IllegalArgumentException("duplicate checkpoint Chainbase Store"); + } + } + } + + private static List mutations(List supplied) { + List result = new ArrayList<>(); + for (PathStateSnapshotDelta.Mutation mutation : supplied) { + result.add(new Mutation(mutation.getKey(), mutation.getValue())); + } + return result; + } + + private static List immutableMutations(List supplied) { + List mutations = new ArrayList<>(Objects.requireNonNull(supplied, "mutations")); + mutations.sort(MUTATION_ORDER); + for (int index = 1; index < mutations.size(); index++) { + if (Arrays.equals(mutations.get(index - 1).key, mutations.get(index).key)) { + throw new IllegalArgumentException("duplicate checkpoint mutation key"); + } + } + return Collections.unmodifiableList(mutations); + } + + private static byte[] digest(byte[] value, String name) { + byte[] copy = copy(Objects.requireNonNull(value, name)); + if (copy.length != DIGEST_LENGTH) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return copy; + } + + private static byte[] copy(byte[] value) { + return Arrays.copyOf(value, value.length); + } + + private static int compareUnsigned(byte[] left, byte[] right) { + for (int index = 0; index < Math.min(left.length, right.length); index++) { + int compared = Integer.compare(left[index] & 0xff, right[index] & 0xff); + if (compared != 0) { + return compared; + } + } + return Integer.compare(left.length, right.length); + } + + public static final class BlockPayload { + + private final BlockSnapshotMeta meta; + private final byte[] parentStateRoot; + private final byte[] stateRoot; + private final byte[] transitionPayloadDigest; + private final byte[] mutationViewDigest; + private final BlockReverseDiff archiveDiff; + + BlockPayload(BlockSnapshotMeta meta, byte[] parentStateRoot, byte[] stateRoot, + byte[] transitionPayloadDigest, byte[] mutationViewDigest, + BlockReverseDiff archiveDiff) { + this.meta = Objects.requireNonNull(meta, "meta"); + this.parentStateRoot = digest(parentStateRoot, "parentStateRoot"); + this.stateRoot = digest(stateRoot, "stateRoot"); + this.transitionPayloadDigest = digest(transitionPayloadDigest, + "transitionPayloadDigest"); + this.mutationViewDigest = digest(mutationViewDigest, "mutationViewDigest"); + this.archiveDiff = Objects.requireNonNull(archiveDiff, "archiveDiff"); + if (!meta.equals(archiveDiff.getMeta())) { + throw new IllegalArgumentException("checkpoint Archive block metadata differs"); + } + } + + public BlockSnapshotMeta getMeta() { + return meta; + } + + public byte[] getParentStateRoot() { + return copy(parentStateRoot); + } + + public byte[] getStateRoot() { + return copy(stateRoot); + } + + public byte[] getTransitionPayloadDigest() { + return copy(transitionPayloadDigest); + } + + public byte[] getMutationViewDigest() { + return copy(mutationViewDigest); + } + + public BlockReverseDiff getArchiveDiff() { + return archiveDiff; + } + } + + public static class StoreMutations { + + private final String dbName; + private final List mutations; + + public StoreMutations(String dbName, List mutations) { + this.dbName = Objects.requireNonNull(dbName, "dbName"); + if (dbName.isEmpty()) { + throw new IllegalArgumentException("checkpoint dbName must not be empty"); + } + this.mutations = immutableMutations(mutations); + } + + public String getDbName() { + return dbName; + } + + public List getMutations() { + return mutations; + } + } + + public static final class PathStoreTarget extends StoreMutations { + + private final int storeId; + private final byte[] storeRoot; + private final List nodeMutations; + + PathStoreTarget(int storeId, String dbName, byte[] storeRoot, + List flatMutations, List nodeMutations) { + super(dbName, flatMutations); + if (storeId <= 0) { + throw new IllegalArgumentException("checkpoint path-state Store ID must be positive"); + } + this.storeId = storeId; + this.storeRoot = digest(storeRoot, "storeRoot"); + this.nodeMutations = immutableMutations(nodeMutations); + } + + public int getStoreId() { + return storeId; + } + + public byte[] getStoreRoot() { + return copy(storeRoot); + } + + public List getFlatMutations() { + return getMutations(); + } + + public List getNodeMutations() { + return nodeMutations; + } + } + + public static final class Mutation { + + private final byte[] key; + private final byte[] value; + + public Mutation(byte[] key, byte[] value) { + this.key = copy(Objects.requireNonNull(key, "key")); + this.value = value == null ? null : copy(value); + } + + public byte[] getKey() { + return copy(key); + } + + public byte[] getValue() { + return value == null ? null : copy(value); + } + + public boolean isDelete() { + return value == null; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java new file mode 100644 index 00000000000..91b8f595c03 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java @@ -0,0 +1,286 @@ +package org.tron.core.db2.core; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.tron.core.db2.archive.BlockHistoryCodec; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.core.CommonCheckpointPayload.BlockPayload; +import org.tron.core.db2.core.CommonCheckpointPayload.Mutation; +import org.tron.core.db2.core.CommonCheckpointPayload.PathStoreTarget; +import org.tron.core.db2.core.CommonCheckpointPayload.StoreMutations; + +/** Deterministic, bounded and checksummed codec for a complete common-checkpoint redo payload. */ +public final class CommonCheckpointPayloadCodec { + + public static final int MAGIC = 0x54434350; // TCCP + public static final short VERSION = 1; + public static final int HEADER_LENGTH = 44; + public static final int DEFAULT_MAX_ENCODED_LENGTH = 256 * 1024 * 1024; + private static final int DIGEST_LENGTH = 32; + private static final int MAX_BLOCKS = 100_000; + private static final int MAX_STORES = 1024; + private static final int MAX_MUTATIONS = 1_000_000; + private static final int MAX_NAME_LENGTH = 256; + private static final int MAX_FIELD_LENGTH = 64 * 1024 * 1024; + + private final int maxEncodedLength; + private final BlockHistoryCodec historyCodec = new BlockHistoryCodec(); + + public CommonCheckpointPayloadCodec() { + this(DEFAULT_MAX_ENCODED_LENGTH); + } + + public CommonCheckpointPayloadCodec(int maxEncodedLength) { + if (maxEncodedLength <= HEADER_LENGTH) { + throw new IllegalArgumentException("common checkpoint maximum length is too small"); + } + this.maxEncodedLength = maxEncodedLength; + } + + public byte[] encode(CommonCheckpointPayload payload) { + try { + byte[] body = encodeBody(payload); + checkEncodedLength(HEADER_LENGTH + (long) body.length); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(HEADER_LENGTH + body.length); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.writeInt(body.length); + output.write(Hashing.sha256().hashBytes(body).asBytes()); + output.write(body); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory common checkpoint encoding failed", impossible); + } + } + + public CommonCheckpointPayload decode(byte[] encoded) { + if (encoded == null || encoded.length < HEADER_LENGTH) { + throw new IllegalArgumentException("common checkpoint payload is truncated"); + } + checkEncodedLength(encoded.length); + try { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); + if (input.readInt() != MAGIC) { + throw new IllegalArgumentException("invalid common checkpoint magic"); + } + if (input.readShort() != VERSION) { + throw new IllegalArgumentException("unsupported common checkpoint version"); + } + if (input.readShort() != 0) { + throw new IllegalArgumentException("unsupported common checkpoint flags"); + } + int bodyLength = input.readInt(); + byte[] expectedDigest = readExact(input, DIGEST_LENGTH); + if (bodyLength < 0 || HEADER_LENGTH + (long) bodyLength != encoded.length) { + throw new IllegalArgumentException("invalid common checkpoint body length"); + } + byte[] body = readExact(input, bodyLength); + if (!Arrays.equals(expectedDigest, Hashing.sha256().hashBytes(body).asBytes())) { + throw new IllegalArgumentException("common checkpoint payload checksum mismatch"); + } + return decodeBody(body); + } catch (EOFException truncated) { + throw new IllegalArgumentException("common checkpoint payload is truncated", truncated); + } catch (IOException invalid) { + throw new IllegalArgumentException("invalid common checkpoint payload", invalid); + } + } + + public byte[] digest(CommonCheckpointPayload payload) { + return Hashing.sha256().hashBytes(encode(payload)).asBytes(); + } + + private byte[] encodeBody(CommonCheckpointPayload payload) throws IOException { + CommonCheckpointPayload admitted = java.util.Objects.requireNonNull(payload, "payload"); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.write(admitted.getFormatIdentity()); + output.write(admitted.getParentStateRoot()); + output.write(admitted.getStateRoot()); + output.writeInt(admitted.getBlocks().size()); + for (BlockPayload block : admitted.getBlocks()) { + writeMeta(output, block.getMeta()); + output.write(block.getParentStateRoot()); + output.write(block.getStateRoot()); + output.write(block.getTransitionPayloadDigest()); + output.write(block.getMutationViewDigest()); + writeBytes(output, historyCodec.encode(block.getArchiveDiff())); + } + writeStores(output, admitted.getChainbaseStores()); + output.writeInt(admitted.getPathStores().size()); + for (PathStoreTarget store : admitted.getPathStores()) { + output.writeInt(store.getStoreId()); + writeName(output, store.getDbName()); + output.write(store.getStoreRoot()); + writeMutations(output, store.getFlatMutations()); + writeMutations(output, store.getNodeMutations()); + } + writeMutations(output, admitted.getSuperNodeMutations()); + output.flush(); + return bytes.toByteArray(); + } + + private CommonCheckpointPayload decodeBody(byte[] body) throws IOException { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(body)); + byte[] formatIdentity = readExact(input, DIGEST_LENGTH); + byte[] parentStateRoot = readExact(input, DIGEST_LENGTH); + byte[] stateRoot = readExact(input, DIGEST_LENGTH); + int blockCount = readCount(input, MAX_BLOCKS, "block"); + List blocks = new ArrayList<>(blockCount); + for (int index = 0; index < blockCount; index++) { + BlockSnapshotMeta meta = readMeta(input); + byte[] parentRoot = readExact(input, DIGEST_LENGTH); + byte[] blockRoot = readExact(input, DIGEST_LENGTH); + byte[] transitionDigest = readExact(input, DIGEST_LENGTH); + byte[] viewDigest = readExact(input, DIGEST_LENGTH); + BlockReverseDiff decoded = historyCodec.decode(readBytes(input)); + BlockReverseDiff archive = new BlockReverseDiff(decoded.getMeta(), decoded.getGroups(), + viewDigest); + blocks.add(new BlockPayload(meta, parentRoot, blockRoot, transitionDigest, viewDigest, + archive)); + } + List chainbase = readStores(input); + int pathStoreCount = readCount(input, MAX_STORES, "path-state Store"); + List pathStores = new ArrayList<>(pathStoreCount); + for (int index = 0; index < pathStoreCount; index++) { + int storeId = input.readInt(); + String dbName = readName(input); + byte[] storeRoot = readExact(input, DIGEST_LENGTH); + pathStores.add(new PathStoreTarget(storeId, dbName, storeRoot, + readMutations(input), readMutations(input))); + } + List superNodes = readMutations(input); + if (input.available() != 0) { + throw new IllegalArgumentException("common checkpoint payload has trailing bytes"); + } + return CommonCheckpointPayload.restore(formatIdentity, blocks, parentStateRoot, stateRoot, + chainbase, pathStores, superNodes); + } + + private void writeStores(DataOutputStream output, List stores) + throws IOException { + output.writeInt(stores.size()); + for (StoreMutations store : stores) { + writeName(output, store.getDbName()); + writeMutations(output, store.getMutations()); + } + } + + private List readStores(DataInputStream input) throws IOException { + int count = readCount(input, MAX_STORES, "Chainbase Store"); + List stores = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + stores.add(new StoreMutations(readName(input), readMutations(input))); + } + return stores; + } + + private void writeMutations(DataOutputStream output, List mutations) + throws IOException { + output.writeInt(mutations.size()); + for (Mutation mutation : mutations) { + writeBytes(output, mutation.getKey()); + byte[] value = mutation.getValue(); + output.writeInt(value == null ? -1 : value.length); + if (value != null) { + output.write(value); + } + } + } + + private List readMutations(DataInputStream input) throws IOException { + int count = readCount(input, MAX_MUTATIONS, "mutation"); + List mutations = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + byte[] key = readBytes(input); + int valueLength = input.readInt(); + if (valueLength < -1 || valueLength > MAX_FIELD_LENGTH) { + throw new IllegalArgumentException("invalid checkpoint mutation value length"); + } + mutations.add(new Mutation(key, + valueLength < 0 ? null : readExact(input, valueLength))); + } + return mutations; + } + + private static void writeMeta(DataOutputStream output, BlockSnapshotMeta meta) + throws IOException { + output.writeLong(meta.getEpoch()); + output.writeLong(meta.getBlockNumber()); + output.write(meta.getBlockHash()); + output.write(meta.getParentHash()); + output.writeLong(meta.getTimestamp()); + } + + private static BlockSnapshotMeta readMeta(DataInputStream input) throws IOException { + return new BlockSnapshotMeta(input.readLong(), input.readLong(), + readExact(input, DIGEST_LENGTH), readExact(input, DIGEST_LENGTH), input.readLong()); + } + + private static void writeName(DataOutputStream output, String value) throws IOException { + byte[] encoded = value.getBytes(StandardCharsets.UTF_8); + if (encoded.length == 0 || encoded.length > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("invalid common checkpoint Store name length"); + } + output.writeInt(encoded.length); + output.write(encoded); + } + + private static String readName(DataInputStream input) throws IOException { + int length = input.readInt(); + if (length <= 0 || length > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("invalid common checkpoint Store name length"); + } + return new String(readExact(input, length), StandardCharsets.UTF_8); + } + + private static void writeBytes(DataOutputStream output, byte[] value) throws IOException { + if (value.length > MAX_FIELD_LENGTH) { + throw new IllegalArgumentException("common checkpoint field exceeds maximum length"); + } + output.writeInt(value.length); + output.write(value); + } + + private static byte[] readBytes(DataInputStream input) throws IOException { + int length = input.readInt(); + if (length < 0 || length > MAX_FIELD_LENGTH) { + throw new IllegalArgumentException("invalid common checkpoint field length"); + } + return readExact(input, length); + } + + private static int readCount(DataInputStream input, int maximum, String name) + throws IOException { + int count = input.readInt(); + if (count < 0 || count > maximum) { + throw new IllegalArgumentException("invalid common checkpoint " + name + " count"); + } + return count; + } + + private static byte[] readExact(DataInputStream input, int length) throws IOException { + byte[] value = new byte[length]; + input.readFully(value); + return value; + } + + private void checkEncodedLength(long length) { + if (length > maxEncodedLength) { + throw new IllegalArgumentException("common checkpoint exceeds maximum encoded length"); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java new file mode 100644 index 00000000000..750a35b521b --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java @@ -0,0 +1,151 @@ +package org.tron.core.db2.core; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.tron.core.db2.archive.ArchiveStoreScope; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.common.Key; +import org.tron.core.db2.common.Value; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.stateroot.PathStateFlushTarget; +import org.tron.core.db2.stateroot.PathStateSnapshotDelta; + +/** Builds one immutable common-checkpoint redo payload without querying durable databases. */ +public final class CommonCheckpointPayloadFactory { + + /** Captures the oldest {@code flushCount} Snapshot layers from every registered Store. */ + public CommonCheckpointPayload capture(byte[] formatIdentity, List databases, + int flushCount) { + Objects.requireNonNull(formatIdentity, "formatIdentity"); + if (flushCount <= 0) { + throw new IllegalArgumentException("common checkpoint flushCount must be positive"); + } + List admitted = new ArrayList<>(Objects.requireNonNull(databases, "databases")); + if (admitted.isEmpty()) { + throw new IllegalArgumentException("common checkpoint requires registered Stores"); + } + + Map> layersByStore = new LinkedHashMap<>(); + List expectedMetas = null; + for (Chainbase database : admitted) { + Chainbase candidate = Objects.requireNonNull(database, "database"); + List layers = layers(candidate, flushCount); + if (layersByStore.putIfAbsent(candidate.getDbName(), layers) != null) { + throw new IllegalArgumentException("duplicate common checkpoint Store: " + + candidate.getDbName()); + } + List metas = metas(layers, candidate.getDbName()); + if (expectedMetas == null) { + expectedMetas = metas; + } else if (!expectedMetas.equals(metas)) { + throw new IllegalStateException("common checkpoint block identities differ across Stores"); + } + } + + List archiveBlocks = new ArrayList<>(); + List pathDeltas = new ArrayList<>(); + boolean foundStateStore = false; + for (Map.Entry> entry : layersByStore.entrySet()) { + if (!ArchiveStoreScope.isStateDatabase(entry.getKey())) { + continue; + } + foundStateStore = true; + for (int index = 0; index < entry.getValue().size(); index++) { + SnapshotImpl layer = entry.getValue().get(index); + BlockReverseDiff archive = layer.getPreparedArchiveBlock(); + PathStateSnapshotDelta path = layer.getPreparedPathStateDelta(); + requireArtifacts(expectedMetas.get(index), archive, path, entry.getKey()); + if (archiveBlocks.size() == index) { + archiveBlocks.add(archive); + pathDeltas.add(path); + } else { + requireSameArtifacts(archiveBlocks.get(index), pathDeltas.get(index), archive, path, + entry.getKey()); + } + } + } + if (!foundStateStore) { + throw new IllegalStateException("common checkpoint has no state Store"); + } + + List stores = new ArrayList<>(); + for (Map.Entry> entry : layersByStore.entrySet()) { + if ("trans-cache".equals(entry.getKey())) { + continue; + } + Map coalesced = + new LinkedHashMap<>(); + for (SnapshotImpl layer : entry.getValue()) { + for (Map.Entry mutation : layer.getDb()) { + byte[] key = mutation.getKey().getBytes(); + coalesced.put(WrappedByteArray.of(key), new CommonCheckpointPayload.Mutation(key, + mutation.getValue().getBytes())); + } + } + if (!coalesced.isEmpty()) { + stores.add(new CommonCheckpointPayload.StoreMutations(entry.getKey(), + new ArrayList<>(coalesced.values()))); + } + } + return CommonCheckpointPayload.create(formatIdentity, + PathStateFlushTarget.coalesce(pathDeltas), archiveBlocks, stores); + } + + private static List layers(Chainbase database, int count) { + Snapshot next = database.getHead().getRoot(); + List layers = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + next = next.getNext(); + if (!(next instanceof SnapshotImpl)) { + throw new IllegalStateException("common checkpoint Store has too few Snapshot layers: " + + database.getDbName()); + } + layers.add((SnapshotImpl) next); + } + return layers; + } + + private static List metas(List layers, String dbName) { + List metas = new ArrayList<>(layers.size()); + for (SnapshotImpl layer : layers) { + BlockSnapshotMeta meta = layer.getBlockSnapshotMeta(); + if (meta == null) { + throw new IllegalStateException("common checkpoint Snapshot has no block identity: " + + dbName); + } + metas.add(meta); + } + return metas; + } + + private static void requireArtifacts(BlockSnapshotMeta meta, BlockReverseDiff archive, + PathStateSnapshotDelta path, String dbName) { + if (archive == null || path == null || !meta.equals(archive.getMeta()) + || !meta.equals(path.getMeta()) || archive.getMutationViewDigest() == null + || !Arrays.equals(archive.getMutationViewDigest(), path.getMutationViewDigest())) { + throw new IllegalStateException("common checkpoint Snapshot artifacts differ: " + dbName); + } + } + + private static void requireSameArtifacts(BlockReverseDiff expectedArchive, + PathStateSnapshotDelta expectedPath, BlockReverseDiff archive, + PathStateSnapshotDelta path, String dbName) { + if (!expectedArchive.getMeta().equals(archive.getMeta()) + || !Arrays.equals(expectedArchive.getMutationViewDigest(), + archive.getMutationViewDigest()) + || !expectedPath.getMeta().equals(path.getMeta()) + || !Arrays.equals(expectedPath.getParentStateRoot(), path.getParentStateRoot()) + || !Arrays.equals(expectedPath.getStateRoot(), path.getStateRoot()) + || !Arrays.equals(expectedPath.getTransitionPayloadDigest(), + path.getTransitionPayloadDigest()) + || !Arrays.equals(expectedPath.getMutationViewDigest(), path.getMutationViewDigest())) { + throw new IllegalStateException("common checkpoint artifacts differ across state Stores: " + + dbName); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java new file mode 100644 index 00000000000..f0ec6ec586b --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java @@ -0,0 +1,166 @@ +package org.tron.core.db2.core; + +import java.io.IOException; +import java.util.EnumMap; +import java.util.Map; +import java.util.Objects; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Status; + +/** Two-barrier, idempotent redo coordinator for one durable common checkpoint. */ +public final class CommonCheckpointRedoCoordinator { + + private static final Authority[] ORDER = { + Authority.CHAINBASE, Authority.PATH_STATE, Authority.STATE_ARCHIVE}; + + private final CommonCheckpointFile checkpointFile; + private final Map materializers; + private final FaultHook faultHook; + + public CommonCheckpointRedoCoordinator(CommonCheckpointFile checkpointFile, + CommonCheckpointMaterializer chainbase, CommonCheckpointMaterializer pathState, + CommonCheckpointMaterializer stateArchive) { + this(checkpointFile, chainbase, pathState, stateArchive, stage -> { }); + } + + CommonCheckpointRedoCoordinator(CommonCheckpointFile checkpointFile, + CommonCheckpointMaterializer chainbase, CommonCheckpointMaterializer pathState, + CommonCheckpointMaterializer stateArchive, FaultHook faultHook) { + this.checkpointFile = Objects.requireNonNull(checkpointFile, "checkpointFile"); + this.materializers = new EnumMap<>(Authority.class); + admit(Authority.CHAINBASE, chainbase); + admit(Authority.PATH_STATE, pathState); + admit(Authority.STATE_ARCHIVE, stateArchive); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + /** Durably publishes the redo payload before applying it to any authority. */ + public synchronized RecoveryAction apply(CommonCheckpointPayload payload) throws IOException { + checkpointFile.publish(Objects.requireNonNull(payload, "payload")); + return redo(checkpointFile.loadRequired()); + } + + /** Resumes the only durable checkpoint, or performs no work when none exists. */ + public synchronized RecoveryAction recover() throws IOException { + CommonCheckpointPayload payload = checkpointFile.loadIfPresent(); + return payload == null ? RecoveryAction.NO_CHECKPOINT : redo(payload); + } + + private RecoveryAction redo(CommonCheckpointPayload payload) throws IOException { + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + Map initial = inspectAll(target); + if (initial.containsValue(Status.PUBLISHED) + && initial.containsValue(Status.NEEDS_MATERIALIZATION)) { + throw new IOException("common checkpoint has published authority before materialization " + + "barrier"); + } + + for (Authority authority : ORDER) { + CommonCheckpointMaterializer materializer = materializers.get(authority); + if (initial.get(authority) == Status.NEEDS_MATERIALIZATION) { + materializer.materialize(payload, target); + requireStatus(authority, Status.MATERIALIZED, materializer.inspect(target), + "materialization"); + faultHook.after(materializeStage(authority)); + } + } + + Map materialized = inspectAll(target); + if (materialized.containsValue(Status.NEEDS_MATERIALIZATION)) { + throw new IOException("common checkpoint materialization barrier is incomplete"); + } + for (Authority authority : ORDER) { + CommonCheckpointMaterializer materializer = materializers.get(authority); + if (materialized.get(authority) == Status.MATERIALIZED) { + materializer.publish(target); + requireStatus(authority, Status.PUBLISHED, materializer.inspect(target), "publication"); + faultHook.after(publishStage(authority)); + } + } + + Map published = inspectAll(target); + for (Authority authority : ORDER) { + requireStatus(authority, Status.PUBLISHED, published.get(authority), "retirement"); + } + faultHook.after(Stage.BEFORE_CHECKPOINT_RETIRE); + checkpointFile.retire(); + faultHook.after(Stage.AFTER_CHECKPOINT_RETIRE); + return RecoveryAction.COMPLETED_REDO; + } + + private Map inspectAll(CommonCheckpointTarget target) throws IOException { + Map statuses = new EnumMap<>(Authority.class); + for (Authority authority : ORDER) { + Status status = materializers.get(authority).inspect(target); + if (status == null) { + throw new IOException("common checkpoint " + authority + " returned null status"); + } + statuses.put(authority, status); + } + return statuses; + } + + private void admit(Authority expected, CommonCheckpointMaterializer materializer) { + CommonCheckpointMaterializer admitted = Objects.requireNonNull(materializer, + expected + " materializer"); + if (admitted.authority() != expected || materializers.put(expected, admitted) != null) { + throw new IllegalArgumentException("common checkpoint materializer authority differs: " + + expected); + } + } + + private static void requireStatus(Authority authority, Status expected, Status actual, + String operation) throws IOException { + if (actual != expected) { + throw new IOException("common checkpoint " + authority + " " + operation + + " returned " + actual + " instead of " + expected); + } + } + + private static Stage materializeStage(Authority authority) { + switch (authority) { + case CHAINBASE: + return Stage.AFTER_CHAINBASE_MATERIALIZE; + case PATH_STATE: + return Stage.AFTER_PATH_STATE_MATERIALIZE; + case STATE_ARCHIVE: + return Stage.AFTER_ARCHIVE_MATERIALIZE; + default: + throw new IllegalArgumentException("unsupported checkpoint authority " + authority); + } + } + + private static Stage publishStage(Authority authority) { + switch (authority) { + case CHAINBASE: + return Stage.AFTER_CHAINBASE_PUBLISH; + case PATH_STATE: + return Stage.AFTER_PATH_STATE_PUBLISH; + case STATE_ARCHIVE: + return Stage.AFTER_ARCHIVE_PUBLISH; + default: + throw new IllegalArgumentException("unsupported checkpoint authority " + authority); + } + } + + public enum RecoveryAction { + NO_CHECKPOINT, + COMPLETED_REDO + } + + enum Stage { + AFTER_CHAINBASE_MATERIALIZE, + AFTER_PATH_STATE_MATERIALIZE, + AFTER_ARCHIVE_MATERIALIZE, + AFTER_CHAINBASE_PUBLISH, + AFTER_PATH_STATE_PUBLISH, + AFTER_ARCHIVE_PUBLISH, + BEFORE_CHECKPOINT_RETIRE, + AFTER_CHECKPOINT_RETIRE + } + + @FunctionalInterface + interface FaultHook { + void after(Stage stage) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java new file mode 100644 index 00000000000..b6888763618 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java @@ -0,0 +1,76 @@ +package org.tron.core.db2.core; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; + +/** Isolated composition boundary for the next-format common-checkpoint runtime. */ +public final class CommonCheckpointRuntime implements AutoCloseable { + + private final CommonCheckpointRuntimeOwner owner; + private final List databases; + private final Path archiveDirectory; + private final byte[] formatIdentity; + private final StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory; + private final CommonCheckpointPayloadFactory payloadFactory = new CommonCheckpointPayloadFactory(); + private final CommonCheckpointSnapshotRebaser rebaser = new CommonCheckpointSnapshotRebaser(); + + public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, + Path archiveDirectory, byte[] formatIdentity, + StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory) { + this.owner = Objects.requireNonNull(owner, "owner"); + this.databases = new ArrayList<>(Objects.requireNonNull(databases, "databases")); + if (this.databases.isEmpty() || this.databases.contains(null)) { + throw new IllegalArgumentException("common checkpoint runtime requires registered Stores"); + } + this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + this.formatIdentity = requireDigest(formatIdentity); + this.latestFactory = Objects.requireNonNull(latestFactory, "latestFactory"); + } + + /** Completes durable redo before this runtime admits checkpoint reads or new flushes. */ + public CommonCheckpointRedoCoordinator.RecoveryAction recoverBeforeServing() + throws IOException { + return owner.recoverBeforeServing(); + } + + /** + * Captures and applies the immutable Snapshot prefix, then rebases it without a second Store + * write. The caller must hold the SnapshotManager monitor for the whole call. + */ + public CommonCheckpointTarget checkpointAndRebase(int flushCount) throws IOException { + CommonCheckpointPayload payload = payloadFactory.capture(formatIdentity, databases, + flushCount); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + owner.apply(payload, () -> rebaser.rebase(databases, target, flushCount)); + return target; + } + + /** Pins one point-only historical request under the same publication gate. */ + public StateArchiveCheckpointReadSnapshot pinPoint(long targetBlock) throws IOException { + return StateArchiveCheckpointReadSnapshot.pin(targetBlock, owner, archiveDirectory, + formatIdentity, latestFactory); + } + + public CommonCheckpointRuntimeOwner.State getState() { + return owner.getState(); + } + + @Override + public void close() { + owner.close(); + } + + private static byte[] requireDigest(byte[] value) { + byte[] admitted = Arrays.copyOf(Objects.requireNonNull(value, "formatIdentity"), + value.length); + if (admitted.length != 32) { + throw new IllegalArgumentException("formatIdentity must contain exactly 32 bytes"); + } + return admitted; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachment.java new file mode 100644 index 00000000000..1162b69d8bf --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachment.java @@ -0,0 +1,101 @@ +package org.tron.core.db2.core; + +import java.io.IOException; +import java.util.Objects; +import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; + +/** Default-off lifecycle attachment that exclusively owns one next-format runtime. */ +public final class CommonCheckpointRuntimeAttachment implements AutoCloseable { + + private final CommonCheckpointRuntime runtime; + private State state; + + private CommonCheckpointRuntimeAttachment(CommonCheckpointRuntime runtime, State state) { + this.runtime = runtime; + this.state = state; + } + + /** + * Does not invoke the factory when disabled. Enabled construction returns only after startup + * redo succeeds; a failed startup closes the newly created runtime before propagating failure. + */ + public static CommonCheckpointRuntimeAttachment open(boolean enabled, RuntimeFactory factory) + throws IOException { + Objects.requireNonNull(factory, "factory"); + if (!enabled) { + return new CommonCheckpointRuntimeAttachment(null, State.DISABLED); + } + CommonCheckpointRuntime runtime = Objects.requireNonNull(factory.open(), + "common checkpoint runtime factory returned null"); + try { + runtime.recoverBeforeServing(); + return new CommonCheckpointRuntimeAttachment(runtime, State.READY); + } catch (IOException | RuntimeException failure) { + try { + runtime.close(); + } catch (RuntimeException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } + } + + /** Applies one immutable flush prefix; any protocol failure permanently closes admission. */ + public synchronized CommonCheckpointTarget checkpointAndRebase(int flushCount) + throws IOException { + requireReady(); + try { + return runtime.checkpointAndRebase(flushCount); + } catch (IOException | RuntimeException failure) { + state = State.FAILED; + throw failure; + } + } + + /** Pins one point-only query from a fully recovered and non-failed runtime. */ + public synchronized StateArchiveCheckpointReadSnapshot pinPoint(long targetBlock) + throws IOException { + requireReady(); + return runtime.pinPoint(targetBlock); + } + + public synchronized State getState() { + return state; + } + + public synchronized boolean isEnabled() { + return runtime != null; + } + + @Override + public synchronized void close() { + if (state == State.CLOSED) { + return; + } + try { + if (runtime != null) { + runtime.close(); + } + } finally { + state = State.CLOSED; + } + } + + private void requireReady() { + if (state != State.READY) { + throw new IllegalStateException("common checkpoint attachment is not ready: " + state); + } + } + + public enum State { + DISABLED, + READY, + FAILED, + CLOSED + } + + @FunctionalInterface + public interface RuntimeFactory { + CommonCheckpointRuntime open() throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java new file mode 100644 index 00000000000..b3f8349788a --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java @@ -0,0 +1,145 @@ +package org.tron.core.db2.core; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** Explicit lifecycle and read gate for the next-format common-checkpoint runtime. */ +public final class CommonCheckpointRuntimeOwner implements AutoCloseable { + + private final CommonCheckpointRedoCoordinator coordinator; + private final ReentrantReadWriteLock gate = new ReentrantReadWriteLock(true); + private volatile State state = State.NEW; + + public CommonCheckpointRuntimeOwner(CommonCheckpointRedoCoordinator coordinator) { + this.coordinator = Objects.requireNonNull(coordinator, "coordinator"); + } + + /** Completes any durable redo before allowing the first read lease. */ + public CommonCheckpointRedoCoordinator.RecoveryAction recoverBeforeServing() + throws IOException { + gate.writeLock().lock(); + try { + requireState(State.NEW, "common checkpoint startup recovery already attempted"); + state = State.RECOVERING; + try { + CommonCheckpointRedoCoordinator.RecoveryAction action = coordinator.recover(); + state = State.READY; + return action; + } catch (IOException | RuntimeException failure) { + state = State.FAILED; + throw failure; + } + } finally { + gate.writeLock().unlock(); + } + } + + /** Blocks all read leases while the durable payload and both barriers are in progress. */ + public CommonCheckpointRedoCoordinator.RecoveryAction apply(CommonCheckpointPayload payload) + throws IOException { + return apply(payload, () -> { }); + } + + /** Keeps the write gate through publication and its required in-memory completion action. */ + CommonCheckpointRedoCoordinator.RecoveryAction apply(CommonCheckpointPayload payload, + CompletionAction completion) throws IOException { + gate.writeLock().lock(); + try { + requireState(State.READY, "common checkpoint runtime is not ready to flush"); + state = State.CHECKPOINTING; + try { + CommonCheckpointRedoCoordinator.RecoveryAction action = coordinator.apply( + Objects.requireNonNull(payload, "payload")); + Objects.requireNonNull(completion, "completion").run(); + state = State.READY; + return action; + } catch (IOException | RuntimeException failure) { + state = State.FAILED; + throw failure; + } + } finally { + gate.writeLock().unlock(); + } + } + + /** Runs one query only while no startup redo or checkpoint publication can interleave. */ + public T read(ReadableOperation operation) throws IOException { + try (ReadLease ignored = acquireReadLease()) { + return Objects.requireNonNull(operation, "operation").read(); + } + } + + /** Acquires a request-thread-owned lease that blocks checkpoint publication until closed. */ + public ReadLease acquireReadLease() throws IOException { + gate.readLock().lock(); + try { + requireState(State.READY, "common checkpoint runtime is not readable"); + return new ReadLease(Thread.currentThread()); + } catch (IOException | RuntimeException failure) { + gate.readLock().unlock(); + throw failure; + } + } + + public State getState() { + return state; + } + + @Override + public void close() { + gate.writeLock().lock(); + try { + state = State.CLOSED; + } finally { + gate.writeLock().unlock(); + } + } + + private void requireState(State expected, String message) throws IOException { + if (state != expected) { + throw new IOException(message + ": " + state); + } + } + + public enum State { + NEW, + RECOVERING, + READY, + CHECKPOINTING, + FAILED, + CLOSED + } + + @FunctionalInterface + public interface ReadableOperation { + T read() throws IOException; + } + + @FunctionalInterface + interface CompletionAction { + void run() throws IOException; + } + + /** One same-thread request lease; close it only after every pinned read resource is released. */ + public final class ReadLease implements AutoCloseable { + + private final Thread ownerThread; + private boolean closed; + + private ReadLease(Thread ownerThread) { + this.ownerThread = ownerThread; + } + + @Override + public void close() { + if (Thread.currentThread() != ownerThread) { + throw new IllegalStateException("common checkpoint read lease changed threads"); + } + if (!closed) { + closed = true; + gate.readLock().unlock(); + } + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointSnapshotRebaser.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointSnapshotRebaser.java new file mode 100644 index 00000000000..1c7aa645a01 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointSnapshotRebaser.java @@ -0,0 +1,110 @@ +package org.tron.core.db2.core; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +/** Removes an already-materialized Snapshot prefix without writing its mutations a second time. */ +public final class CommonCheckpointSnapshotRebaser { + + /** + * Reconnects every Store only after the complete target range has been validated everywhere. + * The caller must hold the common-checkpoint write gate and SnapshotManager monitor. + */ + public void rebase(List databases, CommonCheckpointTarget target, int count) + throws IOException { + CommonCheckpointTarget admittedTarget = Objects.requireNonNull(target, "target"); + if (count <= 0) { + throw new IllegalArgumentException("common checkpoint rebase count must be positive"); + } + List plans = new ArrayList<>(); + for (Chainbase database : Objects.requireNonNull(databases, "databases")) { + plans.add(validate(Objects.requireNonNull(database, "database"), admittedTarget, count)); + } + if (plans.isEmpty()) { + throw new IOException("common checkpoint rebase requires registered Stores"); + } + for (Plan plan : plans) { + plan.apply(); + } + } + + private static Plan validate(Chainbase database, CommonCheckpointTarget target, int count) + throws IOException { + Snapshot rootSnapshot = database.getHead().getRoot(); + if (!(rootSnapshot instanceof SnapshotRoot)) { + throw new IOException("common checkpoint rebase Store has no SnapshotRoot: " + + database.getDbName()); + } + SnapshotRoot root = (SnapshotRoot) rootSnapshot; + Snapshot next = root; + BlockSnapshotMeta previous = null; + BlockSnapshotMeta first = null; + for (int index = 0; index < count; index++) { + next = next.getNext(); + if (!(next instanceof SnapshotImpl)) { + throw new IOException("common checkpoint rebase Store has too few layers: " + + database.getDbName()); + } + BlockSnapshotMeta meta = ((SnapshotImpl) next).getBlockSnapshotMeta(); + if (meta == null || previous != null && !isChild(previous, meta)) { + throw new IOException("common checkpoint rebase Store block chain differs: " + + database.getDbName()); + } + if (first == null) { + first = meta; + } + previous = meta; + } + if (!target.getFirstBlock().equals(first) || !target.getLastBlock().equals(previous)) { + throw new IOException("common checkpoint rebase Store target differs: " + + database.getDbName()); + } + Snapshot successor = next.getNext(); + Snapshot head = database.getHead(); + if (head != next && successor == null) { + throw new IOException("common checkpoint rebase Store chain is disconnected: " + + database.getDbName()); + } + return new Plan(database, root, next, successor, head == next); + } + + private static boolean isChild(BlockSnapshotMeta parent, BlockSnapshotMeta child) { + return child.getEpoch() == parent.getEpoch() + 1 + && child.getBlockNumber() == parent.getBlockNumber() + 1 + && Arrays.equals(child.getParentHash(), parent.getBlockHash()); + } + + private static final class Plan { + + private final Chainbase database; + private final SnapshotRoot root; + private final Snapshot last; + private final Snapshot successor; + private final boolean consumesHead; + + private Plan(Chainbase database, SnapshotRoot root, Snapshot last, Snapshot successor, + boolean consumesHead) { + this.database = database; + this.root = root; + this.last = last; + this.successor = successor; + this.consumesHead = consumesHead; + } + + private void apply() { + root.resetSolidity(); + if (consumesHead) { + database.setHead(root); + root.setNext(null); + } else { + successor.setPrevious(root); + root.setNext(successor); + } + last.setNext(null); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointTarget.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointTarget.java new file mode 100644 index 00000000000..4867fcffb0c --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointTarget.java @@ -0,0 +1,114 @@ +package org.tron.core.db2.core; + +import java.util.Arrays; +import java.util.Objects; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +/** Immutable identity every authority must reach for one common-checkpoint payload. */ +public final class CommonCheckpointTarget { + + private final byte[] formatIdentity; + private final byte[] payloadDigest; + private final BlockSnapshotMeta firstBlock; + private final BlockSnapshotMeta lastBlock; + private final byte[] parentStateRoot; + private final byte[] stateRoot; + + private CommonCheckpointTarget(byte[] formatIdentity, byte[] payloadDigest, + BlockSnapshotMeta firstBlock, BlockSnapshotMeta lastBlock, byte[] parentStateRoot, + byte[] stateRoot) { + this.formatIdentity = copy(formatIdentity); + this.payloadDigest = copy(payloadDigest); + this.firstBlock = Objects.requireNonNull(firstBlock, "firstBlock"); + this.lastBlock = Objects.requireNonNull(lastBlock, "lastBlock"); + this.parentStateRoot = copy(parentStateRoot); + this.stateRoot = copy(stateRoot); + } + + public static CommonCheckpointTarget from(CommonCheckpointPayload payload) { + CommonCheckpointPayload admitted = Objects.requireNonNull(payload, "payload"); + return new CommonCheckpointTarget(admitted.getFormatIdentity(), + new CommonCheckpointPayloadCodec().digest(admitted), + admitted.getBlocks().get(0).getMeta(), + admitted.getBlocks().get(admitted.getBlocks().size() - 1).getMeta(), + admitted.getParentStateRoot(), admitted.getStateRoot()); + } + + /** Reconstructs a target identity from a checksummed authority publication record. */ + public static CommonCheckpointTarget restore(byte[] formatIdentity, byte[] payloadDigest, + BlockSnapshotMeta firstBlock, BlockSnapshotMeta lastBlock, byte[] parentStateRoot, + byte[] stateRoot) { + BlockSnapshotMeta first = Objects.requireNonNull(firstBlock, "firstBlock"); + BlockSnapshotMeta last = Objects.requireNonNull(lastBlock, "lastBlock"); + if (first.getEpoch() > last.getEpoch() + || first.getBlockNumber() > last.getBlockNumber()) { + throw new IllegalArgumentException("checkpoint target block range is reversed"); + } + return new CommonCheckpointTarget(requireDigest(formatIdentity, "formatIdentity"), + requireDigest(payloadDigest, "payloadDigest"), first, last, + requireDigest(parentStateRoot, "parentStateRoot"), requireDigest(stateRoot, "stateRoot")); + } + + public byte[] getFormatIdentity() { + return copy(formatIdentity); + } + + public byte[] getPayloadDigest() { + return copy(payloadDigest); + } + + public BlockSnapshotMeta getFirstBlock() { + return firstBlock; + } + + public BlockSnapshotMeta getLastBlock() { + return lastBlock; + } + + public byte[] getParentStateRoot() { + return copy(parentStateRoot); + } + + public byte[] getStateRoot() { + return copy(stateRoot); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof CommonCheckpointTarget)) { + return false; + } + CommonCheckpointTarget that = (CommonCheckpointTarget) object; + return firstBlock.equals(that.firstBlock) + && lastBlock.equals(that.lastBlock) + && Arrays.equals(formatIdentity, that.formatIdentity) + && Arrays.equals(payloadDigest, that.payloadDigest) + && Arrays.equals(parentStateRoot, that.parentStateRoot) + && Arrays.equals(stateRoot, that.stateRoot); + } + + @Override + public int hashCode() { + int result = Objects.hash(firstBlock, lastBlock); + result = 31 * result + Arrays.hashCode(formatIdentity); + result = 31 * result + Arrays.hashCode(payloadDigest); + result = 31 * result + Arrays.hashCode(parentStateRoot); + result = 31 * result + Arrays.hashCode(stateRoot); + return result; + } + + private static byte[] copy(byte[] value) { + return Arrays.copyOf(Objects.requireNonNull(value, "value"), value.length); + } + + private static byte[] requireDigest(byte[] value, String name) { + byte[] admitted = copy(value); + if (admitted.length != 32) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return admitted; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java index b377689592c..e6a2d2f6c91 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java @@ -5,6 +5,7 @@ import com.google.common.collect.Maps; import com.google.common.collect.Streams; import com.google.common.primitives.Bytes; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -19,6 +20,7 @@ import org.tron.core.db2.common.Value; import org.tron.core.db2.common.Value.Operator; import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.stateroot.PathStateSnapshotDelta; public class SnapshotImpl extends AbstractSnapshot { @@ -31,6 +33,9 @@ public class SnapshotImpl extends AbstractSnapshot { @Getter private BlockReverseDiff preparedArchiveBlock; + @Getter + private PathStateSnapshotDelta preparedPathStateDelta; + SnapshotImpl(Snapshot snapshot) { root = snapshot.getRoot(); synchronized (this) { @@ -47,12 +52,33 @@ public class SnapshotImpl extends AbstractSnapshot { /** * Publishes the immutable block identity and optional archive payload on this layer. * - *

The caller performs all validation and materialization first, so this method is the - * non-throwing ownership-transfer point for a successfully committed block session. + *

The caller validates the prepared payload before this ownership-transfer point. */ void attachArchiveBlock(BlockSnapshotMeta meta, BlockReverseDiff reverseDiff) { + attachBlockArtifacts(meta, reverseDiff, null); + } + + /** Atomically binds all prepared block-final artifacts owned by this Snapshot layer. */ + void attachBlockArtifacts(BlockSnapshotMeta meta, BlockReverseDiff reverseDiff, + PathStateSnapshotDelta pathStateDelta) { + BlockSnapshotMeta admitted = Objects.requireNonNull(meta, "meta"); + if (reverseDiff != null && !admitted.equals(reverseDiff.getMeta())) { + throw new IllegalArgumentException("archive payload differs from Snapshot block identity"); + } + if (pathStateDelta != null && !admitted.equals(pathStateDelta.getMeta())) { + throw new IllegalArgumentException("path-state delta differs from Snapshot block identity"); + } + if (reverseDiff != null && pathStateDelta != null) { + byte[] archiveView = reverseDiff.getMutationViewDigest(); + if (archiveView == null + || !Arrays.equals(archiveView, pathStateDelta.getMutationViewDigest())) { + throw new IllegalArgumentException( + "archive and path-state artifacts differ from mutation view identity"); + } + } blockSnapshotMeta = meta; preparedArchiveBlock = reverseDiff; + preparedPathStateDelta = pathStateDelta; } @Override diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index cbfb12bc423..df584e4be03 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -52,6 +52,7 @@ import org.tron.core.db2.archive.OldValueCollector; import org.tron.core.db2.stateroot.PathStateBlockTransition; import org.tron.core.db2.stateroot.PathStateRuntimeAttachment; +import org.tron.core.db2.stateroot.PathStateSnapshotDelta; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.IRevokingDB; import org.tron.core.db2.common.Key; @@ -252,6 +253,7 @@ public synchronized void commit() { */ public synchronized void commit(BlockSnapshotMeta meta) { Objects.requireNonNull(meta, "meta"); + long startedNanos = System.nanoTime(); if (activeSession <= 0) { throw new RevokingStoreIllegalStateException(activeSession); } @@ -269,14 +271,19 @@ public synchronized void commit(BlockSnapshotMeta meta) { if (oldValueCollector != null || pathStateRuntimeAttachment != null) { changeView = BlockChangeView.capture(meta, dbs); } + long frozenNanos = System.nanoTime(); BlockReverseDiff reverseDiff = null; if (oldValueCollector != null) { reverseDiff = Objects.requireNonNull( oldValueCollector.collect(changeView), "archive collector returned null"); } + long archiveNanos = System.nanoTime(); PathStateBlockTransition pathStateTransition = pathStateRuntimeAttachment == null ? null : pathStateRuntimeAttachment.capture(changeView); + long pathNanos = System.nanoTime(); + PathStateSnapshotDelta pathStateDelta = pathStateTransition == null ? null + : pathStateRuntimeAttachment.preparedSnapshotDelta(pathStateTransition); dbs.forEach(db -> { if (db.getHead().isOptimized()) { @@ -285,13 +292,28 @@ public synchronized void commit(BlockSnapshotMeta meta) { }); for (Chainbase db : dbs) { - ((SnapshotImpl) db.getHead()).attachArchiveBlock(meta, - ArchiveStoreScope.isStateDatabase(db.getDbName()) ? reverseDiff : null); + boolean stateDatabase = ArchiveStoreScope.isStateDatabase(db.getDbName()); + ((SnapshotImpl) db.getHead()).attachBlockArtifacts(meta, + stateDatabase ? reverseDiff : null, stateDatabase ? pathStateDelta : null); } + long attachedNanos = System.nanoTime(); --activeSession; if (pathStateRuntimeAttachment != null) { pathStateRuntimeAttachment.publish(pathStateTransition); } + long completedNanos = System.nanoTime(); + if (changeView != null) { + logger.info("Block-final artifact stages: head={}, freezeMs={}, archiveMs={}, " + + "pathCaptureMs={}, attachMs={}, publishMs={}, totalMs={}", + meta.getBlockNumber(), elapsedMillis(startedNanos, frozenNanos), + elapsedMillis(frozenNanos, archiveNanos), elapsedMillis(archiveNanos, pathNanos), + elapsedMillis(pathNanos, attachedNanos), elapsedMillis(attachedNanos, completedNanos), + elapsedMillis(startedNanos, completedNanos)); + } + } + + private static long elapsedMillis(long startedNanos, long completedNanos) { + return TimeUnit.NANOSECONDS.toMillis(completedNanos - startedNanos); } /** Previews optional producer metadata from the active block session without publishing it. */ diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotRoot.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotRoot.java index f95cf68dafe..09ebab8fb1f 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotRoot.java @@ -121,7 +121,21 @@ public void merge(List snapshots) { } } + /** Applies a fully coalesced common-checkpoint Store batch with an explicit sync barrier. */ + void applyCheckpointMutations(Map batch) { + if (needOptAsset()) { + processAccount(batch, true); + } else { + ((Flusher) db).flushSynced(batch); + putCache(batch); + } + } + private void processAccount(Map batch) { + processAccount(batch, false); + } + + private void processAccount(Map batch, boolean synced) { AccountAssetStore assetStore = ChainBaseManager.getInstance().getAccountAssetStore(); Map accounts = new HashMap<>(); Map assets = new HashMap<>(); @@ -140,10 +154,18 @@ private void processAccount(Map batch) { accounts.put(k, WrappedByteArray.of(item.getData())); } }); - ((Flusher) db).flush(accounts); + if (synced) { + ((Flusher) db).flushSynced(accounts); + } else { + ((Flusher) db).flush(accounts); + } putCache(accounts); if (assets.size() > 0) { - assetStore.updateByBatch(AccountAssetStore.convert(assets)); + if (synced) { + assetStore.updateByBatchSynced(AccountAssetStore.convert(assets)); + } else { + assetStore.updateByBatch(AccountAssetStore.convert(assets)); + } } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index 802849d2fe6..638f6f78974 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -37,8 +37,13 @@ public final class PathMerkleTrie { }; private final PathNodeStore nodeStore; + private static final AtomicLong NODE_CREATE_COUNT = new AtomicLong(); + private static final AtomicLong NODE_KECCAK_COUNT = new AtomicLong(); + + private final boolean lazyHashReferences; private final Map leaves = new TreeMap<>(UNSIGNED_KEY_COMPARATOR); private final IdentityHashMap materializedNodes = new IdentityHashMap<>(); + private final IdentityHashMap resolvedMaterializedNodes = new IdentityHashMap<>(); private Snapshot inheritedSnapshot; private Node rootNode; private Node materializedRoot; @@ -48,11 +53,21 @@ public final class PathMerkleTrie { private boolean frozen; private int lastNodePuts; private int lastNodeDeletes; + private long lastCommitPlanNanos; + private long lastCommitStoreNanos; + private long lastCommitFinalizeNanos; private final AtomicLong nodeDecodeCount = new AtomicLong(); private final AtomicLong nodeHashVerifyCount = new AtomicLong(); + private final AtomicLong hashReferenceCreateCount = new AtomicLong(); + private final AtomicLong hashReferenceResolveCount = new AtomicLong(); public PathMerkleTrie(PathNodeStore nodeStore) { + this(nodeStore, true); + } + + PathMerkleTrie(PathNodeStore nodeStore, boolean lazyHashReferences) { this.nodeStore = Objects.requireNonNull(nodeStore, "nodeStore"); + this.lazyHashReferences = lazyHashReferences; } public synchronized void put(byte[] secureKey, byte[] encodedValue) { @@ -88,7 +103,7 @@ synchronized void applyBatch(List mutations, ExecutorService exec Node resolvedRoot = resolve(rootNode, EMPTY_PATH); rootNode = resolvedRoot; if (batch.size() < PARALLEL_UPDATE_THRESHOLD || !(resolvedRoot instanceof BranchNode)) { - applySequential(batch); + applySequentialBatch(batch); return; } BranchNode root = (BranchNode) resolvedRoot; @@ -110,26 +125,32 @@ synchronized void applyBatch(List mutations, ExecutorService exec } } if (guaranteedSurvivors < 2) { - applySequential(batch); + applySequentialBatch(batch); return; } - List> futures = new ArrayList<>(); - List positions = new ArrayList<>(); + List work = new ArrayList<>(); for (int i = 0; i < groups.size(); i++) { if (!groups.get(i).isEmpty()) { - final int position = i; - positions.add(position); - futures.add(Objects.requireNonNull(executor, "executor").submit( - () -> applySubtree(root.children[position], position, groups.get(position)))); + work.add(new SubtreeWork(i, groups.get(i))); } } + work.sort((left, right) -> { + int compared = Integer.compare(right.mutations.size(), left.mutations.size()); + return compared != 0 ? compared : Integer.compare(left.position, right.position); + }); + List> futures = new ArrayList<>(work.size()); + for (SubtreeWork subtree : work) { + futures.add(Objects.requireNonNull(executor, "executor").submit( + () -> applySubtree(root.children[subtree.position], subtree.position, + subtree.mutations))); + } Node[] children = Arrays.copyOf(root.children, root.children.length); Map previous = new LinkedHashMap<>(); Map changed = new LinkedHashMap<>(); try { for (int i = 0; i < futures.size(); i++) { SubtreeResult result = futures.get(i).get(); - children[positions.get(i)] = result.node; + children[work.get(i).position] = result.node; previous.putAll(result.previous); changed.putAll(result.changed); } @@ -165,13 +186,34 @@ synchronized void applyBatch(List mutations, ExecutorService exec dirty = true; } - private void applySequential(List mutations) { + private static final class SubtreeWork { + + private final int position; + private final List mutations; + + private SubtreeWork(int position, List mutations) { + this.position = position; + this.mutations = mutations; + } + } + + private void applySequentialBatch(List mutations) { for (BatchMutation mutation : mutations) { - if (mutation.encodedValue == null) { - delete(mutation.secureKey); - } else { - put(mutation.secureKey, mutation.encodedValue); + BytesKey key = new BytesKey(mutation.secureKey); + byte[] previous = mutation.previousValueKnown + ? mutation.previousEncodedValue : leafValue(key); + if (Arrays.equals(previous, mutation.encodedValue)) { + continue; + } + leaves.put(key, mutation.encodedValue); + if (previous == null && mutation.encodedValue != null) { + leafCount++; + } else if (previous != null && mutation.encodedValue == null) { + leafCount--; } + rootNode = update(rootNode, mutation.nibbles, 0, EMPTY_PATH, + mutation.encodedValue); + dirty = true; } } @@ -181,16 +223,18 @@ private SubtreeResult applySubtree(Node initial, int nibble, byte[] path = new byte[]{(byte) nibble}; Map previous = new LinkedHashMap<>(); Map changed = new LinkedHashMap<>(); + List effective = new ArrayList<>(); for (BatchMutation mutation : mutations) { BytesKey key = new BytesKey(mutation.secureKey); byte[] oldValue; - if (leaves.containsKey(key)) { + if (mutation.previousValueKnown) { + oldValue = mutation.previousEncodedValue; + } else if (leaves.containsKey(key)) { oldValue = leaves.get(key); } else if (inheritedSnapshot != null && inheritedSnapshot.containsLeaf(key)) { oldValue = inheritedSnapshot.leafValue(key); } else { - byte[] nibbles = toNibbles(key.bytes); - ValueResult result = resolvedValueAt(node, nibbles, 1, path); + ValueResult result = resolvedValueAt(node, mutation.nibbles, 1, path); node = result.node; oldValue = result.value; } @@ -198,12 +242,138 @@ private SubtreeResult applySubtree(Node initial, int nibble, if (Arrays.equals(oldValue, mutation.encodedValue)) { continue; } - node = update(node, toNibbles(key.bytes), 1, path, mutation.encodedValue); changed.put(key, mutation.encodedValue); + effective.add(mutation); + } + if (!effective.isEmpty()) { + node = updateBatch(node, effective, 1, path); } return new SubtreeResult(node, previous, changed); } + /** Applies sorted, unique edits while rebuilding every shared ancestor at most once. */ + private Node updateBatch(Node node, List mutations, int offset, byte[] path) { + if (mutations.isEmpty()) { + return node; + } + if (node == null) { + return buildMutations(mutations, offset); + } + Node present = resolve(node, path); + if (present instanceof LeafNode) { + return updateLeafBatch((LeafNode) present, mutations, offset, path); + } + if (present instanceof ExtensionNode) { + return updateExtensionBatch((ExtensionNode) present, mutations, offset, path); + } + BranchNode branch = (BranchNode) present; + Node[] children = Arrays.copyOf(branch.children, branch.children.length); + boolean changed = false; + List> groups = groupMutations(mutations, offset); + for (int nibble = 0; nibble < groups.size(); nibble++) { + if (groups.get(nibble).isEmpty()) { + continue; + } + Node previous = children[nibble]; + children[nibble] = updateBatch(previous, groups.get(nibble), offset + 1, + append(path, new byte[]{(byte) nibble})); + changed |= previous != children[nibble]; + } + return changed ? normalizeBranch(children, path) : present; + } + + private Node updateLeafBatch(LeafNode leaf, List mutations, int offset, + byte[] path) { + List entries = new ArrayList<>(mutations.size() + 1); + byte[] leafKey = append(path, leaf.path); + boolean retainLeaf = true; + for (BatchMutation mutation : mutations) { + byte[] key = mutation.nibbles; + if (Arrays.equals(key, leafKey)) { + retainLeaf = false; + } + if (mutation.encodedValue != null) { + entries.add(new Leaf(key, mutation.encodedValue)); + } + } + if (retainLeaf) { + entries.add(new Leaf(leafKey, leaf.value)); + } + if (entries.isEmpty()) { + return null; + } + entries.sort((left, right) -> compareNibbles(left.nibbles, right.nibbles)); + return build(entries, offset); + } + + private Node updateExtensionBatch(ExtensionNode extension, List mutations, + int offset, byte[] path) { + int shared = extension.path.length; + for (BatchMutation mutation : mutations) { + shared = Math.min(shared, + commonPrefix(extension.path, 0, mutation.nibbles, offset)); + } + if (shared == extension.path.length) { + Node child = updateBatch(extension.child, mutations, offset + shared, + append(path, extension.path)); + return child == extension.child ? extension + : normalizeExtension(extension.path, child, path); + } + + byte[] common = Arrays.copyOf(extension.path, shared); + byte[] branchPath = append(path, common); + Node[] children = new Node[16]; + int oldNibble = extension.path[shared]; + byte[] oldSuffix = Arrays.copyOfRange(extension.path, shared + 1, + extension.path.length); + children[oldNibble] = oldSuffix.length == 0 ? extension.child + : new ExtensionNode(oldSuffix, extension.child); + int groupOffset = offset + shared; + List> groups = groupMutations(mutations, groupOffset); + for (int nibble = 0; nibble < groups.size(); nibble++) { + if (groups.get(nibble).isEmpty()) { + continue; + } + children[nibble] = updateBatch(children[nibble], groups.get(nibble), groupOffset + 1, + append(branchPath, new byte[]{(byte) nibble})); + } + Node branch = normalizeBranch(children, branchPath); + return shared == 0 ? branch : normalizeExtension(common, branch, path); + } + + private static Node buildMutations(List mutations, int offset) { + List entries = new ArrayList<>(mutations.size()); + for (BatchMutation mutation : mutations) { + if (mutation.encodedValue != null) { + entries.add(new Leaf(mutation.nibbles, mutation.encodedValue)); + } + } + entries.sort((left, right) -> compareNibbles(left.nibbles, right.nibbles)); + return entries.isEmpty() ? null : build(entries, offset); + } + + private static List> groupMutations( + List mutations, int offset) { + List> groups = new ArrayList<>(16); + for (int nibble = 0; nibble < 16; nibble++) { + groups.add(new ArrayList<>()); + } + for (BatchMutation mutation : mutations) { + groups.get(mutation.nibbles[offset]).add(mutation); + } + return groups; + } + + private static int compareNibbles(byte[] left, byte[] right) { + for (int index = 0; index < Math.min(left.length, right.length); index++) { + int compared = Byte.compare(left[index], right[index]); + if (compared != 0) { + return compared; + } + } + return Integer.compare(left.length, right.length); + } + private static void cancel(List> futures) { for (Future future : futures) { if (!future.isDone()) { @@ -237,6 +407,18 @@ synchronized int getLastNodeDeletes() { return lastNodeDeletes; } + synchronized long getLastCommitPlanNanos() { + return lastCommitPlanNanos; + } + + synchronized long getLastCommitStoreNanos() { + return lastCommitStoreNanos; + } + + synchronized long getLastCommitFinalizeNanos() { + return lastCommitFinalizeNanos; + } + synchronized long getNodeDecodeCount() { return nodeDecodeCount.get(); } @@ -245,6 +427,24 @@ synchronized long getNodeHashVerifyCount() { return nodeHashVerifyCount.get(); } + synchronized long getHashReferenceCreateCount() { + return hashReferenceCreateCount.get(); + } + + synchronized long getHashReferenceResolveCount() { + return hashReferenceResolveCount.get(); + } + + /** Cumulative count of LeafNode/ExtensionNode/BranchNode creations (RLP encodes). */ + static long nodeCreateCountTotal() { + return NODE_CREATE_COUNT.get(); + } + + /** Cumulative count of actual Keccak computations on node RLP (cache misses). */ + static long nodeKeccakCountTotal() { + return NODE_KECCAK_COUNT.get(); + } + synchronized List leafEntries() { Map effective = effectiveLeaves(); List entries = new ArrayList<>(effective.size()); @@ -331,7 +531,7 @@ synchronized byte[] restoreRoot() { } private void installStoredRoot(byte[] encoded, byte[] expected) { - rootNode = new StoredNode(encoded, EMPTY_PATH); + rootNode = new StoredNode(encoded, EMPTY_PATH, expected); materializedRoot = rootNode; rememberMaterialized(rootNode, EMPTY_PATH); rootHash = Arrays.copyOf(expected, expected.length); @@ -352,12 +552,13 @@ private void importLeaves(Collection entries, String operation) { } } - /** Verifies every path owned by the current materialized node set without repairing it. */ - public synchronized void verifyNodeStore() { + /** Explicitly scans every reachable path in the materialized node set without repairing it. */ + public synchronized void verifyAllNodeStore() { if (dirty) { throw new IllegalStateException("cannot verify a dirty path trie"); } - Map expectedNodes = collectNodes(rootNode); + Map expectedNodes = new LinkedHashMap<>(); + collectResolvedNodes(rootNode, EMPTY_PATH, expectedNodes); for (Map.Entry entry : expectedNodes.entrySet()) { if (!Arrays.equals(nodeStore.get(entry.getKey().bytes), entry.getValue())) { throw new IllegalStateException("missing or corrupt materialized path node"); @@ -368,26 +569,56 @@ public synchronized void verifyNodeStore() { } } + /** Compatibility alias for explicit rebuild, repair, and test callers. */ + public synchronized void verifyNodeStore() { + verifyAllNodeStore(); + } + + private void collectResolvedNodes(Node node, byte[] path, Map nodes) { + if (node == null) { + return; + } + Node present = resolve(node, path); + if (nodes.put(new BytesKey(path), encodedNode(present)) != null) { + throw new IllegalStateException("duplicate path-state node path"); + } + visitChildren(present, path, + (child, childPath) -> collectResolvedNodes(child, childPath, nodes)); + } + private void commit() { + long startedNanos = System.nanoTime(); IdentityHashMap retained = new IdentityHashMap<>(); List additions = new ArrayList<>(); collectAdditions(rootNode, EMPTY_PATH, retained, additions); List removals = new ArrayList<>(); collectRemovals(materializedRoot, retained, removals); + long plannedNanos = System.nanoTime(); for (NodePath removal : removals) { nodeStore.delete(removal.path); materializedNodes.remove(removal.node); } for (NodePath addition : additions) { - nodeStore.put(addition.path, addition.node.encoded); - materializedNodes.put(addition.node, new BytesKey(addition.path)); + nodeStore.put(addition.path, encodedNode(addition.node)); + rememberMaterialized(addition.node, addition.path); + } + long storedNanos = System.nanoTime(); + synchronized (resolvedMaterializedNodes) { + for (Node resolvedReference : resolvedMaterializedNodes.keySet()) { + materializedNodes.remove(resolvedReference); + } + resolvedMaterializedNodes.clear(); } lastNodeDeletes = removals.size(); lastNodePuts = additions.size(); materializedRoot = rootNode; rootHash = hash(rootNode); dirty = false; + long finishedNanos = System.nanoTime(); + lastCommitPlanNanos = plannedNanos - startedNanos; + lastCommitStoreNanos = storedNanos - plannedNanos; + lastCommitFinalizeNanos = finishedNanos - storedNanos; } private void collectAdditions(Node node, byte[] path, @@ -413,12 +644,16 @@ private void collectRemovals(Node node, IdentityHashMap retained, if (node == null || retained.containsKey(node)) { return; } - BytesKey path = materializedPath(node); + Node present = resolvedMaterializedNode(node); + if (retained.containsKey(present)) { + return; + } + BytesKey path = materializedPath(present); if (path == null) { throw new IllegalStateException("path-local update lost a materialized node identity"); } - removals.add(new NodePath(node, path.copy())); - visitChildren(node, path.bytes, + removals.add(new NodePath(present, path.copy())); + visitChildren(present, path.bytes, (child, ignored) -> collectRemovals(child, retained, removals)); } @@ -454,17 +689,27 @@ private Map effectiveLeaves() { } private BytesKey materializedPath(Node node) { + BytesKey bound = node.materializedPath(); + if (bound != null) { + return bound; + } + if (node instanceof HashRefNode) { + return node.bindMaterializedPath(new BytesKey(((HashRefNode) node).path)); + } BytesKey path; synchronized (materializedNodes) { path = materializedNodes.get(node); } - return path != null || inheritedSnapshot == null - ? path : inheritedSnapshot.materializedPath(node); + if (path == null && inheritedSnapshot != null) { + path = inheritedSnapshot.materializedPath(node); + } + return path == null ? null : node.bindMaterializedPath(path); } private void rememberMaterialized(Node node, byte[] path) { + BytesKey materialized = node.bindMaterializedPath(new BytesKey(path)); synchronized (materializedNodes) { - materializedNodes.put(node, new BytesKey(path)); + materializedNodes.put(node, materialized); } } @@ -475,10 +720,25 @@ private Node retainResolvedReplacement(Node original, Node replacement, byte[] p throw new IllegalStateException("resolved path trie node moved from its durable path"); } rememberMaterialized(replacement, path); + synchronized (resolvedMaterializedNodes) { + resolvedMaterializedNodes.put(original, replacement); + } } return replacement; } + private Node resolvedMaterializedNode(Node node) { + Node present = node; + synchronized (resolvedMaterializedNodes) { + Node replacement = resolvedMaterializedNodes.get(present); + while (replacement != null && replacement != present) { + present = replacement; + replacement = resolvedMaterializedNodes.get(present); + } + } + return present; + } + private void requireMutable() { if (frozen) { throw new IllegalStateException("path trie is frozen as an immutable parent snapshot"); @@ -657,7 +917,7 @@ private static void collectNodes(Node node, byte[] path, Map n if (node == null) { return; } - if (nodes.put(new BytesKey(path), node.encoded) != null) { + if (nodes.put(new BytesKey(path), encodedNode(node)) != null) { throw new IllegalStateException("duplicate path-state node path"); } visitChildren(node, path, (child, childPath) -> collectNodes(child, childPath, nodes)); @@ -668,7 +928,7 @@ private static void indexNodes(Node node, byte[] path, if (node == null) { return; } - indexed.put(node, new BytesKey(path)); + indexed.put(node, node.bindMaterializedPath(new BytesKey(path))); visitChildren(node, path, (child, childPath) -> indexNodes(child, childPath, indexed)); } @@ -688,7 +948,7 @@ private static void visitChildren(Node node, byte[] path, NodeVisitor visitor) { private static byte[] hash(Node node) { return node == null ? Arrays.copyOf(Hash.EMPTY_TRIE_HASH, Hash.EMPTY_TRIE_HASH.length) - : Hash.sha3(node.encoded); + : nodeHash(node); } private static int sharedPrefix(List entries, int depth) { @@ -768,12 +1028,27 @@ && commonPrefix(leaf.path, 0, key, offset) == remaining } private Node resolve(Node node, byte[] expectedPath) { + if (node instanceof HashRefNode) { + HashRefNode reference = (HashRefNode) node; + if (!Arrays.equals(reference.path, expectedPath)) { + throw new IllegalStateException("hashed path trie node moved from its durable path"); + } + byte[] encoded = nodeStore.get(reference.path); + nodeHashVerifyCount.incrementAndGet(); + if (encoded == null || !Arrays.equals(Hash.sha3(encoded), reference.expectedHash)) { + throw new IllegalStateException("path-state durable child is missing or corrupt"); + } + hashReferenceResolveCount.incrementAndGet(); + StoredNode stored = new StoredNode(encoded, reference.path, reference.expectedHash); + rememberMaterialized(stored, reference.path); + return retainResolvedReplacement(reference, resolve(stored, expectedPath), expectedPath); + } if (!(node instanceof StoredNode)) { return node; } StoredNode stored = (StoredNode) node; nodeDecodeCount.incrementAndGet(); - byte[] storedEncoding = node.encoded; + byte[] storedEncoding = encodedNode(node); if (!Arrays.equals(stored.path, expectedPath)) { throw new IllegalStateException("stored path trie node moved from its durable path"); } @@ -806,9 +1081,12 @@ private Node resolve(Node node, byte[] expectedPath) { } else { throw new IllegalStateException("path-state durable node has invalid arity"); } - if (!Arrays.equals(decoded.encoded, storedEncoding)) { + if (!Arrays.equals(encodedNode(decoded), storedEncoding)) { throw new IllegalStateException("path-state durable node is not canonically encoded"); } + if (stored.knownHash != null) { + decoded.bindCachedHash(stored.knownHash); + } return retainResolvedReplacement(stored, decoded, expectedPath); } @@ -832,14 +1110,18 @@ private Node storedChild(RlpElement reference, byte[] path) { if (reference.payload.length != SECURE_KEY_LENGTH) { throw new IllegalStateException("path-state child hash must contain exactly 32 bytes"); } - byte[] encoded = nodeStore.get(path); - nodeHashVerifyCount.incrementAndGet(); - if (encoded == null || !Arrays.equals(Hash.sha3(encoded), reference.payload)) { - throw new IllegalStateException("path-state durable child is missing or corrupt"); + if (!lazyHashReferences) { + byte[] encoded = nodeStore.get(path); + nodeHashVerifyCount.incrementAndGet(); + if (encoded == null || !Arrays.equals(Hash.sha3(encoded), reference.payload)) { + throw new IllegalStateException("path-state durable child is missing or corrupt"); + } + Node stored = new StoredNode(encoded, path, reference.payload); + rememberMaterialized(stored, path); + return stored; } - Node stored = new StoredNode(encoded, path); - rememberMaterialized(stored, path); - return stored; + hashReferenceCreateCount.incrementAndGet(); + return new HashRefNode(path, reference.payload); } private static List decodeList(byte[] encoded) { @@ -916,8 +1198,26 @@ private static Compact decodeCompact(byte[] encoded) { return new Compact((flags & 2) != 0, path); } - private static byte[] nodeReference(byte[] encodedNode) { - return encodedNode.length < SECURE_KEY_LENGTH ? encodedNode : rlpItem(Hash.sha3(encodedNode)); + private static byte[] encodedNode(Node node) { + byte[] encoded = Objects.requireNonNull(node, "node").encoded; + if (encoded == null) { + throw new IllegalStateException("unresolved path-state hash reference has no node encoding"); + } + return encoded; + } + + private static byte[] nodeHash(Node node) { + return node instanceof HashRefNode + ? Arrays.copyOf(((HashRefNode) node).expectedHash, SECURE_KEY_LENGTH) + : node.cachedHash(); + } + + private static byte[] nodeReference(Node node) { + if (node instanceof HashRefNode) { + return rlpItem(((HashRefNode) node).expectedHash); + } + byte[] encoded = encodedNode(node); + return encoded.length < SECURE_KEY_LENGTH ? encoded : rlpItem(node.cachedHash()); } private static byte[] compactPath(byte[] nibbles, boolean leaf) { @@ -1015,19 +1315,98 @@ private static byte[] concatenate(byte[] first, byte[] second) { private abstract static class Node { private final byte[] encoded; + private volatile byte[] hash; + private volatile BytesKey materializedPath; private Node(byte[] encoded) { this.encoded = encoded; } + + private byte[] cachedHash() { + byte[] cached = hash; + if (cached == null) { + synchronized (this) { + cached = hash; + if (cached == null) { + NODE_KECCAK_COUNT.incrementAndGet(); + cached = Hash.sha3(encodedNode(this)); + hash = cached; + } + } + } + return cached; + } + + private BytesKey materializedPath() { + return materializedPath; + } + + private BytesKey bindMaterializedPath(BytesKey path) { + BytesKey present = materializedPath; + if (present == null) { + synchronized (this) { + present = materializedPath; + if (present == null) { + materializedPath = path; + return path; + } + } + } + if (!Arrays.equals(present.bytes, path.bytes)) { + throw new IllegalStateException("path-local node identity moved between durable paths"); + } + return present; + } + + private void bindCachedHash(byte[] knownHash) { + byte[] known = Arrays.copyOf(Objects.requireNonNull(knownHash, "knownHash"), + knownHash.length); + if (known.length != SECURE_KEY_LENGTH) { + throw new IllegalArgumentException("knownHash must contain exactly 32 bytes"); + } + synchronized (this) { + if (hash == null) { + hash = known; + } else if (!Arrays.equals(hash, known)) { + throw new IllegalStateException("path-state node cached hash does not match storage"); + } + } + } + } private static final class StoredNode extends Node { private final byte[] path; + private final byte[] knownHash; private StoredNode(byte[] encoded, byte[] path) { + this(encoded, path, null); + } + + private StoredNode(byte[] encoded, byte[] path, byte[] knownHash) { super(Arrays.copyOf(Objects.requireNonNull(encoded, "encoded"), encoded.length)); this.path = Arrays.copyOf(Objects.requireNonNull(path, "path"), path.length); + this.knownHash = knownHash == null ? null : Arrays.copyOf(knownHash, knownHash.length); + if (this.knownHash != null && this.knownHash.length != SECURE_KEY_LENGTH) { + throw new IllegalArgumentException("knownHash must contain exactly 32 bytes"); + } + } + } + + private static final class HashRefNode extends Node { + + private final byte[] path; + private final byte[] expectedHash; + + private HashRefNode(byte[] path, byte[] expectedHash) { + super(null); + this.path = Arrays.copyOf(Objects.requireNonNull(path, "path"), path.length); + this.expectedHash = Arrays.copyOf(Objects.requireNonNull(expectedHash, "expectedHash"), + expectedHash.length); + if (this.expectedHash.length != SECURE_KEY_LENGTH) { + throw new IllegalArgumentException("expectedHash must contain exactly 32 bytes"); + } } } @@ -1038,9 +1417,11 @@ private static final class LeafNode extends Node { private LeafNode(byte[] path, byte[] value) { super(rlpList(rlpItem(compactPath(path, true)), rlpItem(value))); + NODE_CREATE_COUNT.incrementAndGet(); this.path = Arrays.copyOf(path, path.length); this.value = Arrays.copyOf(value, value.length); } + } private static final class ExtensionNode extends Node { @@ -1050,6 +1431,7 @@ private static final class ExtensionNode extends Node { private ExtensionNode(byte[] path, Node child) { super(encode(path, child)); + NODE_CREATE_COUNT.incrementAndGet(); if (path.length == 0) { throw new IllegalArgumentException("extension path must not be empty"); } @@ -1059,7 +1441,7 @@ private ExtensionNode(byte[] path, Node child) { private static byte[] encode(byte[] path, Node child) { Node present = Objects.requireNonNull(child, "child"); - return rlpList(rlpItem(compactPath(path, false)), nodeReference(present.encoded)); + return rlpList(rlpItem(compactPath(path, false)), nodeReference(present)); } } @@ -1069,6 +1451,7 @@ private static final class BranchNode extends Node { private BranchNode(Node[] children) { super(encode(children)); + NODE_CREATE_COUNT.incrementAndGet(); this.children = Arrays.copyOf(children, children.length); } @@ -1079,7 +1462,7 @@ private static byte[] encode(Node[] children) { List encodedChildren = new ArrayList<>(Collections.nCopies(17, EMPTY_RLP_ITEM)); for (int i = 0; i < children.length; i++) { if (children[i] != null) { - encodedChildren.set(i, nodeReference(children[i].encoded)); + encodedChildren.set(i, nodeReference(children[i])); } } return rlpList(encodedChildren.toArray(new byte[encodedChildren.size()][])); @@ -1122,12 +1505,24 @@ private ValueResult(byte[] value, Node node) { static final class BatchMutation { private final byte[] secureKey; + private final byte[] nibbles; private final byte[] encodedValue; + private final boolean previousValueKnown; + private final byte[] previousEncodedValue; BatchMutation(byte[] secureKey, byte[] encodedValue) { + this(secureKey, encodedValue, false, null); + } + + BatchMutation(byte[] secureKey, byte[] encodedValue, boolean previousValueKnown, + byte[] previousEncodedValue) { this.secureKey = PathMerkleTrie.secureKey(secureKey).copy(); + nibbles = toNibbles(this.secureKey); this.encodedValue = encodedValue == null ? null : nonEmpty(encodedValue, "encodedValue"); + this.previousValueKnown = previousValueKnown; + this.previousEncodedValue = previousEncodedValue == null ? null + : nonEmpty(previousEncodedValue, "previousEncodedValue"); } } @@ -1218,8 +1613,19 @@ private void populateLeaves(Map target) { } private BytesKey materializedPath(Node node) { + BytesKey bound = node.materializedPath(); + if (bound != null) { + return bound; + } BytesKey path = materializedNodes.get(node); - return path != null || parent == null ? path : parent.materializedPath(node); + if (path == null && parent != null) { + path = parent.materializedPath(node); + } + return path == null ? null : node.bindMaterializedPath(path); + } + + byte[] rootHash() { + return Arrays.copyOf(rootHash, rootHash.length); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateAsyncPrepareHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateAsyncPrepareHead.java new file mode 100644 index 00000000000..d261998c76b --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateAsyncPrepareHead.java @@ -0,0 +1,200 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +/** + * Benchmark-only ordered worker that removes PathState preparation from the PushBlock thread. + * + *

The queue is bounded and enqueue blocks when the worker cannot sustain the input rate. This + * owner deliberately returns no Snapshot delta, so it must not cross a Snapshot flush boundary. + */ +@Slf4j(topic = "DB") +public final class PathStateAsyncPrepareHead implements PathStateHead { + + private static final int DEFAULT_QUEUE_CAPACITY = 64; + + private final PathStateHead delegate; + private final BlockingQueue queue; + private final Thread worker; + private Work pending; + private volatile PathStateRootMetadata completed; + private volatile Throwable failure; + private volatile boolean closed; + + public PathStateAsyncPrepareHead(PathStateHead delegate) throws IOException { + this(delegate, DEFAULT_QUEUE_CAPACITY); + } + + PathStateAsyncPrepareHead(PathStateHead delegate, int queueCapacity) throws IOException { + this.delegate = Objects.requireNonNull(delegate, "delegate"); + if (queueCapacity <= 0) { + throw new IllegalArgumentException("async PathState queue capacity must be positive"); + } + queue = new ArrayBlockingQueue<>(queueCapacity); + completed = delegate.getHead(); + worker = new Thread(this::run, "path-state-async-prepare"); + worker.setDaemon(true); + worker.start(); + } + + @Override + public synchronized PathStateSnapshotDelta prepareSnapshotDelta(BlockSnapshotMeta meta, + PathStateBlockTransition transition) throws IOException { + requireHealthy(); + if (pending != null) { + throw new IOException("async PathState transition is already pending enqueue"); + } + pending = new Work(Objects.requireNonNull(meta, "meta"), + Objects.requireNonNull(transition, "transition")); + return null; + } + + @Override + public synchronized PathStateRootMetadata advance(PathStateBlockTransition transition) + throws IOException { + requireHealthy(); + if (pending == null || pending.transition != Objects.requireNonNull(transition, "transition")) { + throw new IOException("async PathState publication differs from captured transition"); + } + Work admitted = pending; + pending = null; + long startedNanos = System.nanoTime(); + try { + queue.put(admitted); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("async PathState enqueue interrupted", interrupted); + } + logger.info("Path-state async enqueued: head={}, queueDepth={}, enqueueMicros={}", + transition.getBlockNumber(), queue.size(), + TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startedNanos)); + return copy(completed); + } + + @Override + public synchronized PathStateRootMetadata rewindTo(long blockNumber, byte[] blockHash) + throws IOException { + requireHealthy(); + if (pending != null || !queue.isEmpty()) { + throw new IOException("async PathState rewind requires an empty benchmark queue"); + } + completed = delegate.rewindTo(blockNumber, blockHash); + return copy(completed); + } + + @Override + public PathStateRootMetadata flushBaseThrough(long blockNumber, byte[] blockHash) + throws IOException { + awaitThrough(blockNumber, blockHash); + return delegate.flushBaseThrough(blockNumber, blockHash); + } + + @Override + public synchronized byte[] preview(PathStateBlockTransition transition) { + return null; + } + + @Override + public synchronized PathStateRootMetadata getHead() throws IOException { + requireHealthy(); + return copy(completed); + } + + private void awaitThrough(long blockNumber, byte[] blockHash) throws IOException { + byte[] expectedHash = Arrays.copyOf(Objects.requireNonNull(blockHash, "blockHash"), + blockHash.length); + long deadline = System.nanoTime() + TimeUnit.MINUTES.toNanos(5); + while (true) { + requireHealthy(); + PathStateRootMetadata current = completed; + if (current.getBlockNumber() == blockNumber + && Arrays.equals(current.getBlockHash(), expectedHash)) { + return; + } + if (current.getBlockNumber() > blockNumber || System.nanoTime() >= deadline) { + throw new IOException("async PathState worker did not reach requested flush target"); + } + try { + Thread.sleep(10L); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("async PathState wait interrupted", interrupted); + } + } + } + + private void run() { + while (!closed || !queue.isEmpty()) { + try { + Work work = queue.poll(100L, TimeUnit.MILLISECONDS); + if (work == null) { + continue; + } + long startedNanos = System.nanoTime(); + delegate.prepareSnapshotDelta(work.meta, work.transition); + PathStateRootMetadata advanced = delegate.advance(work.transition); + completed = advanced; + logger.info("Path-state async prepared: head={}, queueDepth={}, serviceMs={}", + advanced.getBlockNumber(), queue.size(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedNanos)); + } catch (InterruptedException interrupted) { + if (!closed) { + failure = interrupted; + Thread.currentThread().interrupt(); + } + return; + } catch (IOException | RuntimeException currentFailure) { + failure = currentFailure; + logger.error("Path-state async worker failed", currentFailure); + return; + } + } + } + + private void requireHealthy() throws IOException { + if (closed) { + throw new IOException("async PathState owner is closed"); + } + if (failure != null) { + throw new IOException("async PathState worker failed", failure); + } + } + + @Override + public void close() throws IOException { + closed = true; + try { + worker.join(TimeUnit.SECONDS.toMillis(30)); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("async PathState close interrupted", interrupted); + } finally { + if (worker.isAlive()) { + worker.interrupt(); + } + delegate.close(); + } + } + + private static PathStateRootMetadata copy(PathStateRootMetadata metadata) { + return PathStateRootMetadata.decode(metadata.encode()); + } + + private static final class Work { + + private final BlockSnapshotMeta meta; + private final PathStateBlockTransition transition; + + private Work(BlockSnapshotMeta meta, PathStateBlockTransition transition) { + this.meta = meta; + this.transition = transition; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBaseCompaction.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBaseCompaction.java index 64fa01331da..9bc593c9e80 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBaseCompaction.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBaseCompaction.java @@ -134,7 +134,7 @@ private void buildNext(PathStateRootMetadata replacement) throws IOException { try (PathStateNodeStoreSet source = PathStateNodeStoreSet.openPublished(manifest, layer); PathStateNodeStoreSet destination = PathStateNodeStoreSet.beginBaseAt(manifest, nextPath)) { PathStateRoot sourceRoot = source.createRoot(); - sourceRoot.verifyNodeStores(); + sourceRoot.verifyAllNodeStores(); List leaves = new ArrayList<>(source.leafRecords()); PathStateRoot destinationRoot = destination.initializeBase(leaves, layer.getStateRoot()); if (!Arrays.equals(destinationRoot.rootHash(), replacement.getStateRoot())) { @@ -155,7 +155,7 @@ private void verifyPreparedNext(PathStateRootMetadata replacement) throws IOExce if (!Arrays.equals(restored.rootHash(), replacement.getStateRoot())) { throw new IOException("path-state prepared BASE root mismatch"); } - restored.verifyNodeStores(); + restored.verifyAllNodeStores(); } } @@ -167,7 +167,7 @@ private void verifyInstalledBase(PathStateRootMetadata replacement) throws IOExc if (!Arrays.equals(restored.rootHash(), replacement.getStateRoot())) { throw new IOException("path-state installed BASE root mismatch"); } - restored.verifyNodeStores(); + restored.verifyAllNodeStores(); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java index 0c0d724aa77..d94dd0589d1 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java @@ -48,9 +48,16 @@ public final class PathStateBlockTransition { private final P66Phase phase; private final List mutations; private final byte[] payloadDigest; + private final byte[] mutationViewDigest; public PathStateBlockTransition(long blockNumber, byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, Collection mutations) { + this(blockNumber, blockHash, parentHash, timestamp, phase, mutations, null); + } + + public PathStateBlockTransition(long blockNumber, byte[] blockHash, byte[] parentHash, + long timestamp, P66Phase phase, Collection mutations, + byte[] mutationViewDigest) { if (blockNumber < 0) { throw new IllegalArgumentException("blockNumber must not be negative"); } @@ -66,6 +73,9 @@ public PathStateBlockTransition(long blockNumber, byte[] blockHash, byte[] paren } this.mutations = Collections.unmodifiableList(canonical); this.payloadDigest = sha256(encode(prepared)); + this.mutationViewDigest = mutationViewDigest == null + ? Arrays.copyOf(payloadDigest, payloadDigest.length) + : copyHash(mutationViewDigest, "mutationViewDigest"); } public long getBlockNumber() { @@ -100,6 +110,10 @@ public byte[] getPayloadDigest() { return Arrays.copyOf(payloadDigest, payloadDigest.length); } + public byte[] getMutationViewDigest() { + return Arrays.copyOf(mutationViewDigest, mutationViewDigest.length); + } + private List prepare(Collection supplied) { PathStateParticipantDescriptor descriptor = PathStateParticipantDescriptor.current(); List prepared = new ArrayList<>(); @@ -152,10 +166,12 @@ private byte[] encode(List prepared) { } private static PathStateMutation copyMutation(PathStateMutation mutation) { - return mutation.isDelete() + PathStateMutation copy = mutation.isDelete() ? PathStateMutation.delete(mutation.getDbName(), mutation.getCanonicalKey()) : PathStateMutation.put(mutation.getDbName(), mutation.getCanonicalKey(), mutation.getCanonicalValue()); + return mutation.isPreviousValueKnown() + ? copy.withPreviousPhysicalValue(mutation.getPreviousPhysicalValue()) : copy; } private static byte[] copyHash(byte[] value, String name) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java new file mode 100644 index 00000000000..e6a29c88f58 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java @@ -0,0 +1,273 @@ +package org.tron.core.db2.stateroot; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.core.CommonCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointTarget; + +/** Next-format PathState participant for the common-checkpoint two-barrier protocol. */ +public final class PathStateCheckpointMaterializer implements CommonCheckpointMaterializer { + + static final String CURRENT_FILE = "CURRENT"; + static final String MATERIALIZED_DIRECTORY = "checkpoint-materialized"; + private static final int MAGIC = 0x50534354; // PSCT + private static final short VERSION = 1; + private static final int DIGEST_LENGTH = 32; + private static final int RECORD_LENGTH = Integer.BYTES + Short.BYTES + Short.BYTES + + 4 * DIGEST_LENGTH + 2 * Long.BYTES + DIGEST_LENGTH; + + private final PathStatePhysicalStoreSet stores; + private final PathStateParticipantScope scope; + private final Path directory; + private final byte[] formatIdentity; + private final FaultHook faultHook; + + public PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, + PathStateParticipantScope scope, byte[] formatIdentity) { + this(stores, scope, formatIdentity, (stage, storeId) -> { }); + } + + PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, + PathStateParticipantScope scope, byte[] formatIdentity, FaultHook faultHook) { + this.stores = Objects.requireNonNull(stores, "stores"); + this.scope = Objects.requireNonNull(scope, "scope"); + this.directory = stores.getDirectory(); + this.formatIdentity = digest(formatIdentity, "formatIdentity"); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + @Override + public Authority authority() { + return Authority.PATH_STATE; + } + + @Override + public synchronized Status inspect(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = requireTarget(target); + byte[] expected = encode(admitted); + Path currentPath = directory.resolve(CURRENT_FILE); + if (Files.exists(currentPath, LinkOption.NOFOLLOW_LINKS)) { + Marker current = load(currentPath); + if (Arrays.equals(current.encoded, expected)) { + requireMaterialized(admitted, expected); + return Status.PUBLISHED; + } + requireParent(current, admitted); + } + Path materialized = materializedPath(admitted); + if (!Files.exists(materialized, LinkOption.NOFOLLOW_LINKS)) { + return Status.NEEDS_MATERIALIZATION; + } + requireExact(materialized, expected); + return Status.MATERIALIZED; + } + + @Override + public synchronized void materialize(CommonCheckpointPayload payload, + CommonCheckpointTarget target) throws IOException { + CommonCheckpointPayload admittedPayload = Objects.requireNonNull(payload, "payload"); + CommonCheckpointTarget admittedTarget = requireTarget(target); + if (!admittedTarget.equals(CommonCheckpointTarget.from(admittedPayload))) { + throw new IOException("PathState checkpoint payload and target differ"); + } + Status status = inspect(admittedTarget); + if (status == Status.PUBLISHED || status == Status.MATERIALIZED) { + return; + } + byte[] marker = marker(admittedTarget); + Set seen = new HashSet<>(); + for (CommonCheckpointPayload.PathStoreTarget pathStore + : admittedPayload.getPathStores()) { + PathStateParticipant participant = scope.require(pathStore.getDbName()); + if (participant.getStoreId() != pathStore.getStoreId() + || !seen.add(pathStore.getStoreId())) { + throw new IOException("PathState checkpoint participant identity differs"); + } + PathStatePhysicalStoreSet.PhysicalStore store = stores.participant(pathStore.getDbName()); + if (!Arrays.equals(marker, store.checkpointTargetMarker())) { + store.applyCheckpointParticipant(pathStore, marker); + faultHook.after(Stage.AFTER_PARTICIPANT_BATCH, pathStore.getStoreId()); + } + } + PathStatePhysicalStoreSet.PhysicalStore superStore = stores.superStore(); + if (!Arrays.equals(marker, superStore.checkpointTargetMarker())) { + superStore.applyCheckpointSuper(admittedPayload.getSuperNodeMutations(), marker); + faultHook.after(Stage.AFTER_SUPER_BATCH, 0); + } + byte[] encoded = encode(admittedTarget); + PathStateMetadataFile.publishImmutableBytes(materializedPath(admittedTarget), encoded); + faultHook.after(Stage.AFTER_MATERIALIZED_TARGET, 0); + } + + @Override + public synchronized void publish(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = requireTarget(target); + Status status = inspect(admitted); + if (status == Status.PUBLISHED) { + return; + } + if (status != Status.MATERIALIZED) { + throw new IOException("PathState checkpoint target is not fully materialized"); + } + PathStateMetadataFile.replaceCurrentBytes(directory.resolve(CURRENT_FILE), encode(admitted)); + faultHook.after(Stage.AFTER_CURRENT, 0); + } + + private CommonCheckpointTarget requireTarget(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + if (!Arrays.equals(formatIdentity, admitted.getFormatIdentity())) { + throw new IOException("PathState checkpoint format identity differs"); + } + return admitted; + } + + private void requireMaterialized(CommonCheckpointTarget target, byte[] expected) + throws IOException { + requireExact(materializedPath(target), expected); + } + + private void requireParent(Marker current, CommonCheckpointTarget target) throws IOException { + BlockSnapshotMeta first = target.getFirstBlock(); + if (!Arrays.equals(current.formatIdentity, target.getFormatIdentity()) + || current.lastEpoch + 1 != first.getEpoch() + || current.lastBlockNumber + 1 != first.getBlockNumber() + || !Arrays.equals(current.lastBlockHash, first.getParentHash()) + || !Arrays.equals(current.stateRoot, target.getParentStateRoot())) { + throw new IOException("PathState CURRENT is not the checkpoint parent target"); + } + } + + private Path materializedPath(CommonCheckpointTarget target) { + return directory.resolve(MATERIALIZED_DIRECTORY).resolve(hex(target.getPayloadDigest())); + } + + private static byte[] marker(CommonCheckpointTarget target) { + byte[] marker = new byte[2 * DIGEST_LENGTH]; + System.arraycopy(target.getPayloadDigest(), 0, marker, 0, DIGEST_LENGTH); + System.arraycopy(target.getStateRoot(), 0, marker, DIGEST_LENGTH, DIGEST_LENGTH); + return marker; + } + + private static byte[] encode(CommonCheckpointTarget target) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(RECORD_LENGTH); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.write(target.getFormatIdentity()); + output.write(target.getPayloadDigest()); + output.writeLong(target.getLastBlock().getEpoch()); + output.writeLong(target.getLastBlock().getBlockNumber()); + output.write(target.getLastBlock().getBlockHash()); + output.write(target.getStateRoot()); + output.flush(); + byte[] body = bytes.toByteArray(); + output.write(Hashing.sha256().hashBytes(body).asBytes()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory PathState target encoding failed", impossible); + } + } + + private static Marker load(Path path) throws IOException { + byte[] encoded = PathStateMetadataFile.loadImmutableBytes(path, RECORD_LENGTH); + if (encoded.length != RECORD_LENGTH) { + throw new IOException("PathState checkpoint target length is invalid"); + } + int bodyLength = encoded.length - DIGEST_LENGTH; + byte[] body = Arrays.copyOf(encoded, bodyLength); + byte[] checksum = Arrays.copyOfRange(encoded, bodyLength, encoded.length); + if (!Arrays.equals(checksum, Hashing.sha256().hashBytes(body).asBytes())) { + throw new IOException("PathState checkpoint target checksum differs"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(body))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0) { + throw new IOException("PathState checkpoint target format is unsupported"); + } + return new Marker(encoded, readDigest(input), readDigest(input), input.readLong(), + input.readLong(), readDigest(input), readDigest(input)); + } catch (EOFException truncated) { + throw new IOException("PathState checkpoint target is truncated", truncated); + } + } + + private static void requireExact(Path path, byte[] expected) throws IOException { + Marker actual = load(path); + if (!Arrays.equals(actual.encoded, expected)) { + throw new IOException("PathState checkpoint target identity differs"); + } + } + + private static byte[] readDigest(DataInputStream input) throws IOException { + byte[] value = new byte[DIGEST_LENGTH]; + input.readFully(value); + return value; + } + + private static byte[] digest(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length != DIGEST_LENGTH) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return copy; + } + + private static String hex(byte[] value) { + StringBuilder encoded = new StringBuilder(value.length * 2); + for (byte current : value) { + encoded.append(Character.forDigit(current >>> 4 & 0xf, 16)); + encoded.append(Character.forDigit(current & 0xf, 16)); + } + return encoded.toString(); + } + + enum Stage { + AFTER_PARTICIPANT_BATCH, + AFTER_SUPER_BATCH, + AFTER_MATERIALIZED_TARGET, + AFTER_CURRENT + } + + @FunctionalInterface + interface FaultHook { + void after(Stage stage, int storeId) throws IOException; + } + + private static final class Marker { + + private final byte[] encoded; + private final byte[] formatIdentity; + private final byte[] payloadDigest; + private final long lastEpoch; + private final long lastBlockNumber; + private final byte[] lastBlockHash; + private final byte[] stateRoot; + + private Marker(byte[] encoded, byte[] formatIdentity, byte[] payloadDigest, long lastEpoch, + long lastBlockNumber, byte[] lastBlockHash, byte[] stateRoot) { + this.encoded = encoded; + this.formatIdentity = formatIdentity; + this.payloadDigest = payloadDigest; + this.lastEpoch = lastEpoch; + this.lastBlockNumber = lastBlockNumber; + this.lastBlockHash = lastBlockHash; + this.stateRoot = stateRoot; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java index 8b1161e5300..4515e634980 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCurrentStore.java @@ -242,8 +242,7 @@ private void verifyTargetState(PathStateRootMetadata target) throws IOException throw new IOException("path-state canonical switch target has invalid native progress"); } try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openPublished(manifest, target)) { - PathStateRoot root = stores.createRoot(); - root.verifyNodeStores(); + stores.createRoot(); } catch (IllegalArgumentException | IllegalStateException e) { throw new IOException("path-state canonical switch target is corrupt", e); } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateFlushTarget.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateFlushTarget.java new file mode 100644 index 00000000000..43cda5f0597 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateFlushTarget.java @@ -0,0 +1,288 @@ +package org.tron.core.db2.stateroot; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +/** Immutable deterministic coalescing target for one consecutive Snapshot flush range. */ +public final class PathStateFlushTarget { + + private static final Comparator MUTATION_ORDER = + (left, right) -> compareUnsigned(left.getKey(), right.getKey()); + + private final List blocks; + private final byte[] parentStateRoot; + private final byte[] stateRoot; + private final List stores; + private final List superNodeMutations; + private final long mutationBytes; + + private PathStateFlushTarget(List blocks, byte[] parentStateRoot, + byte[] stateRoot, List stores, + List superNodeMutations) { + this.blocks = Collections.unmodifiableList(new ArrayList<>(blocks)); + this.parentStateRoot = copy(parentStateRoot); + this.stateRoot = copy(stateRoot); + this.stores = Collections.unmodifiableList(new ArrayList<>(stores)); + this.superNodeMutations = immutableMutations(superNodeMutations); + this.mutationBytes = mutationBytes(this.stores, this.superNodeMutations); + } + + /** Validates and coalesces a non-empty oldest-to-newest path-state Snapshot delta chain. */ + public static PathStateFlushTarget coalesce(List supplied) { + List deltas = new ArrayList<>(Objects.requireNonNull(supplied, + "deltas")); + if (deltas.isEmpty()) { + throw new IllegalArgumentException("path-state flush delta chain must not be empty"); + } + + List blocks = new ArrayList<>(); + Map stores = new LinkedHashMap<>(); + Map storeNames = new LinkedHashMap<>(); + Map superNodes = new LinkedHashMap<>(); + PathStateSnapshotDelta previous = null; + for (PathStateSnapshotDelta candidate : deltas) { + PathStateSnapshotDelta delta = Objects.requireNonNull(candidate, "delta"); + if (previous != null) { + requireChild(previous, delta); + } + blocks.add(new BlockBinding(delta)); + for (PathStateSnapshotDelta.StoreDelta store : delta.getStores()) { + Integer priorId = storeNames.putIfAbsent(store.getDbName(), store.getStoreId()); + if (priorId != null && priorId != store.getStoreId()) { + throw new IllegalArgumentException("path-state flush Store name changes identity"); + } + StoreAccumulator accumulator = stores.computeIfAbsent(store.getStoreId(), ignored -> + new StoreAccumulator(store.getStoreId(), store.getDbName())); + accumulator.add(store); + } + putAll(superNodes, delta.getSuperNodeMutations()); + previous = delta; + } + + List storeTargets = new ArrayList<>(); + stores.values().stream().sorted(Comparator.comparingInt(store -> store.storeId)) + .forEach(store -> storeTargets.add(store.freeze())); + PathStateSnapshotDelta first = deltas.get(0); + PathStateSnapshotDelta last = deltas.get(deltas.size() - 1); + return new PathStateFlushTarget(blocks, first.getParentStateRoot(), last.getStateRoot(), + storeTargets, new ArrayList<>(superNodes.values())); + } + + public List getBlocks() { + return blocks; + } + + public byte[] getParentStateRoot() { + return copy(parentStateRoot); + } + + public byte[] getStateRoot() { + return copy(stateRoot); + } + + public List getStores() { + return stores; + } + + public List getSuperNodeMutations() { + return superNodeMutations; + } + + public long getMutationBytes() { + return mutationBytes; + } + + private static void requireChild(PathStateSnapshotDelta parent, + PathStateSnapshotDelta child) { + BlockSnapshotMeta parentMeta = parent.getMeta(); + BlockSnapshotMeta childMeta = child.getMeta(); + if (childMeta.getEpoch() != parentMeta.getEpoch() + 1 + || childMeta.getBlockNumber() != parentMeta.getBlockNumber() + 1 + || !Arrays.equals(childMeta.getParentHash(), parentMeta.getBlockHash()) + || !Arrays.equals(child.getParentStateRoot(), parent.getStateRoot())) { + throw new IllegalArgumentException("path-state flush delta chain is not consecutive"); + } + } + + private static void putAll(Map target, + List mutations) { + for (PathStateSnapshotDelta.Mutation mutation : mutations) { + byte[] key = mutation.getKey(); + target.put(new BytesKey(key), new PathStateSnapshotDelta.Mutation(key, + mutation.getValue())); + } + } + + private static List immutableMutations( + List mutations) { + List copy = new ArrayList<>(mutations); + copy.sort(MUTATION_ORDER); + return Collections.unmodifiableList(copy); + } + + private static long mutationBytes(List stores, + List superNodes) { + long bytes = mutationsBytes(superNodes); + for (StoreTarget store : stores) { + bytes = Math.addExact(bytes, mutationsBytes(store.flatMutations)); + bytes = Math.addExact(bytes, mutationsBytes(store.nodeMutations)); + } + return bytes; + } + + private static long mutationsBytes(List mutations) { + long bytes = 0; + for (PathStateSnapshotDelta.Mutation mutation : mutations) { + bytes = Math.addExact(bytes, mutation.getKey().length); + byte[] value = mutation.getValue(); + if (value != null) { + bytes = Math.addExact(bytes, value.length); + } + } + return bytes; + } + + private static byte[] copy(byte[] value) { + return Arrays.copyOf(Objects.requireNonNull(value, "value"), value.length); + } + + private static int compareUnsigned(byte[] left, byte[] right) { + for (int index = 0; index < Math.min(left.length, right.length); index++) { + int compared = Integer.compare(left[index] & 0xff, right[index] & 0xff); + if (compared != 0) { + return compared; + } + } + return Integer.compare(left.length, right.length); + } + + /** Per-block chain and payload identity retained even though forward mutations are coalesced. */ + public static final class BlockBinding { + + private final BlockSnapshotMeta meta; + private final byte[] parentStateRoot; + private final byte[] stateRoot; + private final byte[] transitionPayloadDigest; + private final byte[] mutationViewDigest; + + private BlockBinding(PathStateSnapshotDelta delta) { + this.meta = delta.getMeta(); + this.parentStateRoot = delta.getParentStateRoot(); + this.stateRoot = delta.getStateRoot(); + this.transitionPayloadDigest = delta.getTransitionPayloadDigest(); + this.mutationViewDigest = delta.getMutationViewDigest(); + } + + public BlockSnapshotMeta getMeta() { + return meta; + } + + public byte[] getParentStateRoot() { + return copy(parentStateRoot); + } + + public byte[] getStateRoot() { + return copy(stateRoot); + } + + public byte[] getTransitionPayloadDigest() { + return copy(transitionPayloadDigest); + } + + public byte[] getMutationViewDigest() { + return copy(mutationViewDigest); + } + } + + /** Final target for one participant changed anywhere in the coalesced range. */ + public static final class StoreTarget { + + private final int storeId; + private final String dbName; + private final byte[] storeRoot; + private final List flatMutations; + private final List nodeMutations; + + private StoreTarget(StoreAccumulator accumulator) { + this.storeId = accumulator.storeId; + this.dbName = accumulator.dbName; + this.storeRoot = copy(accumulator.storeRoot); + this.flatMutations = immutableMutations(new ArrayList<>(accumulator.flat.values())); + this.nodeMutations = immutableMutations(new ArrayList<>(accumulator.nodes.values())); + } + + public int getStoreId() { + return storeId; + } + + public String getDbName() { + return dbName; + } + + public byte[] getStoreRoot() { + return copy(storeRoot); + } + + public List getFlatMutations() { + return flatMutations; + } + + public List getNodeMutations() { + return nodeMutations; + } + } + + private static final class StoreAccumulator { + + private final int storeId; + private final String dbName; + private final Map flat = new LinkedHashMap<>(); + private final Map nodes = new LinkedHashMap<>(); + private byte[] storeRoot; + + private StoreAccumulator(int storeId, String dbName) { + this.storeId = storeId; + this.dbName = dbName; + } + + private void add(PathStateSnapshotDelta.StoreDelta store) { + if (!dbName.equals(store.getDbName())) { + throw new IllegalArgumentException("path-state flush Store ID changes identity"); + } + putAll(flat, store.getFlatMutations()); + putAll(nodes, store.getNodeMutations()); + storeRoot = store.getStoreRoot(); + } + + private StoreTarget freeze() { + return new StoreTarget(this); + } + } + + private static final class BytesKey { + + private final byte[] bytes; + + private BytesKey(byte[] bytes) { + this.bytes = copy(bytes); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof BytesKey + && Arrays.equals(bytes, ((BytesKey) other).bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateHead.java index 625594764de..7c81ddd2add 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateHead.java @@ -2,6 +2,7 @@ import java.io.Closeable; import java.io.IOException; +import org.tron.core.db2.archive.BlockSnapshotMeta; /** Runtime-owned current path-state authority used by Manager lifecycle integration. */ public interface PathStateHead extends Closeable { @@ -15,6 +16,12 @@ public interface PathStateHead extends Closeable { /** Computes the child state root without publishing or adopting it. */ byte[] preview(PathStateBlockTransition transition) throws IOException; + /** Prepares an optional Snapshot-owned forward delta without publishing durable state. */ + default PathStateSnapshotDelta prepareSnapshotDelta(BlockSnapshotMeta meta, + PathStateBlockTransition transition) throws IOException { + return null; + } + PathStateRootMetadata getHead() throws IOException; @Override diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java index b9e83048cdd..b0938c810d2 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateLayerPublication.java @@ -149,8 +149,7 @@ private void verifyCurrentProgress() throws IOException { } requireSame(current, progress, "path-state CURRENT and native progress differ"); try (PathStateNodeStoreSet stores = PathStateNodeStoreSet.openPublished(manifest, current)) { - PathStateRoot root = stores.createRoot(); - root.verifyNodeStores(); + stores.createRoot(); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java index 1c5b557e64a..b92dc148502 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateMutation.java @@ -9,12 +9,22 @@ public final class PathStateMutation { private final String dbName; private final byte[] physicalKey; private final byte[] physicalValue; + private final boolean previousValueKnown; + private final byte[] previousPhysicalValue; private PathStateMutation(String dbName, byte[] physicalKey, byte[] physicalValue) { + this(dbName, physicalKey, physicalValue, false, null); + } + + private PathStateMutation(String dbName, byte[] physicalKey, byte[] physicalValue, + boolean previousValueKnown, byte[] previousPhysicalValue) { this.dbName = Objects.requireNonNull(dbName, "dbName"); this.physicalKey = copy(physicalKey, "physicalKey"); this.physicalValue = physicalValue == null ? null : Arrays.copyOf(physicalValue, physicalValue.length); + this.previousValueKnown = previousValueKnown; + this.previousPhysicalValue = previousPhysicalValue == null ? null + : Arrays.copyOf(previousPhysicalValue, previousPhysicalValue.length); } public static PathStateMutation put(String dbName, byte[] physicalKey, byte[] physicalValue) { @@ -26,6 +36,11 @@ public static PathStateMutation delete(String dbName, byte[] physicalKey) { return new PathStateMutation(dbName, physicalKey, null); } + /** Returns an immutable copy carrying the block pre-state value, or known absence. */ + public PathStateMutation withPreviousPhysicalValue(byte[] previousValue) { + return new PathStateMutation(dbName, physicalKey, physicalValue, true, previousValue); + } + public String getDbName() { return dbName; } @@ -50,6 +65,20 @@ public boolean isDelete() { return physicalValue == null; } + /** Whether the block snapshot supplied an authoritative pre-state value for this key. */ + public boolean isPreviousValueKnown() { + return previousValueKnown; + } + + /** Exact pre-state bytes, or {@code null} when the authoritative pre-state is absent. */ + public byte[] getPreviousPhysicalValue() { + if (!previousValueKnown) { + throw new IllegalStateException("previous physical value is unknown"); + } + return previousPhysicalValue == null ? null + : Arrays.copyOf(previousPhysicalValue, previousPhysicalValue.length); + } + /** @deprecated Use {@link #getPhysicalValue()}; this alias is retained for old-format callers. */ @Deprecated public byte[] getCanonicalValue() { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java index 1db5ad34c91..347ec037e5b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNodeStoreSet.java @@ -16,6 +16,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; import org.tron.core.db2.stateroot.PathStateRootMetadata.Kind; /** Exact-27 participant and super-trie namespace views over one BASE or LAYER native database. */ @@ -64,6 +65,7 @@ public final class PathStateNodeStoreSet implements Closeable { private final PathStateNodeStoreSet parentStores; private final PathNodeStore superStore; private final byte[] manifestDigest; + private final AtomicLong nodeStoreGetCalls = new AtomicLong(); private final Kind kind; private final PathStateRootMetadata expectedMetadata; private final boolean sealed; @@ -310,6 +312,7 @@ synchronized PathStateRoot createRootFrom(PreparedPathStateTransition prepared) throw new IllegalStateException("path-state layer has no parent node overlay"); } PreparedPathStateTransition candidate = Objects.requireNonNull(prepared, "prepared"); + candidate.validatePreparedTransition(scope); PathStateRoot next = PathStateRoot.fromSnapshot(scope, participant -> participantStores.get(participant.getDbName()), superStore, candidate.getSnapshot()); @@ -325,12 +328,15 @@ synchronized PathStateRoot createRootFrom(PreparedPathStateTransition prepared) store.put(mutation.getPath(), encoded); } } - next.verifyNodeStores(); root = next; rootClaimed = true; return root; } + long nodeStoreGetCalls() { + return nodeStoreGetCalls.get(); + } + synchronized List leafRecords() { requireOpen(); if (root == null) { @@ -1186,6 +1192,7 @@ private NamespacedNodeStore(PathStateNodeStoreSet owner, int storeId) { @Override public byte[] get(byte[] path) { + owner.nodeStoreGetCalls.incrementAndGet(); return owner.get(key(path)); } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java new file mode 100644 index 00000000000..068cea3ff49 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java @@ -0,0 +1,565 @@ +package org.tron.core.db2.stateroot; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** + * Benchmark-only PathState head that reads one durable physical base and advances in memory. + * + *

No transition method writes F/N/M, INTENT, CURRENT, or a reverse journal. The durable base is + * intentionally unchanged until the common-checkpoint flush path is installed. + */ +@Slf4j(topic = "DB") +public final class PathStatePhysicalOverlayHead implements PathStateHead { + + private static final byte[] ABSENT = new byte[0]; + static final int DEFAULT_PARTICIPANT_THREADS = 4; + static final int DEFAULT_BRANCH_THREADS = 8; + + private final PathStatePhysicalStoreSet stores; + private final PathStateParticipantScope scope; + private final byte[] formatDigest; + private final int maxHistory; + private final ExecutorService participantExecutor; + private final ExecutorService branchExecutor; + private final List history = new ArrayList<>(); + private PathStateRootMetadata head; + private PathStateRoot.Snapshot snapshot; + private PreparedOverlay pending; + private boolean failed; + private boolean closed; + + private PathStatePhysicalOverlayHead(PathStatePhysicalStoreSet stores, + PathStateRootMetadata head, PathStateRoot.Snapshot snapshot, int maxHistory, + int participantThreads, int branchThreads) { + this.stores = Objects.requireNonNull(stores, "stores"); + this.scope = stores.participantScope(); + this.formatDigest = stores.getFormatDigest(); + this.head = Objects.requireNonNull(head, "head"); + this.snapshot = Objects.requireNonNull(snapshot, "snapshot"); + this.maxHistory = maxHistory; + participantExecutor = newExecutor(participantThreads, "participant"); + branchExecutor = newExecutor(branchThreads, "branch"); + } + + /** Opens the physical CURRENT as a read-only base for a volatile benchmark overlay. */ + public static PathStatePhysicalOverlayHead open(Path directory, Engine engine, + PathStateLayerLimits limits) throws IOException { + return open(directory, engine, limits, PathStatePhysicalStoreSet.STEADY_NODE_CACHE_BYTES); + } + + /** Opens a benchmark overlay with an explicit shared resident-node cache budget. */ + public static PathStatePhysicalOverlayHead open(Path directory, Engine engine, + PathStateLayerLimits limits, long residentNodeCacheBytes) throws IOException { + return open(directory, engine, limits, residentNodeCacheBytes, + DEFAULT_PARTICIPANT_THREADS, DEFAULT_BRANCH_THREADS); + } + + /** Opens a benchmark overlay with explicit cache and bounded prepare worker budgets. */ + public static PathStatePhysicalOverlayHead open(Path directory, Engine engine, + PathStateLayerLimits limits, long residentNodeCacheBytes, int participantThreads, + int branchThreads) throws IOException { + requireThreadCount(participantThreads, "participantThreads"); + requireThreadCount(branchThreads, "branchThreads"); + PathStatePhysicalStoreSet opened = PathStatePhysicalStoreSet.openExisting(directory, + new PathStateCanonicalizer().participantScope(), engine, residentNodeCacheBytes); + try { + opened.recoverPublication(); + PathStateRootMetadata current = opened.currentMetadata(); + PathStateRoot root = opened.createRoot(); + root.restoreStoredRoots(current.getStateRoot()); + PathStateRoot.Snapshot restored = root.snapshot(); + if (!Arrays.equals(restored.getStateRoot(), current.getStateRoot())) { + throw new IOException("path-state benchmark overlay root mismatch"); + } + return new PathStatePhysicalOverlayHead(opened, current, restored, + Objects.requireNonNull(limits, "limits").getMaxLayers(), participantThreads, + branchThreads); + } catch (IOException | RuntimeException failure) { + try { + opened.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } + } + + @Override + public synchronized PathStateRootMetadata advance(PathStateBlockTransition transition) + throws IOException { + requireHealthy(); + PathStateBlockTransition admitted = Objects.requireNonNull(transition, "transition"); + if (pending == null || pending.transition != admitted) { + throw new IOException("path-state benchmark publication differs from prepared transition"); + } + history.add(new HeadState(head, snapshot)); + while (history.size() > maxHistory) { + history.remove(0); + } + head = pending.metadata; + snapshot = pending.snapshot; + logger.info("Path-state volatile overlay advanced: head={}, mutations={}, " + + "nodeMutations={}, nativeNodeReads={}, cacheBytes={}, cacheEntries={}, " + + "cacheEvictions={}, changedParticipants={}, maxParticipantMutations={}, " + + "authoritativePreviousValues={}, " + + "maxParticipantStoreId={}, maxParticipantMs={}, participantWorkMs={}, " + + "participantWallMs={}, prepareMs={}, trieMs={}, artifactMs={}, " + + "nodePlanWorkMs={}, nodeStoreWorkMs={}, nodeFinalizeWorkMs={}, " + + "nodePuts={}, nodeDeletes={}, nodeRlpBytes={}, nodeRlpFinalBytes={}, " + + "uniqueNodePaths={}, overwriteWrites={}, nodeCreates={}, nodeKeccaks={}, " + + "nodeDecodes={}, nodeHashVerifies={}, hashRefsCreated={}, hashRefsResolved={}, " + + "durableWrites=0, journal=0", + head.getBlockNumber(), admitted.getMutations().size(), pending.nodeMutations, + pending.nativeNodeReads, stores.residentNodeCacheBytes(), + stores.residentNodeCacheEntries(), stores.residentNodeCacheEvictions(), + pending.changedParticipants, pending.maxParticipantMutations, + pending.authoritativePreviousValues, + pending.maxParticipantStoreId, pending.maxParticipantMillis, + pending.participantWorkMillis, pending.participantWallMillis, + pending.prepareMillis, pending.trieMillis, pending.artifactMillis, + pending.nodePlanWorkMillis, pending.nodeStoreWorkMillis, + pending.nodeFinalizeWorkMillis, + pending.stats.nodePuts, pending.stats.nodeDeletes, pending.stats.nodeRlpBytes, + pending.stats.nodeRlpFinalBytes, pending.stats.uniqueNodePaths, + pending.stats.overwriteWrites(), pending.nodeCreates, pending.nodeKeccaks, + pending.nodeDecodes, pending.nodeHashVerifies, pending.hashRefsCreated, + pending.hashRefsResolved); + logger.info("Path-state artifact stores: head={}, perStore={}", + head.getBlockNumber(), pending.stats.perStore); + pending = null; + return copy(head); + } + + @Override + public synchronized PathStateRootMetadata rewindTo(long blockNumber, byte[] blockHash) + throws IOException { + requireHealthy(); + byte[] admittedHash = Arrays.copyOf(Objects.requireNonNull(blockHash, "blockHash"), + blockHash.length); + if (matches(head, blockNumber, admittedHash)) { + pending = null; + return copy(head); + } + for (int index = history.size() - 1; index >= 0; index--) { + HeadState candidate = history.get(index); + if (matches(candidate.metadata, blockNumber, admittedHash)) { + head = candidate.metadata; + snapshot = candidate.snapshot; + history.subList(index, history.size()).clear(); + pending = null; + return copy(head); + } + } + throw new IOException("path-state benchmark overlay ancestor is outside memory history"); + } + + @Override + public synchronized PathStateRootMetadata flushBaseThrough(long blockNumber, byte[] blockHash) + throws IOException { + requireHealthy(); + logger.info("Path-state volatile overlay skipped durable base flush: target={}, " + + "benchmarkOnly=true", blockNumber); + return copy(head); + } + + @Override + public synchronized byte[] preview(PathStateBlockTransition transition) throws IOException { + requireHealthy(); + return prepare(null, Objects.requireNonNull(transition, "transition")).metadata.getStateRoot(); + } + + @Override + public synchronized PathStateSnapshotDelta prepareSnapshotDelta(BlockSnapshotMeta meta, + PathStateBlockTransition transition) throws IOException { + requireHealthy(); + if (pending != null) { + throw new IOException("path-state benchmark transition is already prepared"); + } + pending = prepare(Objects.requireNonNull(meta, "meta"), + Objects.requireNonNull(transition, "transition")); + return pending.delta; + } + + @Override + public synchronized PathStateRootMetadata getHead() throws IOException { + requireHealthy(); + return copy(head); + } + + long durableWriteBatchCalls() { + long calls = stores.superStore().getWriteBatchCalls(); + for (PathStateParticipant participant : scope.getParticipants()) { + calls += stores.participant(participant.getDbName()).getWriteBatchCalls(); + } + return calls; + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + participantExecutor.shutdownNow(); + branchExecutor.shutdownNow(); + stores.close(); + } + + private PreparedOverlay prepare(BlockSnapshotMeta meta, PathStateBlockTransition transition) + throws IOException { + requireChild(transition); + long startedNanos = System.nanoTime(); + long nodeCreatesBefore = PathMerkleTrie.nodeCreateCountTotal(); + long nodeKeccaksBefore = PathMerkleTrie.nodeKeccakCountTotal(); + Map recordings = new LinkedHashMap<>(); + PathStateRoot candidate = PathStateRoot.fromSnapshot(scope, + participant -> recordings.computeIfAbsent(participant.getStoreId(), ignored -> + new RecordingStore(stores.participant(participant.getDbName()).nodeStore())), + recordings.computeIfAbsent(0, ignored -> + new RecordingStore(stores.superStore().nodeStore())), snapshot); + PathStateRoot.ParallelApplyStats parallelStats = transition.getMutations().isEmpty() ? null + : candidate.applyParallel(transition.getMutations(), participantExecutor, branchExecutor); + PathStateRoot.Snapshot nextSnapshot = candidate.snapshot(); + long trieNanos = System.nanoTime(); + Map> flatByStore = new LinkedHashMap<>(); + for (PathStateMutation mutation : transition.getMutations()) { + PathStateParticipant participant = scope.require(mutation.getDbName()); + byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), + mutation.getPhysicalKey()); + byte[] encodedValue = mutation.isDelete() ? null + : PathStateCommitmentCodec.presentLeafValue(mutation.getPhysicalValue()); + flatByStore.computeIfAbsent(participant.getStoreId(), ignored -> new ArrayList<>()) + .add(new PathStateSnapshotDelta.Mutation(secureKey, encodedValue)); + } + + List deltas = new ArrayList<>(); + int nodeMutations = 0; + for (PathStateParticipant participant : scope.getParticipants()) { + List flats = flatByStore.get(participant.getStoreId()); + if (flats == null) { + continue; + } + List nodes = recordings.get(participant.getStoreId()) + .mutations(); + nodeMutations += nodes.size(); + deltas.add(new PathStateSnapshotDelta.StoreDelta(participant, + nextSnapshot.participantRoot(participant.getDbName()), flats, nodes)); + } + List superNodes = recordings.get(0).mutations(); + nodeMutations += superNodes.size(); + PathStateRootMetadata metadata = PathStateRootMetadata.layer(transition.getBlockNumber(), + transition.getBlockHash(), transition.getParentHash(), transition.getTimestamp(), + transition.getPhase(), formatDigest, head.getStateRoot(), nextSnapshot.getStateRoot(), + transition.getPayloadDigest()); + PathStateSnapshotDelta delta = meta == null ? null : PathStateSnapshotDelta.fromPhysical(meta, + head, transition, nextSnapshot, deltas, superNodes); + long nativeReads = recordings.values().stream().mapToLong(RecordingStore::nativeReads).sum(); + RecordingStats stats = RecordingStats.collect(recordings); + long finishedNanos = System.nanoTime(); + long prepareMillis = TimeUnit.NANOSECONDS.toMillis(finishedNanos - startedNanos); + return new PreparedOverlay(transition, metadata, nextSnapshot, delta, nodeMutations, + nativeReads, parallelStats, prepareMillis, + TimeUnit.NANOSECONDS.toMillis(trieNanos - startedNanos), + TimeUnit.NANOSECONDS.toMillis(finishedNanos - trieNanos), + TimeUnit.NANOSECONDS.toMillis(candidate.nodeCommitPlanNanos()), + TimeUnit.NANOSECONDS.toMillis(candidate.nodeCommitStoreNanos()), + TimeUnit.NANOSECONDS.toMillis(candidate.nodeCommitFinalizeNanos()), + PathMerkleTrie.nodeCreateCountTotal() - nodeCreatesBefore, + PathMerkleTrie.nodeKeccakCountTotal() - nodeKeccaksBefore, + candidate.nodeDecodeCount(), candidate.nodeHashVerifyCount(), + candidate.hashReferenceCreateCount(), candidate.hashReferenceResolveCount(), stats); + } + + private void requireChild(PathStateBlockTransition transition) throws IOException { + if (transition.getBlockNumber() != head.getBlockNumber() + 1 + || !Arrays.equals(transition.getParentHash(), head.getBlockHash())) { + throw new IOException("path-state benchmark transition does not extend volatile head"); + } + } + + private void requireHealthy() throws IOException { + if (closed) { + throw new IOException("path-state benchmark overlay is closed"); + } + if (failed) { + throw new IOException("path-state benchmark overlay failed closed"); + } + } + + private static ExecutorService newExecutor(int threads, String role) { + return Executors.newFixedThreadPool(threads, task -> { + Thread thread = new Thread(task, "path-state-overlay-" + role); + thread.setDaemon(true); + return thread; + }); + } + + private static void requireThreadCount(int threads, String label) { + if (threads <= 0 || threads > 64) { + throw new IllegalArgumentException(label + " must be in [1, 64]"); + } + } + + private static boolean matches(PathStateRootMetadata metadata, long blockNumber, + byte[] blockHash) { + return metadata.getBlockNumber() == blockNumber + && Arrays.equals(metadata.getBlockHash(), blockHash); + } + + private static PathStateRootMetadata copy(PathStateRootMetadata metadata) { + return PathStateRootMetadata.decode(metadata.encode()); + } + + private static final class HeadState { + + private final PathStateRootMetadata metadata; + private final PathStateRoot.Snapshot snapshot; + + private HeadState(PathStateRootMetadata metadata, PathStateRoot.Snapshot snapshot) { + this.metadata = metadata; + this.snapshot = snapshot; + } + } + + private static final class PreparedOverlay { + + private final PathStateBlockTransition transition; + private final PathStateRootMetadata metadata; + private final PathStateRoot.Snapshot snapshot; + private final PathStateSnapshotDelta delta; + private final int nodeMutations; + private final long nativeNodeReads; + private final int changedParticipants; + private final int maxParticipantMutations; + private final int authoritativePreviousValues; + private final int maxParticipantStoreId; + private final long maxParticipantMillis; + private final long participantWorkMillis; + private final long participantWallMillis; + private final long prepareMillis; + private final long trieMillis; + private final long artifactMillis; + private final long nodePlanWorkMillis; + private final long nodeStoreWorkMillis; + private final long nodeFinalizeWorkMillis; + private final long nodeCreates; + private final long nodeKeccaks; + private final long nodeDecodes; + private final long nodeHashVerifies; + private final long hashRefsCreated; + private final long hashRefsResolved; + private final RecordingStats stats; + + private PreparedOverlay(PathStateBlockTransition transition, PathStateRootMetadata metadata, + PathStateRoot.Snapshot snapshot, PathStateSnapshotDelta delta, int nodeMutations, + long nativeNodeReads, PathStateRoot.ParallelApplyStats parallelStats, + long prepareMillis, long trieMillis, long artifactMillis, long nodePlanWorkMillis, + long nodeStoreWorkMillis, long nodeFinalizeWorkMillis, long nodeCreates, + long nodeKeccaks, long nodeDecodes, long nodeHashVerifies, long hashRefsCreated, + long hashRefsResolved, RecordingStats stats) { + this.transition = transition; + this.metadata = metadata; + this.snapshot = snapshot; + this.delta = delta; + this.nodeMutations = nodeMutations; + this.nativeNodeReads = nativeNodeReads; + changedParticipants = parallelStats == null ? 0 : parallelStats.participantCount(); + maxParticipantMutations = parallelStats == null + ? 0 : parallelStats.maxParticipantMutations(); + authoritativePreviousValues = parallelStats == null + ? 0 : parallelStats.authoritativePreviousValues(); + maxParticipantStoreId = parallelStats == null + ? 0 : parallelStats.maxParticipantStoreId(); + maxParticipantMillis = parallelStats == null ? 0 : parallelStats.maxParticipantMillis(); + participantWorkMillis = parallelStats == null ? 0 : parallelStats.participantWorkMillis(); + participantWallMillis = parallelStats == null ? 0 : parallelStats.wallMillis(); + this.prepareMillis = prepareMillis; + this.trieMillis = trieMillis; + this.artifactMillis = artifactMillis; + this.nodePlanWorkMillis = nodePlanWorkMillis; + this.nodeStoreWorkMillis = nodeStoreWorkMillis; + this.nodeFinalizeWorkMillis = nodeFinalizeWorkMillis; + this.nodeCreates = nodeCreates; + this.nodeKeccaks = nodeKeccaks; + this.nodeDecodes = nodeDecodes; + this.nodeHashVerifies = nodeHashVerifies; + this.hashRefsCreated = hashRefsCreated; + this.hashRefsResolved = hashRefsResolved; + this.stats = stats; + } + } + + /** Aggregated per-block node-artifact counters across all recording stores. */ + private static final class RecordingStats { + + private final long nodePuts; + private final long nodeDeletes; + private final long nodeRlpBytes; + private final long nodeRlpFinalBytes; + private final long uniqueNodePaths; + private final String perStore; + + private RecordingStats(long nodePuts, long nodeDeletes, long nodeRlpBytes, + long nodeRlpFinalBytes, long uniqueNodePaths, String perStore) { + this.nodePuts = nodePuts; + this.nodeDeletes = nodeDeletes; + this.nodeRlpBytes = nodeRlpBytes; + this.nodeRlpFinalBytes = nodeRlpFinalBytes; + this.uniqueNodePaths = uniqueNodePaths; + this.perStore = perStore; + } + + private long overwriteWrites() { + return nodePuts + nodeDeletes - uniqueNodePaths; + } + + private static RecordingStats collect(Map recordings) { + long puts = 0; + long deletes = 0; + long bytes = 0; + long finalBytes = 0; + long unique = 0; + StringBuilder detail = new StringBuilder(); + for (Map.Entry entry : recordings.entrySet()) { + RecordingStore store = entry.getValue(); + if (store.putCalls() + store.deleteCalls() == 0) { + continue; + } + puts += store.putCalls(); + deletes += store.deleteCalls(); + bytes += store.putBytes(); + finalBytes += store.finalBytes(); + unique += store.uniquePaths(); + detail.append(entry.getKey()).append(':').append(store.putCalls()).append('/') + .append(store.deleteCalls()).append('/').append(store.putBytes()).append('/') + .append(store.uniquePaths()).append(';'); + } + return new RecordingStats(puts, deletes, bytes, finalBytes, unique, detail.toString()); + } + } + + private static final class RecordingStore implements PathNodeStore { + + private final PathNodeStore base; + private final Map changes = new ConcurrentHashMap<>(); + private final Map reads = new ConcurrentHashMap<>(); + private final long initialNativeReads; + private final AtomicLong directReads = new AtomicLong(); + private final AtomicLong putCalls = new AtomicLong(); + private final AtomicLong deleteCalls = new AtomicLong(); + private final AtomicLong putBytes = new AtomicLong(); + + private RecordingStore(PathNodeStore base) { + this.base = Objects.requireNonNull(base, "base"); + initialNativeReads = base instanceof PathStatePhysicalStoreSet.ResidentNodeStore + ? ((PathStatePhysicalStoreSet.ResidentNodeStore) base).getNativeReads() : 0; + } + + @Override + public byte[] get(byte[] path) { + BytesKey key = new BytesKey(path); + byte[] changed = changes.get(key); + if (changed != null) { + return changed == ABSENT ? null : Arrays.copyOf(changed, changed.length); + } + byte[] value = reads.computeIfAbsent(key, ignored -> { + directReads.incrementAndGet(); + byte[] loaded = base.get(path); + return loaded == null ? ABSENT : Arrays.copyOf(loaded, loaded.length); + }); + return value == ABSENT ? null : Arrays.copyOf(value, value.length); + } + + @Override + public void put(byte[] path, byte[] encodedNode) { + byte[] present = Objects.requireNonNull(encodedNode, "encodedNode"); + putCalls.incrementAndGet(); + putBytes.addAndGet(present.length); + changes.put(new BytesKey(path), Arrays.copyOf(present, present.length)); + } + + @Override + public void delete(byte[] path) { + deleteCalls.incrementAndGet(); + changes.put(new BytesKey(path), ABSENT); + } + + private long putCalls() { + return putCalls.get(); + } + + private long deleteCalls() { + return deleteCalls.get(); + } + + private long putBytes() { + return putBytes.get(); + } + + private int uniquePaths() { + return changes.size(); + } + + private long finalBytes() { + long bytes = 0; + for (byte[] value : changes.values()) { + if (value != ABSENT) { + bytes += value.length; + } + } + return bytes; + } + + private List mutations() { + List result = new ArrayList<>(changes.size()); + for (Map.Entry entry : changes.entrySet()) { + byte[] value = entry.getValue(); + result.add(new PathStateSnapshotDelta.Mutation(entry.getKey().bytes, + value == ABSENT ? null : value)); + } + return result; + } + + private long nativeReads() { + return base instanceof PathStatePhysicalStoreSet.ResidentNodeStore + ? ((PathStatePhysicalStoreSet.ResidentNodeStore) base).getNativeReads() + - initialNativeReads : directReads.get(); + } + + } + + private static final class BytesKey { + + private final byte[] bytes; + + private BytesKey(byte[] bytes) { + this.bytes = Arrays.copyOf(Objects.requireNonNull(bytes, "path"), bytes.length); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof BytesKey + && Arrays.equals(bytes, ((BytesKey) other).bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java index 2523eda68db..78d486539a8 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalSnapshotHead.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Objects; +import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** Runtime owner for a block-bound physical 27+1 CURRENT. */ @@ -11,6 +12,7 @@ public final class PathStatePhysicalSnapshotHead implements PathStateHead { private final PathStatePhysicalStoreSet stores; private final PathStateLayerLimits limits; private PathStateRootMetadata head; + private PathStatePhysicalStoreSet.PreparedPhysicalTransition pending; private boolean failed; private boolean closed; @@ -51,14 +53,21 @@ public static PathStatePhysicalSnapshotHead open(Path directory, Engine engine, public synchronized PathStateRootMetadata advance(PathStateBlockTransition transition) throws IOException { requireHealthy(); + if (pending != null && !pending.matches(transition)) { + throw new IOException("physical path-state publication differs from prepared transition"); + } PathStateRootMetadata previous = head; try { - PathStateRootMetadata committed = stores.applyAndPublish(transition, limits); + PathStatePhysicalStoreSet.PreparedPhysicalTransition prepared = pending; + PathStateRootMetadata committed = prepared != null && prepared.matches(transition) + ? stores.applyAndPublish(prepared, limits) + : stores.applyAndPublish(transition, limits); if (!same(committed, stores.currentMetadata())) { failed = true; throw new IOException("physical path-state committed CURRENT identity mismatch"); } head = committed; + pending = null; return PathStateRootMetadata.decode(committed.encode()); } catch (IOException | RuntimeException failure) { try { @@ -77,6 +86,7 @@ public synchronized PathStateRootMetadata advance(PathStateBlockTransition trans public synchronized PathStateRootMetadata rewindTo(long blockNumber, byte[] blockHash) throws IOException { requireHealthy(); + pending = null; PathStateRootMetadata previous = head; try { PathStateRootMetadata rewound = stores.rewindTo(blockNumber, blockHash, limits); @@ -113,6 +123,18 @@ public synchronized byte[] preview(PathStateBlockTransition transition) throws I .getStateRoot(); } + @Override + public synchronized PathStateSnapshotDelta prepareSnapshotDelta(BlockSnapshotMeta meta, + PathStateBlockTransition transition) throws IOException { + requireHealthy(); + if (pending != null) { + throw new IOException("physical path-state transition is already prepared"); + } + pending = stores.prepareSnapshotDelta(Objects.requireNonNull(meta, "meta"), + Objects.requireNonNull(transition, "transition")); + return pending.getSnapshotDelta(); + } + @Override public synchronized PathStateRootMetadata getHead() throws IOException { requireHealthy(); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java index 0bab1ea922b..9bc6cff307b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java @@ -26,6 +26,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tron.common.crypto.Hash; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.core.CommonCheckpointPayload; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** @@ -74,6 +76,8 @@ public final class PathStatePhysicalStoreSet implements Closeable { 'g', 'e', 'n', 'e', 'r', 'a', 't', 'i', 'o', 'n'}; private static final byte[] SUPER_GENERATION_METADATA = new byte[]{'s', 'u', 'p', 'e', 'r', '-', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'i', 'o', 'n'}; + private static final byte[] CHECKPOINT_TARGET_METADATA = new byte[]{'c', 'h', 'e', 'c', 'k', + 'p', 'o', 'i', 'n', 't', '-', 't', 'a', 'r', 'g', 'e', 't', '-', 'v', '1'}; private final Path directory; private final PathStatePhysicalStoreManifest manifest; @@ -85,11 +89,13 @@ public final class PathStatePhysicalStoreSet implements Closeable { private final ExecutorService trieBranchExecutor; private final ResidentNodeCache residentNodeCache; private Map reverseJournalIndex; + private byte[] cachedTrieTarget; + private PathStateRoot.Snapshot cachedTrieSnapshot; private boolean rootClaimed; private boolean closed; private PathStatePhysicalStoreSet(PathStatePhysicalStoreManifest manifest, - PathStateParticipantScope scope) + PathStateParticipantScope scope, long residentNodeCacheBytes) throws IOException { this.manifest = manifest; this.directory = manifest.getDirectory(); @@ -98,7 +104,7 @@ private PathStatePhysicalStoreSet(PathStatePhysicalStoreManifest manifest, this.participantPrepareExecutor = newTrieExecutor("participant-prepare", MAX_PARALLEL_PARTICIPANT_PREPARES); this.trieBranchExecutor = newTrieExecutor("branch-prepare", MAX_PARALLEL_TRIE_BRANCHES); - this.residentNodeCache = new ResidentNodeCache(STEADY_NODE_CACHE_BYTES); + this.residentNodeCache = new ResidentNodeCache(residentNodeCacheBytes); try { for (PathStateParticipant participant : scope.getParticipants()) { Path participantDirectory = directory.resolve(STORES_DIRECTORY).resolve(String.format( @@ -124,12 +130,20 @@ public static PathStatePhysicalStoreSet open(Path directory, PathStateParticipan rejectLegacySharedNodes(root); PathStatePhysicalStoreManifest manifest = PathStatePhysicalStoreManifest.createOrOpen(root, Objects.requireNonNull(engine, "engine")); - return new PathStatePhysicalStoreSet(manifest, Objects.requireNonNull(scope, "scope")); + return new PathStatePhysicalStoreSet(manifest, Objects.requireNonNull(scope, "scope"), + STEADY_NODE_CACHE_BYTES); } /** Opens only a fully materialized physical layout; missing child databases fail closed. */ public static PathStatePhysicalStoreSet openExisting(Path directory, PathStateParticipantScope scope, Engine engine) throws IOException { + return openExisting(directory, scope, engine, STEADY_NODE_CACHE_BYTES); + } + + /** Opens a complete physical layout with an explicit shared resident-node cache budget. */ + public static PathStatePhysicalStoreSet openExisting(Path directory, + PathStateParticipantScope scope, Engine engine, long residentNodeCacheBytes) + throws IOException { Path root = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); rejectLegacySharedNodes(root); PathStatePhysicalStoreManifest manifest = PathStatePhysicalStoreManifest.validateExisting( @@ -141,7 +155,7 @@ public static PathStatePhysicalStoreSet openExisting(Path directory, .resolve(NODES_DIRECTORY)); } requireStoreDirectory(root.resolve(SUPER_DIRECTORY).resolve(NODES_DIRECTORY)); - return new PathStatePhysicalStoreSet(manifest, admittedScope); + return new PathStatePhysicalStoreSet(manifest, admittedScope, residentNodeCacheBytes); } public synchronized PhysicalStore participant(String dbName) { @@ -181,6 +195,18 @@ PathStateParticipantScope participantScope() { return scope; } + long residentNodeCacheBytes() { + return residentNodeCache.bytes(); + } + + int residentNodeCacheEntries() { + return residentNodeCache.size(); + } + + long residentNodeCacheEvictions() { + return residentNodeCache.evictions(); + } + synchronized void saveIngestCheckpoint(String dbName, PathStatePhysicalIngestCheckpoint value) { participant(dbName).putMetadata(FLAT_INGEST_CHECKPOINT, Objects.requireNonNull(value, "value").encode()); @@ -575,6 +601,26 @@ public synchronized PathStateRootMetadata previewTransition(PathStateBlockTransi return prepareTransition(transition).target.getMetadata(); } + /** Prepares one physical transition and its Snapshot-owned forward delta without any write. */ + synchronized PreparedPhysicalTransition prepareSnapshotDelta(BlockSnapshotMeta meta, + PathStateBlockTransition transition) throws IOException { + requireOpen(); + if (Files.exists(directory.resolve(INTENT_FILE), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("physical path-state prepare requires a settled CURRENT"); + } + TransitionPlan plan = prepareTransition(transition); + return new PreparedPhysicalTransition(transition, plan, + snapshotDelta(Objects.requireNonNull(meta, "meta"), transition, plan)); + } + + /** Materializes an unchanged, previously prepared physical transition exactly once. */ + synchronized PathStateRootMetadata applyAndPublish(PreparedPhysicalTransition prepared, + PathStateLayerLimits limits) throws IOException { + PreparedPhysicalTransition admitted = Objects.requireNonNull(prepared, "prepared"); + return applyAndPublishInternal(admitted.transition, limits, stage -> { }, true, + admitted.plan); + } + /** Applies one block-final child to the physical 27+1 stores and publishes its CURRENT. */ public synchronized PathStateRootMetadata applyAndPublish(PathStateBlockTransition transition) throws IOException { @@ -600,10 +646,21 @@ synchronized PathStateRootMetadata applyAndPublish(PathStateBlockTransition tran private PathStateRootMetadata applyAndPublishInternal(PathStateBlockTransition transition, PathStateLayerLimits limits, TransitionFaultHook faultHook, boolean parallelParticipants) throws IOException { + return applyAndPublishInternal(transition, limits, faultHook, parallelParticipants, null); + } + + private PathStateRootMetadata applyAndPublishInternal(PathStateBlockTransition transition, + PathStateLayerLimits limits, TransitionFaultHook faultHook, boolean parallelParticipants, + TransitionPlan preparedPlan) throws IOException { requireOpen(); long startedNanos = System.nanoTime(); recoverPublication(); - TransitionPlan plan = prepareTransition(transition); + TransitionPlan plan = preparedPlan == null ? prepareTransition(transition) : preparedPlan; + if (preparedPlan != null + && (!Arrays.equals(currentTarget().encode(), plan.parentTarget) + || preparedPlan.transition != transition)) { + throw new IOException("prepared physical transition no longer extends CURRENT"); + } long preparedNanos = System.nanoTime(); TransitionFaultHook hook = Objects.requireNonNull(faultHook, "faultHook"); byte[] encoded = plan.target.encode(); @@ -639,12 +696,15 @@ private PathStateRootMetadata applyAndPublishInternal(PathStateBlockTransition t hook.after(TransitionStage.AFTER_CURRENT); PathStateMetadataFile.deleteDurable(intent); hook.after(TransitionStage.AFTER_RETIRE); + cacheTrieSnapshot(encoded, plan.trieSnapshot); long completedNanos = System.nanoTime(); logger.info("Path-state physical transition completed: head={}, changedStores={}, " + "journalBytes={}, journalCount={}, journalWindowBytes={}, nodeReadMisses={}, " + "nodeReadHits={}, residentNodeHits={}, residentCleanHits={}, " + "residentUpdatedHits={}, nativeNodeReads={}, residentCacheBytes={}, " - + "residentEvictions={}, nodeDecodes={}, nodeHashVerifies={}, prepareMs={}, " + + "residentEvictions={}, nodeDecodes={}, nodeHashVerifies={}, " + + "hashReferencesCreated={}, hashReferencesResolved={}, flatReverseReads={}, " + + "flatReverseReused={}, prepareMs={}, " + "journalMs={}, intentMs={}, participantWaitMs={}, finalizeMs={}, totalMs={}", plan.target.getMetadata().getBlockNumber(), plan.participants.size(), encodedJournal.length, reverseJournalCount(), reverseJournalBytes(), @@ -653,6 +713,10 @@ encodedJournal.length, reverseJournalCount(), reverseJournalBytes(), residentNodeCache.bytes(), residentNodeCache.evictions() - plan.initialResidentEvictions, plan.nodeDecodes, plan.nodeHashVerifies, + plan.hashReferencesCreated, + plan.hashReferencesResolved, + plan.flatReverseReads, + plan.flatReverseReused, elapsedMillis(startedNanos, preparedNanos), elapsedMillis(preparedNanos, journalNanos), elapsedMillis(journalNanos, intentNanos), elapsedMillis(intentNanos, participantsNanos), elapsedMillis(participantsNanos, completedNanos), @@ -700,18 +764,28 @@ private TransitionPlan prepareTransition(PathStateBlockTransition supplied) thro } Map recordings = new LinkedHashMap<>(); - PathStateRoot candidate = new PathStateRoot(scope, - participant -> recordings.computeIfAbsent(participant.getStoreId(), ignored -> - new RecordingNodeStore(participant(participant.getDbName()).nodeStore())), - recordings.computeIfAbsent(0, ignored -> - new RecordingNodeStore(superStore.nodeStore()))); - candidate.restoreStoredRoots(current.getSuperRoot()); - requireParticipantRoots(candidate, current); + PathStateRoot.PathNodeStoreFactory storeFactory = participant -> recordings.computeIfAbsent( + participant.getStoreId(), ignored -> + new RecordingNodeStore(participant(participant.getDbName()).nodeStore())); + RecordingNodeStore recordingSuper = recordings.computeIfAbsent(0, ignored -> + new RecordingNodeStore(superStore.nodeStore())); + PathStateRoot candidate; + boolean reusedTrieSnapshot = cachedTrieSnapshot != null + && Arrays.equals(cachedTrieTarget, current.encode()); + if (reusedTrieSnapshot) { + candidate = PathStateRoot.fromSnapshot(scope, storeFactory, recordingSuper, + cachedTrieSnapshot); + } else { + candidate = new PathStateRoot(scope, storeFactory, recordingSuper); + candidate.restoreStoredRoots(current.getSuperRoot()); + requireParticipantRoots(candidate, current); + } if (!transition.getMutations().isEmpty()) { candidate.applyParallel(transition.getMutations(), participantPrepareExecutor, trieBranchExecutor); } byte[] stateRoot = candidate.rootHash(); + PathStateRoot.Snapshot trieSnapshot = candidate.snapshot(); Map> flatByStore = new LinkedHashMap<>(); for (PathStateMutation mutation : transition.getMutations()) { @@ -720,13 +794,21 @@ private TransitionPlan prepareTransition(PathStateBlockTransition supplied) thro mutation.getPhysicalKey()); byte[] encodedValue = mutation.isDelete() ? null : PathStateCommitmentCodec.presentLeafValue(mutation.getPhysicalValue()); + byte[] previousEncodedValue = null; + if (mutation.isPreviousValueKnown() && mutation.getPreviousPhysicalValue() != null) { + previousEncodedValue = PathStateCommitmentCodec.presentLeafValue( + mutation.getPreviousPhysicalValue()); + } flatByStore.computeIfAbsent(participant.getStoreId(), ignored -> new ArrayList<>()) - .add(new FlatMutation(secureKey, encodedValue)); + .add(new FlatMutation(secureKey, encodedValue, mutation.isPreviousValueKnown(), + previousEncodedValue)); } List targets = new ArrayList<>(); List participantTransitions = new ArrayList<>(); List reverseStores = new ArrayList<>(); + long flatReverseReads = 0; + long flatReverseReused = 0; for (PathStatePhysicalGlobalIntent.ParticipantTarget oldTarget : current.getParticipants()) { List flatMutations = flatByStore.get(oldTarget.getStoreId()); @@ -741,14 +823,22 @@ private TransitionPlan prepareTransition(PathStateBlockTransition supplied) thro byte[] generation = participantGeneration(participant, flatDigest, storeRoot); targets.add(new PathStatePhysicalGlobalIntent.ParticipantTarget(participant.getStoreId(), generation, flatDigest, storeRoot)); - participantTransitions.add(new ParticipantTransition( + participantTransitions.add(new ParticipantTransition(participant, participant(participant.getDbName()), flatMutations, recordings.get(participant.getStoreId()).mutations(), flatDigest, generation, storeRoot)); List reverseFlat = new ArrayList<>(); for (FlatMutation mutation : flatMutations) { + byte[] oldValue; + if (mutation.previousValueKnown) { + oldValue = mutation.previousEncodedValue; + flatReverseReused++; + } else { + oldValue = participant(participant.getDbName()).getFlat(mutation.secureKey); + flatReverseReads++; + } reverseFlat.add(new PathStatePhysicalReverseJournal.Entry(mutation.secureKey, - participant(participant.getDbName()).getFlat(mutation.secureKey))); + oldValue)); } reverseStores.add(new PathStatePhysicalReverseJournal.StoreReverse( participant.getStoreId(), reverseFlat, @@ -775,11 +865,51 @@ private TransitionPlan prepareTransition(PathStateBlockTransition supplied) thro .mapToLong(RecordingNodeStore::getResidentCleanHits).sum(); long residentUpdatedHits = recordings.values().stream() .mapToLong(RecordingNodeStore::getResidentUpdatedHits).sum(); - return new TransitionPlan(target, participantTransitions, superStore, + return new TransitionPlan(transition, current.encode(), target, participantTransitions, + superStore, recordings.get(0).mutations(), journal, nodeReadMisses, nodeReadHits, residentNodeHits, residentCleanHits, residentUpdatedHits, nativeNodeReads, initialResidentEvictions, candidate.nodeDecodeCount(), - candidate.nodeHashVerifyCount()); + candidate.nodeHashVerifyCount(), candidate.hashReferenceCreateCount(), + candidate.hashReferenceResolveCount(), flatReverseReads, flatReverseReused, + trieSnapshot, reusedTrieSnapshot); + } + + private PathStateSnapshotDelta snapshotDelta(BlockSnapshotMeta meta, + PathStateBlockTransition transition, TransitionPlan plan) { + List stores = new ArrayList<>(); + for (ParticipantTransition participant : plan.participants) { + stores.add(new PathStateSnapshotDelta.StoreDelta(participant.participant, + participant.storeRoot, snapshotMutations(participant.flatMutations), + snapshotNodeMutations(participant.nodeMutations))); + } + return PathStateSnapshotDelta.fromPhysical(meta, + PathStatePhysicalGlobalIntent.decode(plan.parentTarget).getMetadata(), transition, + plan.trieSnapshot, stores, snapshotNodeMutations(plan.superNodeMutations)); + } + + private static List snapshotMutations( + List mutations) { + List result = new ArrayList<>(); + for (FlatMutation mutation : mutations) { + result.add(new PathStateSnapshotDelta.Mutation(mutation.secureKey, + mutation.encodedValue)); + } + return result; + } + + private static List snapshotNodeMutations( + List mutations) { + List result = new ArrayList<>(); + for (NodeMutation mutation : mutations) { + result.add(new PathStateSnapshotDelta.Mutation(mutation.path, mutation.encodedNode)); + } + return result; + } + + private void cacheTrieSnapshot(byte[] target, PathStateRoot.Snapshot snapshot) { + cachedTrieTarget = Arrays.copyOf(target, target.length); + cachedTrieSnapshot = Objects.requireNonNull(snapshot, "snapshot"); } private void applyParticipantTransitionsInParallel(List transitions) @@ -862,6 +992,8 @@ private void applyReverseJournal(PathStatePhysicalReverseJournal journal, PathStateMetadataFile.replaceCurrentBytes(current, parent.encode()); hook.after(RewindStage.AFTER_CURRENT); PathStateMetadataFile.deleteDurable(intent); + cachedTrieTarget = null; + cachedTrieSnapshot = null; hook.after(RewindStage.AFTER_RETIRE); } @@ -1623,6 +1755,51 @@ void applySuperTransition(List nodeMutations, byte[] generation, nodeStore.apply(nodeMutations); } + void applyCheckpointParticipant(CommonCheckpointPayload.PathStoreTarget target, + byte[] marker) { + List mutations = new ArrayList<>(); + for (CommonCheckpointPayload.Mutation mutation : target.getFlatMutations()) { + byte[] key = prefixed(FLAT_PREFIX, mutation.getKey(), "secureKey"); + mutations.add(mutation.isDelete() + ? PathStateNativeNodeStore.BatchMutation.delete(key) + : PathStateNativeNodeStore.BatchMutation.put(key, mutation.getValue())); + } + List cacheMutations = new ArrayList<>(); + for (CommonCheckpointPayload.Mutation mutation : target.getNodeMutations()) { + byte[] path = mutation.getKey(); + byte[] value = mutation.getValue(); + mutations.add(mutation.isDelete() + ? PathStateNativeNodeStore.BatchMutation.delete(prefixed(NODE_PREFIX, path, "path")) + : PathStateNativeNodeStore.BatchMutation.put(prefixed(NODE_PREFIX, path, "path"), + value)); + cacheMutations.add(new NodeMutation(path, value)); + } + mutations.add(metadataMutation(CHECKPOINT_TARGET_METADATA, marker)); + nativeStore.writeBatch(mutations); + nodeStore.apply(cacheMutations); + } + + void applyCheckpointSuper(List supplied, byte[] marker) { + List mutations = new ArrayList<>(); + List cacheMutations = new ArrayList<>(); + for (CommonCheckpointPayload.Mutation mutation : supplied) { + byte[] path = mutation.getKey(); + byte[] value = mutation.getValue(); + mutations.add(mutation.isDelete() + ? PathStateNativeNodeStore.BatchMutation.delete(prefixed(NODE_PREFIX, path, "path")) + : PathStateNativeNodeStore.BatchMutation.put(prefixed(NODE_PREFIX, path, "path"), + value)); + cacheMutations.add(new NodeMutation(path, value)); + } + mutations.add(metadataMutation(CHECKPOINT_TARGET_METADATA, marker)); + nativeStore.writeBatch(mutations); + nodeStore.apply(cacheMutations); + } + + byte[] checkpointTargetMarker() { + return getMetadata(CHECKPOINT_TARGET_METADATA); + } + private static void appendNodeMutations( List target, List nodeMutations) { @@ -2084,17 +2261,28 @@ private static final class FlatMutation { private final byte[] secureKey; private final byte[] encodedValue; + private final boolean previousValueKnown; + private final byte[] previousEncodedValue; private FlatMutation(byte[] secureKey, byte[] encodedValue) { + this(secureKey, encodedValue, false, null); + } + + private FlatMutation(byte[] secureKey, byte[] encodedValue, boolean previousValueKnown, + byte[] previousEncodedValue) { this.secureKey = Arrays.copyOf(Objects.requireNonNull(secureKey, "secureKey"), secureKey.length); this.encodedValue = encodedValue == null ? null : Arrays.copyOf(encodedValue, encodedValue.length); + this.previousValueKnown = previousValueKnown; + this.previousEncodedValue = previousEncodedValue == null ? null + : Arrays.copyOf(previousEncodedValue, previousEncodedValue.length); } } private static final class ParticipantTransition { + private final PathStateParticipant participant; private final PhysicalStore store; private final List flatMutations; private final List nodeMutations; @@ -2102,9 +2290,10 @@ private static final class ParticipantTransition { private final byte[] generation; private final byte[] storeRoot; - private ParticipantTransition(PhysicalStore store, List flatMutations, - List nodeMutations, byte[] flatDigest, byte[] generation, - byte[] storeRoot) { + private ParticipantTransition(PathStateParticipant participant, PhysicalStore store, + List flatMutations, List nodeMutations, byte[] flatDigest, + byte[] generation, byte[] storeRoot) { + this.participant = participant; this.store = store; this.flatMutations = flatMutations; this.nodeMutations = nodeMutations; @@ -2116,6 +2305,8 @@ private ParticipantTransition(PhysicalStore store, List flatMutati private static final class TransitionPlan { + private final PathStateBlockTransition transition; + private final byte[] parentTarget; private final PathStatePhysicalGlobalIntent target; private final List participants; private final PhysicalStore superStore; @@ -2130,13 +2321,25 @@ private static final class TransitionPlan { private final long initialResidentEvictions; private final long nodeDecodes; private final long nodeHashVerifies; - - private TransitionPlan(PathStatePhysicalGlobalIntent target, + private final long hashReferencesCreated; + private final long hashReferencesResolved; + private final long flatReverseReads; + private final long flatReverseReused; + private final PathStateRoot.Snapshot trieSnapshot; + private final boolean reusedTrieSnapshot; + + private TransitionPlan(PathStateBlockTransition transition, byte[] parentTarget, + PathStatePhysicalGlobalIntent target, List participants, PhysicalStore superStore, List superNodeMutations, PathStatePhysicalReverseJournal journal, long nodeReadMisses, long nodeReadHits, long residentNodeHits, long residentCleanHits, long residentUpdatedHits, long nativeNodeReads, - long initialResidentEvictions, long nodeDecodes, long nodeHashVerifies) { + long initialResidentEvictions, long nodeDecodes, long nodeHashVerifies, + long hashReferencesCreated, long hashReferencesResolved, + long flatReverseReads, long flatReverseReused, + PathStateRoot.Snapshot trieSnapshot, boolean reusedTrieSnapshot) { + this.transition = transition; + this.parentTarget = Arrays.copyOf(parentTarget, parentTarget.length); this.target = target; this.participants = participants; this.superStore = superStore; @@ -2151,6 +2354,38 @@ private TransitionPlan(PathStatePhysicalGlobalIntent target, this.initialResidentEvictions = initialResidentEvictions; this.nodeDecodes = nodeDecodes; this.nodeHashVerifies = nodeHashVerifies; + this.hashReferencesCreated = hashReferencesCreated; + this.hashReferencesResolved = hashReferencesResolved; + this.flatReverseReads = flatReverseReads; + this.flatReverseReused = flatReverseReused; + this.trieSnapshot = Objects.requireNonNull(trieSnapshot, "trieSnapshot"); + this.reusedTrieSnapshot = reusedTrieSnapshot; + } + } + + static final class PreparedPhysicalTransition { + + private final PathStateBlockTransition transition; + private final TransitionPlan plan; + private final PathStateSnapshotDelta snapshotDelta; + + private PreparedPhysicalTransition(PathStateBlockTransition transition, + TransitionPlan plan, PathStateSnapshotDelta snapshotDelta) { + this.transition = transition; + this.plan = plan; + this.snapshotDelta = snapshotDelta; + } + + PathStateSnapshotDelta getSnapshotDelta() { + return snapshotDelta; + } + + boolean reusedTrieSnapshot() { + return plan.reusedTrieSnapshot; + } + + boolean matches(PathStateBlockTransition supplied) { + return transition == supplied; } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index 9d68c6a96d9..988bbe229d3 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -15,6 +15,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; /** * Current-only per-Store trie and super-trie aggregator for TASK-016. @@ -103,7 +104,7 @@ public synchronized void apply(Collection mutations) { rootMaterialized = false; } - synchronized void applyParallel(Collection mutations, + synchronized ParallelApplyStats applyParallel(Collection mutations, ExecutorService participantExecutor, ExecutorService branchExecutor) { List prepared = prepare(mutations); Map> grouped = new LinkedHashMap<>(); @@ -111,16 +112,29 @@ synchronized void applyParallel(Collection mutations, grouped.computeIfAbsent(mutation.participant.getStoreId(), ignored -> new ArrayList<>()) .add(mutation); } - List> futures = new ArrayList<>(); + List work = new ArrayList<>(grouped.size()); for (List participantMutations : grouped.values()) { + work.add(new ParticipantWork(participantMutations)); + } + work.sort((left, right) -> { + int compared = Integer.compare(right.mutations.size(), left.mutations.size()); + return compared != 0 ? compared + : Integer.compare(left.participant.getStoreId(), right.participant.getStoreId()); + }); + List> futures = new ArrayList<>(work.size()); + long startedNanos = System.nanoTime(); + for (ParticipantWork participantWork : work) { futures.add(Objects.requireNonNull(participantExecutor, "participantExecutor").submit(() -> { - PathStateParticipant participant = participantMutations.get(0).participant; - List batch = new ArrayList<>(participantMutations.size()); - for (PreparedMutation mutation : participantMutations) { - batch.add(new PathMerkleTrie.BatchMutation(mutation.secureKey, mutation.encodedValue)); + long participantStartedNanos = System.nanoTime(); + List batch = + new ArrayList<>(participantWork.mutations.size()); + for (PreparedMutation mutation : participantWork.mutations) { + batch.add(new PathMerkleTrie.BatchMutation(mutation.secureKey, mutation.encodedValue, + mutation.previousValueKnown, mutation.previousEncodedValue)); } - participantTries.get(participant.getDbName()).applyBatch(batch, + participantTries.get(participantWork.participant.getDbName()).applyBatch(batch, Objects.requireNonNull(branchExecutor, "branchExecutor")); + participantWork.elapsedNanos = System.nanoTime() - participantStartedNanos; })); } try { @@ -141,6 +155,29 @@ synchronized void applyParallel(Collection mutations, } recordPendingLeafMutations(prepared); rootMaterialized = false; + long totalWorkNanos = 0; + int maxMutations = 0; + int authoritativePreviousValues = 0; + long maxElapsedNanos = 0; + int maxElapsedStoreId = 0; + for (ParticipantWork participantWork : work) { + totalWorkNanos += participantWork.elapsedNanos; + maxMutations = Math.max(maxMutations, participantWork.mutations.size()); + if (participantWork.elapsedNanos > maxElapsedNanos) { + maxElapsedNanos = participantWork.elapsedNanos; + maxElapsedStoreId = participantWork.participant.getStoreId(); + } + } + for (PreparedMutation mutation : prepared) { + if (mutation.previousValueKnown) { + authoritativePreviousValues++; + } + } + return new ParallelApplyStats(work.size(), prepared.size(), authoritativePreviousValues, + maxMutations, + TimeUnit.NANOSECONDS.toMillis(maxElapsedNanos), maxElapsedStoreId, + TimeUnit.NANOSECONDS.toMillis(totalWorkNanos), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedNanos)); } private static void cancel(List> futures) { @@ -151,6 +188,76 @@ private static void cancel(List> futures) { } } + static final class ParallelApplyStats { + + private final int participantCount; + private final int mutationCount; + private final int authoritativePreviousValues; + private final int maxParticipantMutations; + private final long maxParticipantMillis; + private final int maxParticipantStoreId; + private final long participantWorkMillis; + private final long wallMillis; + + private ParallelApplyStats(int participantCount, int mutationCount, + int authoritativePreviousValues, + int maxParticipantMutations, long maxParticipantMillis, int maxParticipantStoreId, + long participantWorkMillis, long wallMillis) { + this.participantCount = participantCount; + this.mutationCount = mutationCount; + this.authoritativePreviousValues = authoritativePreviousValues; + this.maxParticipantMutations = maxParticipantMutations; + this.maxParticipantMillis = maxParticipantMillis; + this.maxParticipantStoreId = maxParticipantStoreId; + this.participantWorkMillis = participantWorkMillis; + this.wallMillis = wallMillis; + } + + int participantCount() { + return participantCount; + } + + int mutationCount() { + return mutationCount; + } + + int authoritativePreviousValues() { + return authoritativePreviousValues; + } + + int maxParticipantMutations() { + return maxParticipantMutations; + } + + long maxParticipantMillis() { + return maxParticipantMillis; + } + + int maxParticipantStoreId() { + return maxParticipantStoreId; + } + + long participantWorkMillis() { + return participantWorkMillis; + } + + long wallMillis() { + return wallMillis; + } + } + + private static final class ParticipantWork { + + private final PathStateParticipant participant; + private final List mutations; + private long elapsedNanos; + + private ParticipantWork(List mutations) { + this.mutations = mutations; + participant = mutations.get(0).participant; + } + } + /** Applies one rebuild batch while locking only the participant tries touched by that batch. */ void applyRebuild(Collection mutations) { List prepared = prepare(mutations); @@ -191,13 +298,26 @@ void restoreRebuildParticipants(PathStateRebuildCheckpoint checkpoint) { } synchronized void restoreStoredRoots(byte[] expectedRoot) { + Map participantRoots = new LinkedHashMap<>(); for (PathStateParticipant participant : scope.getParticipants()) { - participantTries.get(participant.getDbName()).restoreRoot(); + participantRoots.put(participant.getDbName(), + participantTries.get(participant.getDbName()).restoreRoot()); } superTrie.restoreRoot(expectedRoot); if (!Arrays.equals(superTrie.rootHash(), expectedRoot)) { throw new IllegalStateException("restored path-state root differs from durable progress"); } + for (PathStateParticipant participant : scope.getParticipants()) { + byte[] expectedLeaf = PathStateCommitmentCodec.superLeafValue(participant.getStoreId(), + participant.getDbName(), participant.getStoreFormatVersion(), + participantRoots.get(participant.getDbName())); + byte[] actualLeaf = superTrie.get( + PathStateCommitmentCodec.superLeafKey(participant.getStoreId())); + if (!Arrays.equals(expectedLeaf, actualLeaf)) { + throw new IllegalStateException( + "restored path-state participant root is not bound by the super trie"); + } + } rootMaterialized = true; } @@ -243,6 +363,46 @@ synchronized long nodeHashVerifyCount() { return count; } + synchronized long hashReferenceCreateCount() { + long count = superTrie.getHashReferenceCreateCount(); + for (PathMerkleTrie trie : participantTries.values()) { + count += trie.getHashReferenceCreateCount(); + } + return count; + } + + synchronized long hashReferenceResolveCount() { + long count = superTrie.getHashReferenceResolveCount(); + for (PathMerkleTrie trie : participantTries.values()) { + count += trie.getHashReferenceResolveCount(); + } + return count; + } + + synchronized long nodeCommitPlanNanos() { + long nanos = superTrie.getLastCommitPlanNanos(); + for (PathMerkleTrie trie : participantTries.values()) { + nanos += trie.getLastCommitPlanNanos(); + } + return nanos; + } + + synchronized long nodeCommitStoreNanos() { + long nanos = superTrie.getLastCommitStoreNanos(); + for (PathMerkleTrie trie : participantTries.values()) { + nanos += trie.getLastCommitStoreNanos(); + } + return nanos; + } + + synchronized long nodeCommitFinalizeNanos() { + long nanos = superTrie.getLastCommitFinalizeNanos(); + for (PathMerkleTrie trie : participantTries.values()) { + nanos += trie.getLastCommitFinalizeNanos(); + } + return nanos; + } + /** Returns the super root after binding every participant identity, format, and current root. */ public synchronized byte[] rootHash() { if (rootMaterialized) { @@ -259,15 +419,20 @@ public synchronized byte[] rootHash() { return root; } - /** Verifies all current participant nodes and the already-published super-trie nodes. */ - public synchronized void verifyNodeStores() { + /** Explicitly scans all current participant nodes and the published super-trie nodes. */ + public synchronized void verifyAllNodeStores() { if (!rootMaterialized) { throw new IllegalStateException("path state root is not materialized"); } for (PathMerkleTrie trie : participantTries.values()) { - trie.verifyNodeStore(); + trie.verifyAllNodeStore(); } - superTrie.verifyNodeStore(); + superTrie.verifyAllNodeStore(); + } + + /** Compatibility alias for explicit rebuild, repair, and test callers. */ + public synchronized void verifyNodeStores() { + verifyAllNodeStores(); } synchronized List leafRecords() { @@ -397,7 +562,7 @@ private void restoreLeaves(Collection records, byte[] expectedRoot, throw new IllegalStateException("restored path-state root differs from durable progress"); } rootMaterialized = true; - verifyNodeStores(); + verifyAllNodeStores(); } private Map> restoreParticipantLeaves( @@ -446,7 +611,13 @@ private List prepare(Collection mutations) } byte[] encodedValue = present.isDelete() ? null : PathStateCommitmentCodec.presentLeafValue(present.getPhysicalValue()); - prepared.add(new PreparedMutation(participant, secureKey, encodedValue)); + boolean previousValueKnown = present.isPreviousValueKnown(); + byte[] previousPhysicalValue = previousValueKnown + ? present.getPreviousPhysicalValue() : null; + byte[] previousEncodedValue = previousPhysicalValue == null ? null + : PathStateCommitmentCodec.presentLeafValue(previousPhysicalValue); + prepared.add(new PreparedMutation(participant, secureKey, encodedValue, + previousValueKnown, previousEncodedValue)); } Collections.sort(prepared, MUTATION_COMPARATOR); return prepared; @@ -493,6 +664,15 @@ private Snapshot(Map participants, public byte[] getStateRoot() { return Arrays.copyOf(stateRoot, stateRoot.length); } + + byte[] participantRoot(String dbName) { + PathMerkleTrie.Snapshot participant = participants.get( + Objects.requireNonNull(dbName, "dbName")); + if (participant == null) { + throw new IllegalArgumentException("snapshot has no path-state participant: " + dbName); + } + return participant.rootHash(); + } } static final class LeafRecord { @@ -554,12 +734,16 @@ private static final class PreparedMutation { private final PathStateParticipant participant; private final byte[] secureKey; private final byte[] encodedValue; + private final boolean previousValueKnown; + private final byte[] previousEncodedValue; private PreparedMutation(PathStateParticipant participant, byte[] secureKey, - byte[] encodedValue) { + byte[] encodedValue, boolean previousValueKnown, byte[] previousEncodedValue) { this.participant = participant; this.secureKey = secureKey; this.encodedValue = encodedValue; + this.previousValueKnown = previousValueKnown; + this.previousEncodedValue = previousEncodedValue; } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java index 949ae1866ae..e30ac282f6a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java @@ -4,8 +4,12 @@ import java.util.Arrays; import java.util.Locale; import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; import org.tron.core.db2.archive.BlockChangeView; +import org.tron.core.db2.archive.BlockSnapshotMeta; /** Independent non-consensus runtime installed at the metadata-aware block commit boundary. */ @Slf4j(topic = "DB") @@ -15,6 +19,10 @@ public final class PathStateRuntimeAttachment { private final TransitionSink sink; private final BaseFlushSink baseFlushSink; private final TransitionPreviewer previewer; + private final SnapshotDeltaPreparer snapshotDeltaPreparer; + private final boolean deferredCapture; + private final BlockingQueue deferredQueue; + private final Thread deferredWorker; private Throwable failure; private FailureStage failureStage; private long readyBlockNumber = -1; @@ -22,6 +30,9 @@ public final class PathStateRuntimeAttachment { private long observedBlockNumber = -1; private byte[] observedBlockHash; private PathStateBlockTransition pending; + private PathStateSnapshotDelta pendingSnapshotDelta; + private BlockChangeView pendingView; + private volatile boolean closed; private HeaderDiagnostic headerDiagnostic = HeaderDiagnostic.NONE; private long headerDiagnosticBlockNumber = -1; private byte[] headerDiagnosticBlockHash; @@ -37,10 +48,39 @@ public PathStateRuntimeAttachment(PathStateTransitionCollector collector, Transi public PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink, BaseFlushSink baseFlushSink, TransitionPreviewer previewer) { + this(collector, sink, baseFlushSink, previewer, null); + } + + public PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink, + BaseFlushSink baseFlushSink, TransitionPreviewer previewer, + SnapshotDeltaPreparer snapshotDeltaPreparer) { + this(collector, sink, baseFlushSink, previewer, snapshotDeltaPreparer, false); + } + + /** Creates a benchmark runtime that defers collect, delta preparation, and head advance. */ + public static PathStateRuntimeAttachment deferred(PathStateTransitionCollector collector, + TransitionSink sink, BaseFlushSink baseFlushSink, TransitionPreviewer previewer, + SnapshotDeltaPreparer snapshotDeltaPreparer) { + return new PathStateRuntimeAttachment(collector, sink, baseFlushSink, previewer, + snapshotDeltaPreparer, true); + } + + private PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink, + BaseFlushSink baseFlushSink, TransitionPreviewer previewer, + SnapshotDeltaPreparer snapshotDeltaPreparer, boolean deferredCapture) { this.collector = Objects.requireNonNull(collector, "collector"); this.sink = Objects.requireNonNull(sink, "sink"); this.baseFlushSink = Objects.requireNonNull(baseFlushSink, "baseFlushSink"); this.previewer = previewer; + this.snapshotDeltaPreparer = snapshotDeltaPreparer; + this.deferredCapture = deferredCapture; + deferredQueue = deferredCapture ? new ArrayBlockingQueue<>(64) : null; + deferredWorker = deferredCapture + ? new Thread(this::runDeferred, "path-state-deferred-capture") : null; + if (deferredWorker != null) { + deferredWorker.setDaemon(true); + deferredWorker.start(); + } } /** Computes producer metadata without observing, publishing, or failing this runtime. */ @@ -69,9 +109,17 @@ public synchronized PathStateBlockTransition capture(BlockChangeView view) { if (failure != null) { return null; } + if (deferredCapture) { + pendingView = admitted; + return null; + } try { PathStateBlockTransition transition = collectAndValidate(admitted); + PathStateSnapshotDelta snapshotDelta = snapshotDeltaPreparer == null ? null + : snapshotDeltaPreparer.prepare(admitted.getMeta(), transition); + validateSnapshotDelta(admitted.getMeta(), transition, snapshotDelta); pending = transition; + pendingSnapshotDelta = snapshotDelta; return transition; } catch (IOException | RuntimeException currentFailure) { fail(FailureStage.CAPTURE, currentFailure); @@ -80,7 +128,15 @@ public synchronized PathStateBlockTransition capture(BlockChangeView view) { } /** Durable publication failures are retained as observable fail-stop state. */ - public synchronized void publish(PathStateBlockTransition transition) { + public void publish(PathStateBlockTransition transition) { + if (deferredCapture) { + publishDeferred(transition); + return; + } + publishNow(transition); + } + + private synchronized void publishNow(PathStateBlockTransition transition) { if (failure != null || transition == null) { return; } @@ -92,11 +148,92 @@ public synchronized void publish(PathStateBlockTransition transition) { readyBlockNumber = transition.getBlockNumber(); readyBlockHash = transition.getBlockHash(); pending = null; + pendingSnapshotDelta = null; } catch (IOException | RuntimeException currentFailure) { fail(FailureStage.PUBLISH, currentFailure); } } + private void publishDeferred(PathStateBlockTransition transition) { + BlockChangeView admitted; + synchronized (this) { + if (failure != null) { + return; + } + if (transition != null || pendingView == null) { + fail(FailureStage.PUBLISH, + new IOException("deferred PathState publication has no captured view")); + return; + } + admitted = pendingView; + pendingView = null; + } + long startedNanos = System.nanoTime(); + try { + deferredQueue.put(admitted); + logger.info("Path-state deferred view enqueued: head={}, queueDepth={}, enqueueMicros={}", + admitted.getMeta().getBlockNumber(), deferredQueue.size(), + TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startedNanos)); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + fail(FailureStage.PUBLISH, interrupted); + } + } + + private void runDeferred() { + while (!closed || !deferredQueue.isEmpty()) { + try { + BlockChangeView view = deferredQueue.poll(100L, TimeUnit.MILLISECONDS); + if (view == null) { + continue; + } + long startedNanos = System.nanoTime(); + PathStateBlockTransition transition = collectAndValidate(view); + PathStateSnapshotDelta delta = snapshotDeltaPreparer == null ? null + : snapshotDeltaPreparer.prepare(view.getMeta(), transition); + validateSnapshotDelta(view.getMeta(), transition, delta); + sink.accept(transition); + synchronized (this) { + readyBlockNumber = transition.getBlockNumber(); + readyBlockHash = transition.getBlockHash(); + } + logger.info("Path-state deferred view prepared: head={}, queueDepth={}, serviceMs={}", + transition.getBlockNumber(), deferredQueue.size(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedNanos)); + } catch (InterruptedException interrupted) { + if (!closed) { + Thread.currentThread().interrupt(); + fail(FailureStage.CAPTURE, interrupted); + } + return; + } catch (IOException | RuntimeException currentFailure) { + fail(FailureStage.CAPTURE, currentFailure); + return; + } + } + } + + /** Drains the deferred benchmark worker before its Manager-owned head is closed. */ + public void close() throws IOException { + if (deferredWorker == null) { + return; + } + closed = true; + try { + deferredWorker.join(TimeUnit.SECONDS.toMillis(30)); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("deferred PathState close interrupted", interrupted); + } + if (deferredWorker.isAlive()) { + deferredWorker.interrupt(); + throw new IOException("deferred PathState worker did not drain before close"); + } + if (failure != null) { + throw new IOException("deferred PathState worker failed", failure); + } + } + /** Compacts only after Chainbase has durably refreshed the matching prefix. */ public synchronized void flushBaseThrough(long blockNumber, byte[] blockHash) { if (failure != null) { @@ -113,6 +250,15 @@ public synchronized boolean isFailed() { return failure != null; } + /** Returns the exact optional delta bound to the currently captured transition. */ + public synchronized PathStateSnapshotDelta preparedSnapshotDelta( + PathStateBlockTransition transition) { + if (pending != Objects.requireNonNull(transition, "transition")) { + throw new IllegalStateException("path-state Snapshot delta transition is not pending"); + } + return pendingSnapshotDelta; + } + public synchronized Throwable getFailure() { return failure; } @@ -203,7 +349,7 @@ private void observe(BlockChangeView view) { byte[] hash = view.getMeta().getBlockHash(); byte[] parentHash = view.getMeta().getParentHash(); if (failure == null) { - if (pending != null) { + if (pending != null || pendingView != null) { fail(FailureStage.CAPTURE_GAP, new IOException("path-state previous capture is not published")); } @@ -234,6 +380,18 @@ private PathStateBlockTransition collectAndValidate(BlockChangeView view) throws return transition; } + private static void validateSnapshotDelta(BlockSnapshotMeta meta, + PathStateBlockTransition transition, PathStateSnapshotDelta delta) throws IOException { + if (delta == null) { + return; + } + if (!meta.equals(delta.getMeta()) + || !Arrays.equals(transition.getPayloadDigest(), delta.getTransitionPayloadDigest()) + || !Arrays.equals(transition.getMutationViewDigest(), delta.getMutationViewDigest())) { + throw new IOException("path-state Snapshot delta identity mismatch"); + } + } + private static FailureKind classify(Throwable failure) { if (failure == null) { return FailureKind.NONE; @@ -407,4 +565,11 @@ public interface TransitionPreviewer { byte[] prepare(PathStateBlockTransition transition) throws IOException; } + + @FunctionalInterface + public interface SnapshotDeltaPreparer { + + PathStateSnapshotDelta prepare(BlockSnapshotMeta meta, PathStateBlockTransition transition) + throws IOException; + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java new file mode 100644 index 00000000000..a3d652ea4bf --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java @@ -0,0 +1,267 @@ +package org.tron.core.db2.stateroot; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +/** + * Immutable Snapshot-owned path-state forward delta for one successfully applied block. + * + *

This object contains the actual F/N mutations needed by a future common-checkpoint redo + * payload. It is memory-only in this slice and does not publish CURRENT or write a native Store. + */ +public final class PathStateSnapshotDelta { + + private static final Comparator MUTATION_ORDER = + (left, right) -> compareUnsigned(left.key, right.key); + + private final BlockSnapshotMeta meta; + private final byte[] parentStateRoot; + private final byte[] stateRoot; + private final byte[] transitionPayloadDigest; + private final byte[] mutationViewDigest; + private final List stores; + private final List superNodeMutations; + private final PathStateRoot.Snapshot trieSnapshot; + + private PathStateSnapshotDelta(BlockSnapshotMeta meta, byte[] parentStateRoot, + byte[] stateRoot, byte[] transitionPayloadDigest, byte[] mutationViewDigest, + List stores, List superNodeMutations, + PathStateRoot.Snapshot trieSnapshot) { + this.meta = Objects.requireNonNull(meta, "meta"); + this.parentStateRoot = root(parentStateRoot, "parentStateRoot"); + this.stateRoot = root(stateRoot, "stateRoot"); + this.transitionPayloadDigest = root(transitionPayloadDigest, "transitionPayloadDigest"); + this.mutationViewDigest = root(mutationViewDigest, "mutationViewDigest"); + this.stores = Collections.unmodifiableList(new ArrayList<>(stores)); + this.superNodeMutations = immutableMutations(superNodeMutations); + this.trieSnapshot = Objects.requireNonNull(trieSnapshot, "trieSnapshot"); + } + + static PathStateSnapshotDelta from(BlockSnapshotMeta meta, + PreparedPathStateTransition prepared, PathStateParticipantScope scope) { + BlockSnapshotMeta admittedMeta = Objects.requireNonNull(meta, "meta"); + PreparedPathStateTransition candidate = Objects.requireNonNull(prepared, "prepared"); + PathStateBlockTransition transition = candidate.getTransition(); + requireSameBlock(admittedMeta, transition); + PathStateParticipantScope admittedScope = Objects.requireNonNull(scope, "scope"); + + Map builders = new LinkedHashMap<>(); + for (PathStateMutation mutation : transition.getMutations()) { + PathStateParticipant participant = admittedScope.require(mutation.getDbName()); + StoreBuilder builder = builders.computeIfAbsent(participant.getStoreId(), + ignored -> new StoreBuilder(participant)); + byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), + mutation.getPhysicalKey()); + byte[] encodedValue = mutation.isDelete() ? null + : PathStateCommitmentCodec.presentLeafValue(mutation.getPhysicalValue()); + builder.flatMutations.add(new Mutation(secureKey, encodedValue)); + } + + List superMutations = new ArrayList<>(); + for (PreparedPathStateTransition.NodeMutation mutation : candidate.getNodeMutations()) { + Mutation forward = new Mutation(mutation.getPath(), mutation.getEncodedNode()); + if (mutation.getStoreId() == 0) { + superMutations.add(forward); + } else { + PathStateParticipant participant = participant(admittedScope, mutation.getStoreId()); + builders.computeIfAbsent(participant.getStoreId(), + ignored -> new StoreBuilder(participant)).nodeMutations.add(forward); + } + } + + List deltas = new ArrayList<>(); + for (PathStateParticipant participant : admittedScope.getParticipants()) { + StoreBuilder builder = builders.get(participant.getStoreId()); + if (builder != null) { + deltas.add(builder.freeze(candidate.getSnapshot().participantRoot( + participant.getDbName()))); + } + } + return new PathStateSnapshotDelta(admittedMeta, candidate.getParent().getStateRoot(), + candidate.getStateRoot(), transition.getPayloadDigest(), + transition.getMutationViewDigest(), deltas, superMutations, candidate.getSnapshot()); + } + + static PathStateSnapshotDelta fromPhysical(BlockSnapshotMeta meta, + PathStateRootMetadata parent, PathStateBlockTransition transition, + PathStateRoot.Snapshot snapshot, List stores, + List superNodeMutations) { + BlockSnapshotMeta admittedMeta = Objects.requireNonNull(meta, "meta"); + PathStateRootMetadata admittedParent = Objects.requireNonNull(parent, "parent"); + PathStateBlockTransition admittedTransition = Objects.requireNonNull(transition, + "transition"); + PathStateRoot.Snapshot admittedSnapshot = Objects.requireNonNull(snapshot, "snapshot"); + requireSameBlock(admittedMeta, admittedTransition); + return new PathStateSnapshotDelta(admittedMeta, admittedParent.getStateRoot(), + admittedSnapshot.getStateRoot(), admittedTransition.getPayloadDigest(), + admittedTransition.getMutationViewDigest(), stores, superNodeMutations, + admittedSnapshot); + } + + public BlockSnapshotMeta getMeta() { + return meta; + } + + public byte[] getParentStateRoot() { + return Arrays.copyOf(parentStateRoot, parentStateRoot.length); + } + + public byte[] getStateRoot() { + return Arrays.copyOf(stateRoot, stateRoot.length); + } + + public byte[] getMutationViewDigest() { + return Arrays.copyOf(mutationViewDigest, mutationViewDigest.length); + } + + public byte[] getTransitionPayloadDigest() { + return Arrays.copyOf(transitionPayloadDigest, transitionPayloadDigest.length); + } + + public List getStores() { + return stores; + } + + public List getSuperNodeMutations() { + return superNodeMutations; + } + + PathStateRoot.Snapshot getTrieSnapshot() { + return trieSnapshot; + } + + private static void requireSameBlock(BlockSnapshotMeta meta, + PathStateBlockTransition transition) { + if (meta.getBlockNumber() != transition.getBlockNumber() + || !Arrays.equals(meta.getBlockHash(), transition.getBlockHash()) + || !Arrays.equals(meta.getParentHash(), transition.getParentHash()) + || meta.getTimestamp() != transition.getTimestamp()) { + throw new IllegalArgumentException( + "path-state delta differs from Snapshot block identity"); + } + } + + private static PathStateParticipant participant(PathStateParticipantScope scope, int storeId) { + for (PathStateParticipant candidate : scope.getParticipants()) { + if (candidate.getStoreId() == storeId) { + return candidate; + } + } + throw new IllegalArgumentException("unknown path-state Store ID: " + storeId); + } + + private static byte[] root(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length != PathStateCommitmentCodec.ROOT_LENGTH) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return copy; + } + + private static List immutableMutations(List supplied) { + List copy = new ArrayList<>(Objects.requireNonNull(supplied, "mutations")); + copy.sort(MUTATION_ORDER); + for (int index = 1; index < copy.size(); index++) { + if (Arrays.equals(copy.get(index - 1).key, copy.get(index).key)) { + throw new IllegalArgumentException("duplicate path-state forward mutation key"); + } + } + return Collections.unmodifiableList(copy); + } + + private static int compareUnsigned(byte[] left, byte[] right) { + for (int index = 0; index < Math.min(left.length, right.length); index++) { + int compared = Integer.compare(left[index] & 0xff, right[index] & 0xff); + if (compared != 0) { + return compared; + } + } + return Integer.compare(left.length, right.length); + } + + public static final class StoreDelta { + + private final int storeId; + private final String dbName; + private final byte[] storeRoot; + private final List flatMutations; + private final List nodeMutations; + + StoreDelta(PathStateParticipant participant, byte[] storeRoot, + List flatMutations, List nodeMutations) { + this.storeId = participant.getStoreId(); + this.dbName = participant.getDbName(); + this.storeRoot = root(storeRoot, "storeRoot"); + this.flatMutations = immutableMutations(flatMutations); + this.nodeMutations = immutableMutations(nodeMutations); + if (this.flatMutations.isEmpty()) { + throw new IllegalArgumentException("changed path-state Store has no flat mutations"); + } + } + + public int getStoreId() { + return storeId; + } + + public String getDbName() { + return dbName; + } + + public byte[] getStoreRoot() { + return Arrays.copyOf(storeRoot, storeRoot.length); + } + + public List getFlatMutations() { + return flatMutations; + } + + public List getNodeMutations() { + return nodeMutations; + } + } + + public static final class Mutation { + + private final byte[] key; + private final byte[] value; + + Mutation(byte[] key, byte[] value) { + this.key = Arrays.copyOf(Objects.requireNonNull(key, "key"), key.length); + this.value = value == null ? null : Arrays.copyOf(value, value.length); + } + + public byte[] getKey() { + return Arrays.copyOf(key, key.length); + } + + public byte[] getValue() { + return value == null ? null : Arrays.copyOf(value, value.length); + } + + public boolean isDelete() { + return value == null; + } + } + + private static final class StoreBuilder { + + private final PathStateParticipant participant; + private final List flatMutations = new ArrayList<>(); + private final List nodeMutations = new ArrayList<>(); + + private StoreBuilder(PathStateParticipant participant) { + this.participant = participant; + } + + private StoreDelta freeze(byte[] storeRoot) { + return new StoreDelta(participant, storeRoot, flatMutations, nodeMutations); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java index 9629a17f660..8d9d4aaff8e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotHead.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Objects; +import org.tron.core.db2.archive.BlockSnapshotMeta; /** In-process snapshot authority that advances only with the durable block-final CURRENT head. */ public final class PathStateSnapshotHead implements PathStateHead { @@ -105,6 +106,12 @@ public synchronized byte[] preview(PathStateBlockTransition transition) throws I return prepare(transition).getStateRoot(); } + @Override + public synchronized PathStateSnapshotDelta prepareSnapshotDelta(BlockSnapshotMeta meta, + PathStateBlockTransition transition) throws IOException { + return prepare(transition).toSnapshotDelta(Objects.requireNonNull(meta, "meta")); + } + /** Publishes one exact prepared child and adopts it only after CURRENT confirms durability. */ public synchronized PathStateRootMetadata advancePrepared( PreparedPathStateTransition prepared) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PreparedPathStateTransition.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PreparedPathStateTransition.java index f5e7ff0d69a..dd4c8c928ca 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PreparedPathStateTransition.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PreparedPathStateTransition.java @@ -4,9 +4,13 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import org.tron.common.crypto.Hash; +import org.tron.core.db2.archive.BlockSnapshotMeta; /** Immutable, memory-only candidate trie result for one exact block transition. */ public final class PreparedPathStateTransition { @@ -60,6 +64,12 @@ public int getNodeMutationCount() { return nodeMutations.size(); } + /** Freezes this candidate as one immutable Snapshot-owned forward delta. */ + public PathStateSnapshotDelta toSnapshotDelta(BlockSnapshotMeta meta) { + return PathStateSnapshotDelta.from(meta, this, + new PathStateCanonicalizer().participantScope()); + } + PathStateRootMetadata getParent() { return parent; } @@ -80,6 +90,54 @@ boolean extendsParent(PathStateRootMetadata expected) { return Arrays.equals(parent.encode(), expected.encode()); } + /** Validates the immutable candidate without reading any path-state Store. */ + void validatePreparedTransition(PathStateParticipantScope scope) { + PathStateParticipantScope admittedScope = Objects.requireNonNull(scope, "scope"); + if (transition.getBlockNumber() != parent.getBlockNumber() + 1 + || !Arrays.equals(transition.getParentHash(), parent.getBlockHash())) { + throw new IllegalStateException("path-state prepared transition no longer extends parent"); + } + byte[] candidateRoot = snapshot.getStateRoot(); + if (candidateRoot.length != PathStateCommitmentCodec.ROOT_LENGTH) { + throw new IllegalStateException("path-state prepared root must contain exactly 32 bytes"); + } + Set admittedStoreIds = new LinkedHashSet<>(); + admittedStoreIds.add(0); + for (PathStateParticipant participant : admittedScope.getParticipants()) { + admittedStoreIds.add(participant.getStoreId()); + } + Set unique = new LinkedHashSet<>(); + NodeMutation superRoot = null; + for (NodeMutation mutation : nodeMutations) { + if (!admittedStoreIds.contains(mutation.storeId)) { + throw new IllegalStateException("prepared path-state node has unknown Store ID"); + } + if (mutation.path.length > PathMerkleTrie.SECURE_KEY_LENGTH * 2) { + throw new IllegalStateException("prepared path-state node path is too long"); + } + for (byte nibble : mutation.path) { + if (nibble < 0 || nibble > 15) { + throw new IllegalStateException("prepared path-state node path is not nibble encoded"); + } + } + if (!unique.add(new NodeMutationKey(mutation.storeId, mutation.path))) { + throw new IllegalStateException("duplicate prepared path-state node mutation"); + } + if (mutation.encodedNode != null && mutation.encodedNode.length == 0) { + throw new IllegalStateException("prepared path-state node encoding is empty"); + } + if (mutation.storeId == 0 && mutation.path.length == 0) { + superRoot = mutation; + } + } + if (!Arrays.equals(candidateRoot, parent.getStateRoot())) { + if (superRoot == null || superRoot.encodedNode == null + || !Arrays.equals(Hash.sha3(superRoot.encodedNode), candidateRoot)) { + throw new IllegalStateException("prepared path-state super root mutation is inconsistent"); + } + } + } + private static void requireChild(PathStateRootMetadata parent, PathStateRoot.Snapshot snapshot, PathStateBlockTransition transition) { if (!Arrays.equals(parent.getStateRoot(), snapshot.getStateRoot())) { @@ -163,4 +221,27 @@ public int hashCode() { return Arrays.hashCode(bytes); } } + + private static final class NodeMutationKey { + + private final int storeId; + private final byte[] path; + + private NodeMutationKey(int storeId, byte[] path) { + this.storeId = storeId; + this.path = Arrays.copyOf(path, path.length); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof NodeMutationKey + && storeId == ((NodeMutationKey) other).storeId + && Arrays.equals(path, ((NodeMutationKey) other).path); + } + + @Override + public int hashCode() { + return 31 * storeId + Arrays.hashCode(path); + } + } } diff --git a/common/src/main/java/org/tron/core/config/args/Storage.java b/common/src/main/java/org/tron/core/config/args/Storage.java index 46c188d83dc..54d8f8bd618 100644 --- a/common/src/main/java/org/tron/core/config/args/Storage.java +++ b/common/src/main/java/org/tron/core/config/args/Storage.java @@ -129,6 +129,18 @@ public class Storage { @Setter private long pathStateRootWriteBufferBytes; + @Getter + @Setter + private long pathStateRootNodeCacheBytes; + + @Getter + @Setter + private int pathStateRootParticipantThreads; + + @Getter + @Setter + private int pathStateRootBranchThreads; + @Getter @Setter private boolean pathStateRootRebuildFromGenesis; @@ -137,6 +149,16 @@ public class Storage { @Setter private boolean pathStateRootVerifyEveryBlock; + /** Benchmark-only: advance the path-state head in memory and persist no per-block state. */ + @Getter + @Setter + private boolean pathStateRootVolatileSnapshotBenchmark; + + /** Benchmark-only: compute ordered PathState deltas on a bounded background worker. */ + @Getter + @Setter + private boolean pathStateRootAsyncPrepareBenchmark; + private Options defaultDbOptions; @Getter diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index 38c33c22516..ce381fbb631 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -178,8 +178,13 @@ public static class PathStateRootConfig { private int reversibleLayerLimit = 128; private long reversibleLayerBytes = 2147483648L; private long writeBufferBytes = 268435456L; + private long nodeCacheBytes = 268435456L; + private int participantThreads = 4; + private int branchThreads = 8; private boolean rebuildFromGenesis = false; private boolean verifyEveryBlock = true; + private boolean volatileSnapshotBenchmark = false; + private boolean asyncPrepareBenchmark = false; void postProcess() { if (!"shadow".equals(mode)) { @@ -191,15 +196,25 @@ void postProcess() { if (formatVersion != 1) { throw new IllegalArgumentException("pathStateRoot.formatVersion must be 1"); } - if (reversibleLayerLimit <= 0 || reversibleLayerBytes <= 0 || writeBufferBytes <= 0) { + if (reversibleLayerLimit <= 0 || reversibleLayerBytes <= 0 || writeBufferBytes <= 0 + || nodeCacheBytes <= 0) { throw new IllegalArgumentException("pathStateRoot limits must be positive"); } + if (participantThreads <= 0 || participantThreads > 64 + || branchThreads <= 0 || branchThreads > 64) { + throw new IllegalArgumentException( + "pathStateRoot prepare threads must be in [1, 64]"); + } if (rebuildFromGenesis) { throw new IllegalArgumentException("pathStateRoot.rebuildFromGenesis is not supported"); } if (!verifyEveryBlock) { throw new IllegalArgumentException("pathStateRoot.verifyEveryBlock must remain enabled"); } + if (asyncPrepareBenchmark && !volatileSnapshotBenchmark) { + throw new IllegalArgumentException( + "pathStateRoot.asyncPrepareBenchmark requires volatileSnapshotBenchmark"); + } } } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 894326c7e5b..178e46465ab 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -146,8 +146,14 @@ storage { pathStateRoot.reversibleLayerLimit = 128 pathStateRoot.reversibleLayerBytes = 2147483648 # 2 GiB pathStateRoot.writeBufferBytes = 268435456 # 256 MiB + pathStateRoot.nodeCacheBytes = 268435456 # 256 MiB + pathStateRoot.participantThreads = 4 + pathStateRoot.branchThreads = 8 pathStateRoot.rebuildFromGenesis = false pathStateRoot.verifyEveryBlock = true + # Benchmark-only. Advances PathState in memory and writes no per-block journal/F/N/CURRENT. + pathStateRoot.volatileSnapshotBenchmark = false + pathStateRoot.asyncPrepareBenchmark = false # Data root setting, for check data, currently only reward-vi is used. # merkleRoot = { diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java index 86155b7ea97..4e934557ba9 100644 --- a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -88,19 +88,36 @@ public void testPathStateRootDefaultsAndOverrides() { assertEquals(128, defaults.getPathStateRoot().getReversibleLayerLimit()); assertEquals(2147483648L, defaults.getPathStateRoot().getReversibleLayerBytes()); assertEquals(268435456L, defaults.getPathStateRoot().getWriteBufferBytes()); + assertEquals(268435456L, defaults.getPathStateRoot().getNodeCacheBytes()); + assertEquals(4, defaults.getPathStateRoot().getParticipantThreads()); + assertEquals(8, defaults.getPathStateRoot().getBranchThreads()); assertFalse(defaults.getPathStateRoot().isRebuildFromGenesis()); assertTrue(defaults.getPathStateRoot().isVerifyEveryBlock()); + assertFalse(defaults.getPathStateRoot().isVolatileSnapshotBenchmark()); + assertFalse(defaults.getPathStateRoot().isAsyncPrepareBenchmark()); StorageConfig configured = StorageConfig.fromConfig(withRef( "storage.pathStateRoot { enabled = true, mode = shadow, directory = root-test, " + "formatVersion = 1, reversibleLayerLimit = 8, reversibleLayerBytes = 4096, " - + "writeBufferBytes = 1024, rebuildFromGenesis = false, " - + "verifyEveryBlock = true }")); + + "writeBufferBytes = 1024, nodeCacheBytes = 2048, participantThreads = 2, " + + "branchThreads = 3, rebuildFromGenesis = false, " + + "verifyEveryBlock = true, volatileSnapshotBenchmark = true, " + + "asyncPrepareBenchmark = true }")); assertTrue(configured.getPathStateRoot().isEnabled()); assertEquals("root-test", configured.getPathStateRoot().getDirectory()); assertEquals(8, configured.getPathStateRoot().getReversibleLayerLimit()); assertEquals(4096L, configured.getPathStateRoot().getReversibleLayerBytes()); assertEquals(1024L, configured.getPathStateRoot().getWriteBufferBytes()); + assertEquals(2048L, configured.getPathStateRoot().getNodeCacheBytes()); + assertEquals(2, configured.getPathStateRoot().getParticipantThreads()); + assertEquals(3, configured.getPathStateRoot().getBranchThreads()); + assertTrue(configured.getPathStateRoot().isVolatileSnapshotBenchmark()); + assertTrue(configured.getPathStateRoot().isAsyncPrepareBenchmark()); + } + + @Test(expected = IllegalArgumentException.class) + public void testPathStateRootRejectsInvalidPrepareThreads() { + StorageConfig.fromConfig(withRef("storage.pathStateRoot.participantThreads = 0")); } @Test(expected = IllegalArgumentException.class) diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 5e1dba4b7f6..429f309ead8 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -231,10 +231,20 @@ private static void applyStorageConfig(StorageConfig sc) { sc.getPathStateRoot().getReversibleLayerBytes()); PARAMETER.storage.setPathStateRootWriteBufferBytes( sc.getPathStateRoot().getWriteBufferBytes()); + PARAMETER.storage.setPathStateRootNodeCacheBytes( + sc.getPathStateRoot().getNodeCacheBytes()); + PARAMETER.storage.setPathStateRootParticipantThreads( + sc.getPathStateRoot().getParticipantThreads()); + PARAMETER.storage.setPathStateRootBranchThreads( + sc.getPathStateRoot().getBranchThreads()); PARAMETER.storage.setPathStateRootRebuildFromGenesis( sc.getPathStateRoot().isRebuildFromGenesis()); PARAMETER.storage.setPathStateRootVerifyEveryBlock( sc.getPathStateRoot().isVerifyEveryBlock()); + PARAMETER.storage.setPathStateRootVolatileSnapshotBenchmark( + sc.getPathStateRoot().isVolatileSnapshotBenchmark()); + PARAMETER.storage.setPathStateRootAsyncPrepareBenchmark( + sc.getPathStateRoot().isAsyncPrepareBenchmark()); // estimatedTransactions / maxFlushCount clamping & validation run inside // TxCacheConfig.postProcess / SnapshotConfig.postProcess during bean load. diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 46c7c5f4ec4..ea91d47daec 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -137,6 +137,7 @@ import org.tron.core.db2.stateroot.PathStateHead; import org.tron.core.db2.stateroot.PathStateLayerLimits; import org.tron.core.db2.stateroot.PathStateNativeSnapshotSource; +import org.tron.core.db2.stateroot.PathStatePhysicalOverlayHead; import org.tron.core.db2.stateroot.PathStatePhysicalRuntimeAdmission; import org.tron.core.db2.stateroot.PathStatePhysicalSnapshotHead; import org.tron.core.db2.stateroot.PathStatePhysicalStoreSet; @@ -766,9 +767,15 @@ private void initPathStateRoot() { throw new IllegalStateException( "Path-state startup requires a completed admitted rebuild"); } - recovered = PathStatePhysicalSnapshotHead.open(directory, engine, - new PathStateLayerLimits(storage.getPathStateRootReversibleLayerLimit(), - storage.getPathStateRootReversibleLayerBytes())); + PathStateLayerLimits limits = new PathStateLayerLimits( + storage.getPathStateRootReversibleLayerLimit(), + storage.getPathStateRootReversibleLayerBytes()); + recovered = storage.isPathStateRootVolatileSnapshotBenchmark() + ? PathStatePhysicalOverlayHead.open(directory, engine, limits, + storage.getPathStateRootNodeCacheBytes(), + storage.getPathStateRootParticipantThreads(), + storage.getPathStateRootBranchThreads()) + : PathStatePhysicalSnapshotHead.open(directory, engine, limits); PathStateRootMetadata recoveredHead = recovered.getHead(); if (recoveredHead.getBlockNumber() != getDynamicPropertiesStore().getLatestBlockHeaderNumber() @@ -779,8 +786,15 @@ private void initPathStateRoot() { } pathStateSnapshotHead = recovered; attachPathStateBlockFinalRuntime(); - logger.info("Path-state current root attached: directory={}, head={}, engine={}", - directory, recoveredHead.getBlockNumber(), storage.getDbEngine()); + logger.info("Path-state current root attached: directory={}, head={}, engine={}, " + + "volatileSnapshotBenchmark={}, asyncPrepareBenchmark={}, nodeCacheBytes={}, " + + "participantThreads={}, branchThreads={}", + directory, + recoveredHead.getBlockNumber(), storage.getDbEngine(), + storage.isPathStateRootVolatileSnapshotBenchmark(), + storage.isPathStateRootAsyncPrepareBenchmark(), + storage.getPathStateRootNodeCacheBytes(), storage.getPathStateRootParticipantThreads(), + storage.getPathStateRootBranchThreads()); recovered = null; } catch (java.io.IOException | RuntimeException failure) { pathStateSnapshotHead = null; @@ -804,11 +818,18 @@ private void attachPathStateBlockFinalRuntime() throws java.io.IOException { throw new IllegalStateException( "Path-state block-final capture requires account-asset Store"); } - PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment( - new SnapshotPathStateTransitionCollector(accountAssetStore::prefixQuery, - this::scanPathStateActivationAccounts), - this::advancePathStateRoot, this::flushPathStateBaseThrough, - transition -> pathStateSnapshotHead.preview(transition)); + SnapshotPathStateTransitionCollector collector = new SnapshotPathStateTransitionCollector( + accountAssetStore::prefixQuery, this::scanPathStateActivationAccounts); + PathStateRuntimeAttachment attachment = Args.getInstance().getStorage() + .isPathStateRootAsyncPrepareBenchmark() + ? PathStateRuntimeAttachment.deferred(collector, this::advancePathStateRoot, + this::flushPathStateBaseThrough, + transition -> pathStateSnapshotHead.preview(transition), + (meta, transition) -> pathStateSnapshotHead.prepareSnapshotDelta(meta, transition)) + : new PathStateRuntimeAttachment(collector, this::advancePathStateRoot, + this::flushPathStateBaseThrough, + transition -> pathStateSnapshotHead.preview(transition), + (meta, transition) -> pathStateSnapshotHead.prepareSnapshotDelta(meta, transition)); attachment.synchronizeReadyHead(pathStateSnapshotHead.getHead()); ((SnapshotManager) revokingStore).attachPathStateRuntime(attachment); pathStateRuntime = attachment; @@ -1531,15 +1552,24 @@ private void applyBlock(BlockCapsule block, List txs) TooBigTransactionException, DupTransactionException, TaposException, ValidateScheduleException, ReceiptCheckErrException, VMIllegalException, TooBigTransactionResultException, ZksnarkException, BadBlockException, EventBloomException { + long startedNanos = System.nanoTime(); processBlock(block, txs); + long processedNanos = System.nanoTime(); chainBaseManager.getBlockStore().put(block.getBlockId().getBytes(), block); chainBaseManager.getBlockIndexStore().put(block.getBlockId()); if (block.getTransactions().size() != 0) { chainBaseManager.getTransactionRetStore() .put(ByteArray.fromLong(block.getNum()), block.getResult()); } + long storedNanos = System.nanoTime(); updateFork(block); + long forkUpdatedNanos = System.nanoTime(); + logger.info("ApplyBlock outer stages: head={}, processMs={}, storeMs={}, forkMs={}, totalMs={}", + block.getNum(), elapsedMillis(startedNanos, processedNanos), + elapsedMillis(processedNanos, storedNanos), + elapsedMillis(storedNanos, forkUpdatedNanos), + elapsedMillis(startedNanos, forkUpdatedNanos)); if (System.currentTimeMillis() - block.getTimeStamp() >= 60_000) { revokingStore.setMaxFlushCount(maxFlushCount); if (Args.getInstance().getShutdownBlockTime() != null @@ -1795,7 +1825,9 @@ public void pushBlock(final BlockCapsule block) final Histogram.Timer timer = Metrics.histogramStartTimer( MetricKeys.Histogram.BLOCK_PUSH_LATENCY); long start = System.currentTimeMillis(); + long startedNanos = System.nanoTime(); List txs = getVerifyTxs(block); + long verifiedNanos = System.nanoTime(); logger.info("Block num: {}, re-push-size: {}, pending-size: {}, " + "block-tx-size: {}, verify-tx-size: {}", block.getNum(), rePushTransactions.size(), pendingTransactions.size(), @@ -1883,9 +1915,14 @@ public void pushBlock(final BlockCapsule block) return; } long oldSolidNum = getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); + long applyStartedNanos = System.nanoTime(); + long appliedNanos; + long committedNanos; try (ISession tmpSession = revokingStore.buildSession()) { applyBlock(newBlock, txs); + appliedNanos = System.nanoTime(); commitBlockSession(tmpSession, newBlock); + committedNanos = System.nanoTime(); } catch (Throwable throwable) { logger.error(throwable.getMessage(), throwable); khaosDb.removeBlk(block.getBlockId()); @@ -1894,6 +1931,15 @@ public void pushBlock(final BlockCapsule block) } long newSolidNum = getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); blockTrigger(newBlock, oldSolidNum, newSolidNum); + long triggeredNanos = System.nanoTime(); + logger.info("PushBlock core stages: head={}, verifyMs={}, preApplyMs={}, applyMs={}, " + + "commitMs={}, triggerMs={}, throughTriggerMs={}", newBlock.getNum(), + elapsedMillis(startedNanos, verifiedNanos), + elapsedMillis(verifiedNanos, applyStartedNanos), + elapsedMillis(applyStartedNanos, appliedNanos), + elapsedMillis(appliedNanos, committedNanos), + elapsedMillis(committedNanos, triggeredNanos), + elapsedMillis(startedNanos, triggeredNanos)); } logger.info(SAVE_BLOCK, newBlock); } @@ -1923,6 +1969,10 @@ public void pushBlock(final BlockCapsule block) } } + private static long elapsedMillis(long startedNanos, long completedNanos) { + return TimeUnit.NANOSECONDS.toMillis(completedNanos - startedNanos); + } + void blockTrigger(final BlockCapsule block, long oldSolid, long newSolid) { // post block and logs for jsonrpc try { @@ -2369,6 +2419,7 @@ private void processBlock(BlockCapsule block, List txs) DupTransactionException, TransactionExpirationException, ValidateScheduleException, ReceiptCheckErrException, VMIllegalException, TooBigTransactionResultException, ZksnarkException, BadBlockException, EventBloomException { + long startedNanos = System.nanoTime(); // todo set revoking db max size. // checkWitness @@ -2393,6 +2444,7 @@ private void processBlock(BlockCapsule block, List txs) TransactionRetCapsule transactionRetCapsule = new TransactionRetCapsule(block); HistoryBlockHashUtil.write(this, block); + long transactionStartedNanos = System.nanoTime(); try { merkleContainer.resetCurrentMerkleTree(); accountStateCallBack.preExecute(block); @@ -2422,6 +2474,7 @@ private void processBlock(BlockCapsule block, List txs) } finally { accountStateCallBack.exceptionFinish(); } + long transactionCompletedNanos = System.nanoTime(); merkleContainer.saveCurrentMerkleTreeAsBestMerkleTree(block.getNum()); block.setResult(transactionRetCapsule); if (getDynamicPropertiesStore().getAllowAdaptiveEnergy() == 1) { @@ -2458,6 +2511,12 @@ private void processBlock(BlockCapsule block, List txs) .initBlockSection(transactionRetCapsule); chainBaseManager.getSectionBloomStore().write(block.getNum()); block.setBloom(blockBloom); + long completedNanos = System.nanoTime(); + logger.info("ProcessBlock stages: head={}, preTxMs={}, txLoopMs={}, postTxMs={}, totalMs={}", + block.getNum(), elapsedMillis(startedNanos, transactionStartedNanos), + elapsedMillis(transactionStartedNanos, transactionCompletedNanos), + elapsedMillis(transactionCompletedNanos, completedNanos), + elapsedMillis(startedNanos, completedNanos)); } private void payReward(BlockCapsule block) { @@ -3206,6 +3265,11 @@ private void closePathStateRoot() { throw new IllegalStateException("Path-state runtime lost SnapshotManager ownership"); } ((SnapshotManager) revokingStore).detachPathStateRuntime(runtime); + try { + runtime.close(); + } catch (java.io.IOException failure) { + throw new IllegalStateException("Failed to close PathState runtime", failure); + } pathStateRuntime = null; } PathStateHead owner = pathStateSnapshotHead; diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index be691371a17..0ac85c5b000 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -51,8 +51,14 @@ storage { pathStateRoot.reversibleLayerLimit = 128 pathStateRoot.reversibleLayerBytes = 2147483648 pathStateRoot.writeBufferBytes = 268435456 + pathStateRoot.nodeCacheBytes = 268435456 + pathStateRoot.participantThreads = 4 + pathStateRoot.branchThreads = 8 pathStateRoot.rebuildFromGenesis = false pathStateRoot.verifyEveryBlock = true + # Benchmark-only. Not restart-safe until common-checkpoint flush is installed. + pathStateRoot.volatileSnapshotBenchmark = false + pathStateRoot.asyncPrepareBenchmark = false # If true, transaction cache initialization will be faster. Default: false txCache.initOptimization = true diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 9d97d381873..3e5628f73e0 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -319,6 +319,11 @@ public void testPathStateRootStorageConfigMapping() { override.put("storage.pathStateRoot.directory", "root-mapped"); override.put("storage.pathStateRoot.reversibleLayerLimit", "9"); override.put("storage.pathStateRoot.reversibleLayerBytes", "8192"); + override.put("storage.pathStateRoot.volatileSnapshotBenchmark", "true"); + override.put("storage.pathStateRoot.nodeCacheBytes", "1073741824"); + override.put("storage.pathStateRoot.participantThreads", "2"); + override.put("storage.pathStateRoot.branchThreads", "3"); + override.put("storage.pathStateRoot.asyncPrepareBenchmark", "true"); Config config = ConfigFactory.parseMap(override) .withFallback(ConfigFactory.defaultReference()); @@ -332,8 +337,13 @@ public void testPathStateRootStorageConfigMapping() { Assert.assertEquals(9, storage.getPathStateRootReversibleLayerLimit()); Assert.assertEquals(8192L, storage.getPathStateRootReversibleLayerBytes()); Assert.assertEquals(268435456L, storage.getPathStateRootWriteBufferBytes()); + Assert.assertEquals(1073741824L, storage.getPathStateRootNodeCacheBytes()); + Assert.assertEquals(2, storage.getPathStateRootParticipantThreads()); + Assert.assertEquals(3, storage.getPathStateRootBranchThreads()); Assert.assertFalse(storage.isPathStateRootRebuildFromGenesis()); Assert.assertTrue(storage.isPathStateRootVerifyEveryBlock()); + Assert.assertTrue(storage.isPathStateRootVolatileSnapshotBenchmark()); + Assert.assertTrue(storage.isPathStateRootAsyncPrepareBenchmark()); Args.clearParam(); } diff --git a/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java b/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java index 3ee61065d1f..760619e852e 100644 --- a/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java +++ b/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java @@ -2,14 +2,24 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; import org.junit.Test; import org.tron.common.BaseMethodTest; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.core.Snapshot; import org.tron.core.db2.core.SnapshotImpl; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.db2.core.SnapshotRoot; +import org.tron.core.db2.stateroot.PathStateSnapshotDelta; public class SnapshotImplTest extends BaseMethodTest { private RevokingDbWithCacheNewValueTest.TestRevokingTronStore tronDatabase; @@ -161,6 +171,53 @@ public void testMergeOverride() throws Exception { assertEquals(new String("value4".getBytes()), new String(s4)); } + @Test + public void testAttachBlockArtifactsRequiresOneSnapshotIdentity() throws Exception { + SnapshotRoot root = new SnapshotRoot(tronDatabase.getDb()); + SnapshotImpl layer = getSnapshotImplIns(root); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); + PathStateSnapshotDelta delta = mock(PathStateSnapshotDelta.class); + when(delta.getMeta()).thenReturn(meta); + + attachBlockArtifacts(layer, meta, null, delta); + + assertEquals(meta, layer.getBlockSnapshotMeta()); + assertSame(delta, layer.getPreparedPathStateDelta()); + + PathStateSnapshotDelta wrong = mock(PathStateSnapshotDelta.class); + when(wrong.getMeta()).thenReturn( + BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 6_000L)); + InvocationTargetException failure = assertThrows(InvocationTargetException.class, + () -> attachBlockArtifacts(layer, meta, null, wrong)); + assertEquals(IllegalArgumentException.class, failure.getCause().getClass()); + assertSame(delta, layer.getPreparedPathStateDelta()); + } + + @Test + public void testAttachBlockArtifactsRequiresOneMutationViewIdentity() throws Exception { + SnapshotRoot root = new SnapshotRoot(tronDatabase.getDb()); + SnapshotImpl layer = getSnapshotImplIns(root); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); + byte[] viewDigest = hash(7); + BlockReverseDiff reverseDiff = new BlockReverseDiff(meta, Collections.emptyList(), + viewDigest); + PathStateSnapshotDelta delta = mock(PathStateSnapshotDelta.class); + when(delta.getMeta()).thenReturn(meta); + when(delta.getMutationViewDigest()).thenReturn(viewDigest); + + attachBlockArtifacts(layer, meta, reverseDiff, delta); + assertSame(reverseDiff, layer.getPreparedArchiveBlock()); + assertSame(delta, layer.getPreparedPathStateDelta()); + + PathStateSnapshotDelta wrong = mock(PathStateSnapshotDelta.class); + when(wrong.getMeta()).thenReturn(meta); + when(wrong.getMutationViewDigest()).thenReturn(hash(8)); + InvocationTargetException failure = assertThrows(InvocationTargetException.class, + () -> attachBlockArtifacts(layer, meta, reverseDiff, wrong)); + assertEquals(IllegalArgumentException.class, failure.getCause().getClass()); + assertSame(delta, layer.getPreparedPathStateDelta()); + } + /** * The constructor of SnapshotImpl is not public * so reflection is used to construct the object here. @@ -172,4 +229,20 @@ private SnapshotImpl getSnapshotImplIns(Snapshot snapshot) throws Exception { return (SnapshotImpl) constructor.newInstance(snapshot); } + private void attachBlockArtifacts(SnapshotImpl snapshot, BlockSnapshotMeta meta, + BlockReverseDiff reverseDiff, PathStateSnapshotDelta delta) throws Exception { + Method method = SnapshotImpl.class.getDeclaredMethod("attachBlockArtifacts", + BlockSnapshotMeta.class, BlockReverseDiff.class, PathStateSnapshotDelta.class); + method.setAccessible(true); + method.invoke(snapshot, meta, reverseDiff, delta); + } + + private static byte[] hash(int seed) { + byte[] hash = new byte[32]; + for (int index = 0; index < hash.length; index++) { + hash[index] = (byte) (seed + index); + } + return hash; + } + } diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index a4e5fb25da5..e384f38d338 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -35,6 +35,8 @@ import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; @@ -61,6 +63,7 @@ import org.tron.core.db2.stateroot.PathStateRootMetadata; import org.tron.core.db2.stateroot.PathStateRuntimeAdmission; import org.tron.core.db2.stateroot.PathStateRuntimeAttachment; +import org.tron.core.db2.stateroot.PathStateSnapshotDelta; import org.tron.core.db2.stateroot.PathStateSnapshotHead; import org.tron.core.db2.stateroot.PathStateStoreManifest; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; @@ -72,6 +75,52 @@ public class SnapshotOldValueCollectorTest extends BaseMethodTest { + @Test + public void deferredPathStateCaptureDoesNotWaitForCollectorAndDrainsOnClose() + throws Exception { + MemoryDb codeDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase code = new Chainbase(new SnapshotRoot(codeDb)); + manager.add(code); + manager.enable(); + CountDownLatch collecting = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicReference published = new AtomicReference<>(); + PathStateRuntimeAttachment attachment = PathStateRuntimeAttachment.deferred(view -> { + collecting.countDown(); + try { + if (!release.await(5, TimeUnit.SECONDS)) { + throw new IOException("timed out waiting to release deferred collector"); + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("deferred collector interrupted", interrupted); + } + BlockSnapshotMeta meta = view.getMeta(); + return new PathStateBlockTransition(meta.getBlockNumber(), meta.getBlockHash(), + meta.getParentHash(), meta.getTimestamp(), P66Phase.P66_ON, + Collections.singletonList( + PathStateMutation.put("code", bytes("contract"), bytes("runtime"))), + view.getMutationViewDigest()); + }, published::set, (blockNumber, blockHash) -> { }, null, (meta, transition) -> null); + attachment.synchronizeReadyHead(PathStateRootMetadata.base(0, hash(0), hash(9), 0, + P66Phase.P66_ON, hash(7), hash(8), hash(6))); + manager.attachPathStateRuntime(attachment); + + try (ISession block = manager.buildSession()) { + code.put(bytes("contract"), bytes("runtime")); + block.commit(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L)); + } + assertTrue(collecting.await(5, TimeUnit.SECONDS)); + assertNull(published.get()); + release.countDown(); + attachment.close(); + assertEquals(1, published.get().getBlockNumber()); + assertEquals(PathStateRuntimeAttachment.State.READY, attachment.status().getState()); + assertSame(attachment, manager.detachPathStateRuntime(attachment)); + manager.shutdown(); + } + @Test public void pathStateRuntimeCoexistsWithArchiveAtBlockFinalBoundary() throws Exception { MemoryDb propertiesDb = new MemoryDb("properties"); @@ -86,8 +135,15 @@ public void pathStateRuntimeCoexistsWithArchiveAtBlockFinalBoundary() throws Exc manager.enable(); manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); AtomicReference published = new AtomicReference<>(); + AtomicReference capturedViewDigest = new AtomicReference<>(); + SnapshotPathStateTransitionCollector collector = new SnapshotPathStateTransitionCollector( + key -> Collections.emptyMap()); PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment( - new SnapshotPathStateTransitionCollector(key -> Collections.emptyMap()), published::set); + view -> { + capturedViewDigest.set(view.getMutationViewDigest()); + return collector.collect(view); + }, published::set, + (blockNumber, blockHash) -> { }, null, (meta, transition) -> null); manager.attachPathStateRuntime(attachment); byte[] key = bytes("contract"); @@ -101,10 +157,55 @@ public void pathStateRuntimeCoexistsWithArchiveAtBlockFinalBoundary() throws Exc assertEquals(1, published.get().getMutations().size()); assertEquals("code", published.get().getMutations().get(0).getDbName()); assertArrayEquals(key, published.get().getMutations().get(0).getCanonicalKey()); + assertArrayEquals(capturedViewDigest.get(), published.get().getMutationViewDigest()); + assertFalse(Arrays.equals(published.get().getPayloadDigest(), + published.get().getMutationViewDigest())); assertSame(attachment, manager.detachPathStateRuntime(attachment)); manager.shutdown(); } + @Test + public void pathStateForwardDeltaIsOwnedByTheSameBlockSnapshotLayer() throws Exception { + MemoryDb codeDb = new MemoryDb("code"); + SnapshotManager manager = new SnapshotManager(""); + Chainbase code = new Chainbase(new SnapshotRoot(codeDb)); + manager.add(code); + manager.enable(); + manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + AtomicReference published = new AtomicReference<>(); + AtomicReference prepared = new AtomicReference<>(); + PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment(view -> { + BlockSnapshotMeta meta = view.getMeta(); + return new PathStateBlockTransition(meta.getBlockNumber(), meta.getBlockHash(), + meta.getParentHash(), meta.getTimestamp(), P66Phase.P66_ON, + Collections.singletonList( + PathStateMutation.put("code", bytes("contract"), bytes("runtime"))), + view.getMutationViewDigest()); + }, published::set, (blockNumber, blockHash) -> { }, null, (meta, transition) -> { + PathStateSnapshotDelta delta = mock(PathStateSnapshotDelta.class); + when(delta.getMeta()).thenReturn(meta); + when(delta.getTransitionPayloadDigest()).thenReturn(transition.getPayloadDigest()); + when(delta.getMutationViewDigest()).thenReturn(transition.getMutationViewDigest()); + prepared.set(delta); + return delta; + }); + manager.attachPathStateRuntime(attachment); + + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); + try (ISession block = manager.buildSession()) { + code.put(bytes("contract"), bytes("runtime")); + block.commit(meta); + } + + SnapshotImpl layer = (SnapshotImpl) code.getHead(); + assertEquals(meta, layer.getBlockSnapshotMeta()); + assertSame(prepared.get(), layer.getPreparedPathStateDelta()); + assertEquals(1, published.get().getBlockNumber()); + assertFalse(attachment.isFailed()); + manager.detachPathStateRuntime(attachment); + manager.shutdown(); + } + @Test public void disabledAndShadowCapturePreserveCanonicalBlockAndStateOutcome() throws Exception { byte[] codeKey = bytes("equivalence-code"); @@ -284,6 +385,8 @@ public void pathStateP66ActivationScansPostStateThenResumesIncrementalCapture() PathStateMutation firstAsset = mutation(activation, "account-asset", Bytes.concat(firstAddress, bytes("1000021"))); assertArrayEquals(Longs.toByteArray(210L), firstAsset.getCanonicalValue()); + assertTrue(firstAccount.isPreviousValueKnown()); + assertFalse(firstAsset.isPreviousValueKnown()); byte[] codeKey = bytes("after-activation"); try (ISession block = manager.buildSession()) { @@ -296,6 +399,8 @@ public void pathStateP66ActivationScansPostStateThenResumesIncrementalCapture() assertEquals(P66Phase.P66_ON, published.get(1).getPhase()); assertEquals(1, published.get(1).getMutations().size()); assertEquals("code", published.get(1).getMutations().get(0).getDbName()); + assertTrue(published.get(1).getMutations().get(0).isPreviousValueKnown()); + assertNull(published.get(1).getMutations().get(0).getPreviousPhysicalValue()); manager.detachPathStateRuntime(attachment); manager.shutdown(); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java new file mode 100644 index 00000000000..6af3a629dd3 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java @@ -0,0 +1,261 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Status; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateFlushTarget; + +public class StateArchiveCheckpointMaterializerTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void preservesEveryBlockBoundaryBeforePublishingReadableAcrossReopen() + throws Exception { + Path root = temporaryFolder.newFolder("normal").toPath(); + byte[] format = hash(90); + CommonCheckpointPayload payload = payload(format, 1, 3, hash(0), hash(10), hash(13)); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + StateArchiveCheckpointMaterializer materializer = + new StateArchiveCheckpointMaterializer(root, format); + + assertEquals(Status.NEEDS_MATERIALIZATION, materializer.inspect(target)); + materializer.materialize(payload, target); + assertEquals(Status.MATERIALIZED, materializer.inspect(target)); + assertFalse(Files.exists(root.resolve(StateArchiveCheckpointMaterializer.READABLE_FILE))); + assertEquals(3, blockFileCount(root)); + assertThrows(IOException.class, + () -> StateArchiveCheckpointReadAdapter.open(root, target)); + for (int index = 0; index < payload.getBlocks().size(); index++) { + BlockReverseDiff actual = materializer.loadBlock(target, index); + assertEquals(payload.getBlocks().get(index).getMeta(), actual.getMeta()); + assertArrayEquals(payload.getBlocks().get(index).getMutationViewDigest(), + actual.getMutationViewDigest()); + assertEquals("code", actual.getGroups().get(0).getDbName()); + } + + materializer.materialize(payload, target); + assertEquals(3, blockFileCount(root)); + materializer.publish(target); + assertEquals(Status.PUBLISHED, materializer.inspect(target)); + try (StateArchiveCheckpointReadAdapter reader = + StateArchiveCheckpointReadAdapter.open(root, target)) { + assertEquals(0, reader.getIndexedFrom()); + assertEquals(3, reader.getIndexedThrough()); + assertArrayEquals(new byte[]{0}, reader.findOldValueAfter("code", new byte[]{1}, 0) + .get().getValue()); + assertFalse(reader.findOldValueAfter("code", new byte[]{2}, 2).isPresent()); + } + + StateArchiveCheckpointMaterializer reopened = + new StateArchiveCheckpointMaterializer(root, format); + assertEquals(Status.PUBLISHED, reopened.inspect(target)); + reopened.publish(target); + + CommonCheckpointPayload child = payload(format, 4, 2, hash(3), hash(13), hash(15)); + CommonCheckpointTarget childTarget = CommonCheckpointTarget.from(child); + reopened.materialize(child, childTarget); + reopened.publish(childTarget); + assertEquals(Status.PUBLISHED, reopened.inspect(childTarget)); + try (StateArchiveCheckpointReadAdapter reader = + StateArchiveCheckpointReadAdapter.open(root, childTarget)) { + assertEquals(0, reader.getIndexedFrom()); + assertEquals(5, reader.getIndexedThrough()); + assertArrayEquals(hash(5), reader.getHeadHash()); + assertArrayEquals(new byte[]{1}, reader.findOldValueAfter("code", new byte[]{2}, 0) + .get().getValue()); + assertArrayEquals(new byte[]{3}, reader.findOldValueAfter("code", new byte[]{4}, 3) + .get().getValue()); + } + CommonCheckpointTarget restored = + StateArchiveCheckpointMaterializer.loadPublishedTarget(root, format); + assertEquals(childTarget, restored); + try (StateArchiveCheckpointReadAdapter reader = + StateArchiveCheckpointReadAdapter.open(root, format)) { + assertEquals(5, reader.getIndexedThrough()); + assertArrayEquals(new byte[]{1}, reader.findOldValueAfter("code", new byte[]{2}, 0) + .get().getValue()); + } + } + + @Test + public void resumesEveryDurabilityBoundaryUsingOnlyCheckpointRedo() throws Exception { + for (StateArchiveCheckpointMaterializer.Stage stage + : StateArchiveCheckpointMaterializer.Stage.values()) { + Path root = temporaryFolder.newFolder("fault-" + stage).toPath(); + byte[] format = hash(91); + CommonCheckpointPayload payload = payload(format, 8, 3, hash(7), hash(20), hash(23)); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + StateArchiveCheckpointMaterializer failed = new StateArchiveCheckpointMaterializer(root, + format, failAt(stage)); + if (stage == StateArchiveCheckpointMaterializer.Stage.AFTER_READABLE) { + failed.materialize(payload, target); + assertThrows(IOException.class, () -> failed.publish(target)); + } else { + assertThrows(IOException.class, () -> failed.materialize(payload, target)); + } + + StateArchiveCheckpointMaterializer recovered = + new StateArchiveCheckpointMaterializer(root, format); + if (recovered.inspect(target) == Status.NEEDS_MATERIALIZATION) { + recovered.materialize(payload, target); + } + recovered.publish(target); + assertEquals(Status.PUBLISHED, recovered.inspect(target)); + assertEquals(3, blockFileCount(root)); + } + } + + @Test + public void rejectsForeignFormatCorruptImmutableBlockAndNonParentReadable() + throws Exception { + Path root = temporaryFolder.newFolder("reject").toPath(); + byte[] format = hash(92); + CommonCheckpointPayload payload = payload(format, 1, 2, hash(0), hash(30), hash(32)); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + StateArchiveCheckpointMaterializer materializer = + new StateArchiveCheckpointMaterializer(root, format); + + CommonCheckpointPayload foreign = payload(hash(99), 1, 1, hash(0), hash(30), hash(31)); + assertThrows(IOException.class, () -> materializer.materialize(foreign, + CommonCheckpointTarget.from(foreign))); + + StateArchiveCheckpointMaterializer interrupted = new StateArchiveCheckpointMaterializer(root, + format, failAt(StateArchiveCheckpointMaterializer.Stage.AFTER_BLOCK_FILE)); + assertThrows(IOException.class, () -> interrupted.materialize(payload, target)); + Path block = firstBlockFile(root); + byte[] corrupt = Files.readAllBytes(block); + corrupt[corrupt.length - 1] ^= 1; + Files.write(block, corrupt); + assertThrows(IOException.class, () -> materializer.materialize(payload, target)); + + Path cleanRoot = temporaryFolder.newFolder("non-parent").toPath(); + StateArchiveCheckpointMaterializer clean = + new StateArchiveCheckpointMaterializer(cleanRoot, format); + clean.materialize(payload, target); + clean.publish(target); + CommonCheckpointPayload nonChild = payload(format, 5, 1, hash(9), hash(40), hash(41)); + assertThrows(IOException.class, + () -> clean.inspect(CommonCheckpointTarget.from(nonChild))); + assertTrue(Files.isRegularFile(cleanRoot.resolve( + StateArchiveCheckpointMaterializer.READABLE_FILE))); + try (Options options = new Options().setCreateIfMissing(false); + RocksDB database = RocksDB.open(options, cleanRoot.resolve( + StateArchiveCheckpointServingIndex.DIRECTORY).resolve("keys").toString())) { + database.put(new byte[]{0}, new byte[]{1}); + } + assertThrows(IOException.class, () -> clean.inspect(target)); + } + + private static StateArchiveCheckpointMaterializer.FaultHook failAt( + StateArchiveCheckpointMaterializer.Stage failedStage) { + return (stage, blockIndex) -> { + if (stage == failedStage) { + throw new IOException("injected " + stage + " at " + blockIndex); + } + }; + } + + private static CommonCheckpointPayload payload(byte[] format, long firstBlock, int count, + byte[] parentHash, byte[] parentRoot, byte[] stateRoot) { + List bindings = new ArrayList<>(); + List archives = new ArrayList<>(); + byte[] priorHash = parentHash; + byte[] priorRoot = parentRoot; + for (int index = 0; index < count; index++) { + long number = firstBlock + index; + byte[] blockHash = hash((int) number); + byte[] nextRoot = index == count - 1 ? stateRoot : hash(30 + (int) number); + byte[] view = hash(60 + (int) number); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(number, blockHash, priorHash, + number * 3_000L); + PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); + when(binding.getMeta()).thenReturn(meta); + when(binding.getParentStateRoot()).thenReturn(priorRoot); + when(binding.getStateRoot()).thenReturn(nextRoot); + when(binding.getTransitionPayloadDigest()).thenReturn(hash(70 + (int) number)); + when(binding.getMutationViewDigest()).thenReturn(view); + bindings.add(binding); + archives.add(new BlockReverseDiff(meta, Collections.singletonList(new DbGroup( + "code", Collections.singletonList(new Entry(new byte[]{(byte) number}, + OldValue.present(new byte[]{(byte) (number - 1)}))))), view)); + priorHash = blockHash; + priorRoot = nextRoot; + } + PathStateFlushTarget pathState = mock(PathStateFlushTarget.class); + when(pathState.getBlocks()).thenReturn(bindings); + when(pathState.getParentStateRoot()).thenReturn(parentRoot); + when(pathState.getStateRoot()).thenReturn(stateRoot); + when(pathState.getStores()).thenReturn(Collections.emptyList()); + when(pathState.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + return CommonCheckpointPayload.create(format, pathState, archives, + Collections.emptyList()); + } + + private static int blockFileCount(Path root) throws IOException { + Path targets = root.resolve(StateArchiveCheckpointMaterializer.TARGET_DIRECTORY); + try (DirectoryStream targetPaths = Files.newDirectoryStream(targets)) { + for (Path target : targetPaths) { + Path blocks = target.resolve(StateArchiveCheckpointMaterializer.BLOCK_DIRECTORY); + if (Files.isDirectory(blocks)) { + int count = 0; + try (DirectoryStream paths = Files.newDirectoryStream(blocks, "*.diff")) { + for (Path ignored : paths) { + count++; + } + } + return count; + } + } + } + return 0; + } + + private static Path firstBlockFile(Path root) throws IOException { + Path targets = root.resolve(StateArchiveCheckpointMaterializer.TARGET_DIRECTORY); + try (DirectoryStream targetPaths = Files.newDirectoryStream(targets)) { + for (Path target : targetPaths) { + Path blocks = target.resolve(StateArchiveCheckpointMaterializer.BLOCK_DIRECTORY); + if (Files.isDirectory(blocks)) { + try (DirectoryStream paths = Files.newDirectoryStream(blocks, "*.diff")) { + for (Path path : paths) { + return path; + } + } + } + } + } + throw new IOException("checkpoint block not found"); + } + + private static byte[] hash(int seed) { + byte[] hash = new byte[32]; + for (int index = 0; index < hash.length; index++) { + hash[index] = (byte) (seed + index); + } + return hash; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshotTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshotTest.java new file mode 100644 index 00000000000..7e661c638cf --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshotTest.java @@ -0,0 +1,189 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.core.CommonCheckpointFile; +import org.tron.core.db2.core.CommonCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointRedoCoordinator; +import org.tron.core.db2.core.CommonCheckpointRuntimeOwner; +import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateFlushTarget; + +public class StateArchiveCheckpointReadSnapshotTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void combinesHistoryAndPinnedLatestUnderOneRuntimeLease() throws Exception { + Path root = temporaryFolder.newFolder("snapshot").toPath(); + byte[] format = hash(90); + CommonCheckpointPayload payload = payload(format, 1, 3); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + StateArchiveCheckpointMaterializer materializer = + new StateArchiveCheckpointMaterializer(root, format); + materializer.materialize(payload, target); + materializer.publish(target); + CommonCheckpointRuntimeOwner owner = readyOwner(root.resolve("runtime")); + FakeLatest latest = new FakeLatest(3, hash(3), OldValue.present(new byte[]{99})); + + try (StateArchiveCheckpointReadSnapshot snapshot = + StateArchiveCheckpointReadSnapshot.pin(2, owner, root, format, + (blockNumber, blockHash) -> { + assertEquals(3, blockNumber); + assertArrayEquals(hash(3), blockHash); + return latest; + })) { + assertEquals(2, snapshot.getTargetBlock()); + assertEquals(3, snapshot.getPinnedBlock()); + assertArrayEquals(hash(3), snapshot.getPinnedHash()); + assertArrayEquals(new byte[]{2}, snapshot.get("code", new byte[]{3}).getValue()); + assertArrayEquals(new byte[]{99}, snapshot.get("code", new byte[]{1}).getValue()); + snapshot.requirePinnedIdentity(); + assertFalse(latest.closed); + } + assertTrue(latest.closed); + owner.close(); + assertEquals(CommonCheckpointRuntimeOwner.State.CLOSED, owner.getState()); + } + + @Test + public void rejectsLatestHeadMismatchAndClosesFailedPin() throws Exception { + Path root = temporaryFolder.newFolder("mismatch").toPath(); + byte[] format = hash(91); + CommonCheckpointPayload payload = payload(format, 1, 2); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + StateArchiveCheckpointMaterializer materializer = + new StateArchiveCheckpointMaterializer(root, format); + materializer.materialize(payload, target); + materializer.publish(target); + CommonCheckpointRuntimeOwner owner = readyOwner(root.resolve("runtime")); + FakeLatest latest = new FakeLatest(1, hash(1), OldValue.absent()); + + assertThrows(IllegalArgumentException.class, + () -> StateArchiveCheckpointReadSnapshot.pin(1, owner, root, format, + (blockNumber, blockHash) -> latest)); + assertTrue(latest.closed); + owner.close(); + assertEquals(CommonCheckpointRuntimeOwner.State.CLOSED, owner.getState()); + } + + private static CommonCheckpointRuntimeOwner readyOwner(Path directory) throws IOException { + CommonCheckpointMaterializer chainbase = materializer(Authority.CHAINBASE); + CommonCheckpointMaterializer pathState = materializer(Authority.PATH_STATE); + CommonCheckpointMaterializer archive = materializer(Authority.STATE_ARCHIVE); + CommonCheckpointRuntimeOwner owner = new CommonCheckpointRuntimeOwner( + new CommonCheckpointRedoCoordinator(new CommonCheckpointFile(directory), chainbase, + pathState, archive)); + owner.recoverBeforeServing(); + return owner; + } + + private static CommonCheckpointMaterializer materializer(Authority authority) { + CommonCheckpointMaterializer materializer = mock(CommonCheckpointMaterializer.class); + when(materializer.authority()).thenReturn(authority); + return materializer; + } + + private static CommonCheckpointPayload payload(byte[] format, long firstBlock, int count) { + List bindings = new ArrayList<>(); + List archives = new ArrayList<>(); + byte[] priorHash = hash((int) firstBlock - 1); + byte[] priorRoot = hash(30); + for (int index = 0; index < count; index++) { + long number = firstBlock + index; + byte[] blockHash = hash((int) number); + byte[] nextRoot = hash(31 + index); + byte[] view = hash(60 + index); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(number, blockHash, priorHash, + number * 3_000L); + PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); + when(binding.getMeta()).thenReturn(meta); + when(binding.getParentStateRoot()).thenReturn(priorRoot); + when(binding.getStateRoot()).thenReturn(nextRoot); + when(binding.getTransitionPayloadDigest()).thenReturn(hash(70 + index)); + when(binding.getMutationViewDigest()).thenReturn(view); + bindings.add(binding); + archives.add(new BlockReverseDiff(meta, Collections.singletonList(new DbGroup( + "code", Collections.singletonList(new Entry(new byte[]{(byte) number}, + OldValue.present(new byte[]{(byte) (number - 1)}))))), view)); + priorHash = blockHash; + priorRoot = nextRoot; + } + PathStateFlushTarget pathState = mock(PathStateFlushTarget.class); + when(pathState.getBlocks()).thenReturn(bindings); + when(pathState.getParentStateRoot()).thenReturn(hash(30)); + when(pathState.getStateRoot()).thenReturn(priorRoot); + when(pathState.getStores()).thenReturn(Collections.emptyList()); + when(pathState.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + return CommonCheckpointPayload.create(format, pathState, archives, + Collections.emptyList()); + } + + private static byte[] hash(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } + + private static final class FakeLatest implements PinnedLatestState { + + private final long blockNumber; + private final byte[] blockHash; + private final OldValue value; + private boolean closed; + + private FakeLatest(long blockNumber, byte[] blockHash, OldValue value) { + this.blockNumber = blockNumber; + this.blockHash = blockHash; + this.value = value; + } + + @Override + public long getBlockNumber() { + return blockNumber; + } + + @Override + public byte[] getBlockHash() { + return blockHash; + } + + @Override + public OldValue get(String dbName, byte[] physicalRawKey) { + return value; + } + + @Override + public List range(String dbName, byte[] lowerInclusive, + byte[] upperExclusive, int maxEntries) { + throw new UnsupportedOperationException("point-only checkpoint snapshot"); + } + + @Override + public void close() { + closed = true; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index 52637319ee7..b3f22f5b929 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -177,9 +177,14 @@ public void servingIndexPublicationFailuresRetryWithoutResubmittingHistoryAndRes assertThrows(org.tron.core.exception.TronError.class, snapshots::flushPending); assertEquals(target, manager.getArchiveHistoryWriter().committedHeadMeta()); assertEquals(6, snapshots.getArchiveReadableEpoch()); - assertThrows(ArchivePersistenceException.class, - () -> manager.getArchiveAccountBalance(6, - new byte[HistoricalAccountBalanceReader.ADDRESS_LENGTH])); + if (failureStage == ServingIndexStage.CURRENT_PUBLISHED) { + assertThrows(ArchivePersistenceException.class, + () -> manager.getArchiveAccountBalance(6, + new byte[HistoricalAccountBalanceReader.ADDRESS_LENGTH])); + } else { + assertFalse(manager.getArchiveAccountBalance(6, + new byte[HistoricalAccountBalanceReader.ADDRESS_LENGTH]).isPresent()); + } assertThrows(ArchivePersistenceException.class, manager::inspectArchiveServingIndex); assertTrue(fixture.databases.values().stream().allMatch(database -> failureStage == ServingIndexStage.BEFORE_BUILD @@ -569,10 +574,17 @@ private void runPostRefreshP66Failure(Path output, String engine, assertArrayEquals(longValue(40), accountAssetStore.get(directKey)); assertTrue(fixture.databases.values().stream() .allMatch(database -> database.getHead() instanceof SnapshotRoot)); - assertThrows(ArchivePersistenceException.class, - () -> manager.getArchiveAccountAssetBalance(7, address, tokenId)); - assertThrows(ArchivePersistenceException.class, - () -> manager.getArchiveAccountAssetBalance(8, address, tokenId)); + if (failureStage == ReadableStateStage.CANONICAL_REFRESHED) { + assertAccountAsset(manager, 7, address, tokenId, + P66AccountAssetCodec.Phase.P66_ON, true, 30); + assertThrows(IllegalArgumentException.class, + () -> manager.getArchiveAccountAssetBalance(8, address, tokenId)); + } else { + assertThrows(ArchivePersistenceException.class, + () -> manager.getArchiveAccountAssetBalance(7, address, tokenId)); + assertThrows(ArchivePersistenceException.class, + () -> manager.getArchiveAccountAssetBalance(8, address, tokenId)); + } Map historyAuthority = historyAuthoritySnapshot(archive); @SuppressWarnings("unchecked") diff --git a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java new file mode 100644 index 00000000000..a3d43757c57 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java @@ -0,0 +1,662 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.TestConstants; +import org.tron.core.config.args.Args; +import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.HistoricalRangeOverlay; +import org.tron.core.db2.archive.OldValue; +import org.tron.core.db2.archive.StateArchiveCheckpointMaterializer; +import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; +import org.tron.core.db2.common.DB; +import org.tron.core.db2.common.Flusher; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Status; +import org.tron.core.db2.stateroot.PathStateCanonicalizer; +import org.tron.core.db2.stateroot.PathStateCheckpointMaterializer; +import org.tron.core.db2.stateroot.PathStateFlushTarget; +import org.tron.core.db2.stateroot.PathStateParticipantScope; +import org.tron.core.db2.stateroot.PathStatePhysicalStoreSet; +import org.tron.core.db2.stateroot.PathStateSnapshotDelta; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class ChainbaseCheckpointMaterializerTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeClass + public static void configure() { + Args.setParam(new String[]{}, TestConstants.TEST_CONF); + } + + @AfterClass + public static void clearConfiguration() { + Args.clearParam(); + } + + @Test + public void appliesEachStoreWithSyncBeforePublishingCurrentAndReopens() throws Exception { + Fixture fixture = fixture("normal", null); + fixture.code.put(new byte[]{9}, new byte[]{9}); + + assertEquals(Status.NEEDS_MATERIALIZATION, + fixture.materializer.inspect(fixture.target)); + fixture.materializer.materialize(fixture.payload, fixture.target); + assertEquals(Status.MATERIALIZED, fixture.materializer.inspect(fixture.target)); + assertFalse(java.nio.file.Files.exists(fixture.root.resolve( + ChainbaseCheckpointMaterializer.CURRENT_FILE))); + assertArrayEquals(new byte[]{2}, fixture.code.get(new byte[]{1})); + assertNull(fixture.code.get(new byte[]{9})); + assertArrayEquals(new byte[]{4}, fixture.storage.get(new byte[]{3})); + assertEquals(1, fixture.code.syncedFlushes); + assertEquals(1, fixture.storage.syncedFlushes); + + fixture.materializer.materialize(fixture.payload, fixture.target); + assertEquals(1, fixture.code.syncedFlushes); + fixture.materializer.publish(fixture.target); + assertEquals(Status.PUBLISHED, fixture.materializer.inspect(fixture.target)); + + ChainbaseCheckpointMaterializer reopened = new ChainbaseCheckpointMaterializer(fixture.root, + fixture.format, fixture.databases); + assertEquals(Status.PUBLISHED, reopened.inspect(fixture.target)); + CommonCheckpointPayload child = payload(fixture.format, 2, hash(1), hash(2), hash(11), + hash(12)); + CommonCheckpointTarget childTarget = CommonCheckpointTarget.from(child); + reopened.materialize(child, childTarget); + reopened.publish(childTarget); + assertEquals(Status.PUBLISHED, reopened.inspect(childTarget)); + } + + @Test + public void resumesEveryStoreAndMarkerBoundaryUsingCheckpointRedo() throws Exception { + for (ChainbaseCheckpointMaterializer.Stage stage + : ChainbaseCheckpointMaterializer.Stage.values()) { + Fixture fixture = fixture("fault-" + stage, stage); + if (stage == ChainbaseCheckpointMaterializer.Stage.AFTER_CURRENT) { + fixture.materializer.materialize(fixture.payload, fixture.target); + assertThrows(IOException.class, () -> fixture.materializer.publish(fixture.target)); + } else { + assertThrows(IOException.class, + () -> fixture.materializer.materialize(fixture.payload, fixture.target)); + } + + ChainbaseCheckpointMaterializer recovered = new ChainbaseCheckpointMaterializer( + fixture.root, fixture.format, fixture.databases); + if (recovered.inspect(fixture.target) == Status.NEEDS_MATERIALIZATION) { + recovered.materialize(fixture.payload, fixture.target); + } + recovered.publish(fixture.target); + assertEquals(Status.PUBLISHED, recovered.inspect(fixture.target)); + assertArrayEquals(new byte[]{2}, fixture.code.get(new byte[]{1})); + assertArrayEquals(new byte[]{4}, fixture.storage.get(new byte[]{3})); + } + } + + @Test + public void rejectsUnknownStoreForeignFormatAndNonParentTarget() throws Exception { + Fixture fixture = fixture("reject", null); + CommonCheckpointPayload unknown = payload(fixture.format, 1, hash(0), hash(1), hash(10), + hash(11), "unknown"); + assertThrows(IOException.class, () -> fixture.materializer.materialize(unknown, + CommonCheckpointTarget.from(unknown))); + + CommonCheckpointPayload foreign = payload(hash(99), 1, hash(0), hash(1), hash(10), + hash(11)); + assertThrows(IOException.class, () -> fixture.materializer.materialize(foreign, + CommonCheckpointTarget.from(foreign))); + + fixture.materializer.materialize(fixture.payload, fixture.target); + fixture.materializer.publish(fixture.target); + CommonCheckpointPayload nonChild = payload(fixture.format, 4, hash(8), hash(9), hash(20), + hash(21)); + assertThrows(IOException.class, + () -> fixture.materializer.inspect(CommonCheckpointTarget.from(nonChild))); + } + + @Test + public void payloadFactoryCoalescesSnapshotMutationsWithoutDurableReads() { + MemoryDb code = new MemoryDb("code"); + MemoryDb storage = new MemoryDb("storage-row"); + Chainbase codeChainbase = new Chainbase(new SnapshotRoot(code)); + Chainbase storageChainbase = new Chainbase(new SnapshotRoot(storage)); + List databases = Arrays.asList(codeChainbase, storageChainbase); + + for (int number = 1; number <= 2; number++) { + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(number, hash(number), hash(number - 1), + number * 3_000L); + byte[] parentRoot = hash(10 + number - 1); + byte[] stateRoot = hash(10 + number); + byte[] view = hash(40 + number); + PathStateSnapshotDelta path = mock(PathStateSnapshotDelta.class); + when(path.getMeta()).thenReturn(meta); + when(path.getParentStateRoot()).thenReturn(parentRoot); + when(path.getStateRoot()).thenReturn(stateRoot); + when(path.getTransitionPayloadDigest()).thenReturn(hash(50 + number)); + when(path.getMutationViewDigest()).thenReturn(view); + when(path.getStores()).thenReturn(Collections.emptyList()); + when(path.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList(), view); + + SnapshotImpl codeLayer = append(codeChainbase, meta, archive, path); + SnapshotImpl storageLayer = append(storageChainbase, meta, archive, path); + codeLayer.put(new byte[]{1}, new byte[]{(byte) number}); + codeLayer.put(new byte[]{(byte) (10 + number)}, new byte[]{(byte) (20 + number)}); + if (number == 1) { + storageLayer.put(new byte[]{3}, new byte[]{3}); + } else { + storageLayer.remove(new byte[]{3}); + } + } + + CommonCheckpointPayload captured = new CommonCheckpointPayloadFactory().capture(hash(80), + databases, 2); + assertEquals(2, captured.getBlocks().size()); + assertEquals(2, captured.getChainbaseStores().size()); + CommonCheckpointPayload.StoreMutations codeStore = captured.getChainbaseStores().get(0); + assertEquals("code", codeStore.getDbName()); + assertEquals(3, codeStore.getMutations().size()); + CommonCheckpointPayload.Mutation overwritten = codeStore.getMutations().stream() + .filter(mutation -> Arrays.equals(new byte[]{1}, mutation.getKey())) + .findFirst().orElseThrow(AssertionError::new); + assertArrayEquals(new byte[]{2}, overwritten.getValue()); + CommonCheckpointPayload.StoreMutations storageStore = + captured.getChainbaseStores().get(1); + assertEquals("storage-row", storageStore.getDbName()); + assertEquals(1, storageStore.getMutations().size()); + assertEquals(true, storageStore.getMutations().get(0).isDelete()); + assertEquals(0, code.getCalls); + assertEquals(0, storage.getCalls); + } + + @Test + public void realThreeAuthorityCoordinatorCrossesBothBarriersThenRetiresWal() + throws Exception { + java.nio.file.Path root = temporaryFolder.newFolder("three-authority").toPath(); + byte[] format = hash(88); + MemoryDb code = new MemoryDb("code"); + Chainbase codeChainbase = new Chainbase(new SnapshotRoot(code)); + List databases = Collections.singletonList(codeChainbase); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + CommonCheckpointPayload payload = integratedPayload(format, scope); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + + try (PathStatePhysicalStoreSet pathStores = PathStatePhysicalStoreSet.open( + root.resolve("path-state"), scope, Engine.ROCKSDB)) { + ChainbaseCheckpointMaterializer chainbase = new ChainbaseCheckpointMaterializer( + root.resolve("chainbase"), format, databases); + PathStateCheckpointMaterializer pathState = new PathStateCheckpointMaterializer(pathStores, + scope, format); + StateArchiveCheckpointMaterializer archive = new StateArchiveCheckpointMaterializer( + root.resolve("archive"), format); + CommonCheckpointFile file = new CommonCheckpointFile(root.resolve("wal")); + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator(file, + chainbase, pathState, archive); + + assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.COMPLETED_REDO, + coordinator.apply(payload)); + assertEquals(Status.PUBLISHED, chainbase.inspect(target)); + assertEquals(Status.PUBLISHED, pathState.inspect(target)); + assertEquals(Status.PUBLISHED, archive.inspect(target)); + assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.NO_CHECKPOINT, + coordinator.recover()); + assertArrayEquals(new byte[]{2}, code.get(new byte[]{1})); + assertArrayEquals(new byte[]{4}, pathStores.participant("account").getFlat( + new byte[]{3})); + assertFalse(java.nio.file.Files.exists(root.resolve("wal").resolve( + CommonCheckpointFile.FILE_NAME))); + } + } + + @Test + public void snapshotRebaserDropsOnlyMaterializedPrefixWithoutSecondStoreWrite() + throws Exception { + java.nio.file.Path root = temporaryFolder.newFolder("snapshot-rebase").toPath(); + MemoryDb code = new MemoryDb("code"); + MemoryDb storage = new MemoryDb("storage-row"); + Chainbase codeChainbase = new Chainbase(new SnapshotRoot(code)); + Chainbase storageChainbase = new Chainbase(new SnapshotRoot(storage)); + List databases = Arrays.asList(codeChainbase, storageChainbase); + for (int number = 1; number <= 3; number++) { + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(number, hash(number), hash(number - 1), + number * 3_000L); + byte[] view = hash(40 + number); + PathStateSnapshotDelta path = pathDelta(meta, hash(10 + number - 1), hash(10 + number), + view); + BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList(), view); + append(codeChainbase, meta, archive, path).put(new byte[]{1}, + new byte[]{(byte) number}); + append(storageChainbase, meta, archive, path).put(new byte[]{3}, + new byte[]{(byte) number}); + } + byte[] format = hash(80); + CommonCheckpointPayload payload = new CommonCheckpointPayloadFactory().capture(format, + databases, 2); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + ChainbaseCheckpointMaterializer materializer = new ChainbaseCheckpointMaterializer(root, + format, databases); + materializer.materialize(payload, target); + materializer.publish(target); + assertEquals(1, code.syncedFlushes); + assertEquals(1, storage.syncedFlushes); + + new CommonCheckpointSnapshotRebaser().rebase(databases, target, 2); + assertEquals(1, code.syncedFlushes); + assertEquals(1, storage.syncedFlushes); + assertArrayEquals(new byte[]{2}, code.get(new byte[]{1})); + assertArrayEquals(new byte[]{2}, storage.get(new byte[]{3})); + assertArrayEquals(new byte[]{3}, codeChainbase.getUnchecked(new byte[]{1})); + assertArrayEquals(new byte[]{3}, storageChainbase.getUnchecked(new byte[]{3})); + assertSame(codeChainbase.getHead().getRoot(), codeChainbase.getHead().getPrevious()); + assertSame(storageChainbase.getHead().getRoot(), + storageChainbase.getHead().getPrevious()); + } + + @Test + public void snapshotRebaserPrevalidatesEveryStoreBeforeChangingAnyChain() { + MemoryDb code = new MemoryDb("code"); + MemoryDb storage = new MemoryDb("storage-row"); + Chainbase codeChainbase = new Chainbase(new SnapshotRoot(code)); + Chainbase storageChainbase = new Chainbase(new SnapshotRoot(storage)); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload(hash(80), 1, hash(0), + hash(1), hash(10), hash(11))); + SnapshotImpl codeLayer = (SnapshotImpl) codeChainbase.getHead().advance(); + codeLayer.attachBlockArtifacts(target.getFirstBlock(), null, null); + codeChainbase.setHead(codeLayer); + SnapshotImpl storageLayer = (SnapshotImpl) storageChainbase.getHead().advance(); + storageLayer.attachBlockArtifacts(BlockSnapshotMeta.forBlock(1, hash(9), hash(0), 3_000L), + null, null); + storageChainbase.setHead(storageLayer); + + assertThrows(IOException.class, () -> new CommonCheckpointSnapshotRebaser().rebase( + Arrays.asList(codeChainbase, storageChainbase), target, 1)); + assertSame(codeLayer, codeChainbase.getHead()); + assertSame(storageLayer, storageChainbase.getHead()); + } + + @Test + public void runtimeComposesStartupCheckpointRebaseAndPointQuery() throws Exception { + java.nio.file.Path root = temporaryFolder.newFolder("composed-runtime").toPath(); + byte[] format = hash(93); + MemoryDb code = new MemoryDb("code"); + Chainbase database = new Chainbase(new SnapshotRoot(code)); + List databases = Collections.singletonList(database); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); + byte[] view = hash(41); + PathStateSnapshotDelta path = pathDelta(meta, hash(10), hash(11), view); + BlockReverseDiff archiveBlock = new BlockReverseDiff(meta, + Collections.singletonList(new DbGroup("code", Collections.singletonList( + new Entry(new byte[]{1}, OldValue.present(new byte[]{0}))))), view); + append(database, meta, archiveBlock, path).put(new byte[]{1}, new byte[]{2}); + + ChainbaseCheckpointMaterializer chainbase = new ChainbaseCheckpointMaterializer( + root.resolve("chainbase"), format, databases); + PublishingMaterializer pathState = new PublishingMaterializer(Authority.PATH_STATE); + StateArchiveCheckpointMaterializer archive = new StateArchiveCheckpointMaterializer( + root.resolve("archive"), format); + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( + new CommonCheckpointFile(root.resolve("wal")), chainbase, pathState, archive); + CommonCheckpointRuntime runtime = new CommonCheckpointRuntime( + new CommonCheckpointRuntimeOwner(coordinator), databases, root.resolve("archive"), + format, (blockNumber, blockHash) -> new TestLatest(code, blockNumber, blockHash)); + + assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.NO_CHECKPOINT, + runtime.recoverBeforeServing()); + CommonCheckpointTarget target = runtime.checkpointAndRebase(1); + assertEquals(meta, target.getLastBlock()); + assertSame(database.getHead().getRoot(), database.getHead()); + assertEquals(1, code.syncedFlushes); + try (StateArchiveCheckpointReadSnapshot snapshot = runtime.pinPoint(0)) { + assertArrayEquals(new byte[]{0}, snapshot.get("code", new byte[]{1}).getValue()); + } + try (StateArchiveCheckpointReadSnapshot snapshot = runtime.pinPoint(1)) { + assertArrayEquals(new byte[]{2}, snapshot.get("code", new byte[]{1}).getValue()); + } + runtime.close(); + assertEquals(CommonCheckpointRuntimeOwner.State.CLOSED, runtime.getState()); + } + + private Fixture fixture(String name, ChainbaseCheckpointMaterializer.Stage failedStage) + throws Exception { + java.nio.file.Path root = temporaryFolder.newFolder(name).toPath(); + MemoryDb code = new MemoryDb("code"); + MemoryDb storage = new MemoryDb("storage-row"); + List databases = Arrays.asList( + new Chainbase(new SnapshotRoot(code)), new Chainbase(new SnapshotRoot(storage))); + byte[] format = hash(80); + CommonCheckpointPayload payload = payload(format, 1, hash(0), hash(1), hash(10), hash(11)); + ChainbaseCheckpointMaterializer materializer = new ChainbaseCheckpointMaterializer(root, + format, databases, failAt(failedStage)); + return new Fixture(root, code, storage, databases, format, payload, materializer); + } + + private static ChainbaseCheckpointMaterializer.FaultHook failAt( + ChainbaseCheckpointMaterializer.Stage failedStage) { + return (stage, dbName) -> { + if (stage == failedStage) { + throw new IOException("injected " + stage + " at " + dbName); + } + }; + } + + private static CommonCheckpointPayload payload(byte[] format, long blockNumber, + byte[] parentHash, byte[] blockHash, byte[] parentRoot, byte[] stateRoot, + String... storeOverride) { + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(blockNumber, blockHash, parentHash, + blockNumber * 3_000L); + byte[] view = hash(40 + (int) blockNumber); + PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); + when(binding.getMeta()).thenReturn(meta); + when(binding.getParentStateRoot()).thenReturn(parentRoot); + when(binding.getStateRoot()).thenReturn(stateRoot); + when(binding.getTransitionPayloadDigest()).thenReturn(hash(50 + (int) blockNumber)); + when(binding.getMutationViewDigest()).thenReturn(view); + PathStateFlushTarget pathState = mock(PathStateFlushTarget.class); + when(pathState.getBlocks()).thenReturn(Collections.singletonList(binding)); + when(pathState.getParentStateRoot()).thenReturn(parentRoot); + when(pathState.getStateRoot()).thenReturn(stateRoot); + when(pathState.getStores()).thenReturn(Collections.emptyList()); + when(pathState.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + + List stores = new ArrayList<>(); + String firstName = storeOverride.length == 0 ? "code" : storeOverride[0]; + stores.add(new CommonCheckpointPayload.StoreMutations(firstName, Arrays.asList( + new CommonCheckpointPayload.Mutation(new byte[]{1}, new byte[]{2}), + new CommonCheckpointPayload.Mutation(new byte[]{9}, null)))); + if (storeOverride.length == 0) { + stores.add(new CommonCheckpointPayload.StoreMutations("storage-row", + Collections.singletonList( + new CommonCheckpointPayload.Mutation(new byte[]{3}, new byte[]{4})))); + } + BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList(), view); + return CommonCheckpointPayload.create(format, pathState, + Collections.singletonList(archive), stores); + } + + private static byte[] hash(int seed) { + byte[] hash = new byte[32]; + for (int index = 0; index < hash.length; index++) { + hash[index] = (byte) (seed + index); + } + return hash; + } + + private static CommonCheckpointPayload integratedPayload(byte[] format, + PathStateParticipantScope scope) { + long blockNumber = 1; + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(blockNumber, hash(1), hash(0), 3_000L); + byte[] view = hash(41); + PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); + when(binding.getMeta()).thenReturn(meta); + when(binding.getParentStateRoot()).thenReturn(hash(10)); + when(binding.getStateRoot()).thenReturn(hash(11)); + when(binding.getTransitionPayloadDigest()).thenReturn(hash(51)); + when(binding.getMutationViewDigest()).thenReturn(view); + PathStateFlushTarget.StoreTarget account = mock(PathStateFlushTarget.StoreTarget.class); + when(account.getStoreId()).thenReturn(scope.require("account").getStoreId()); + when(account.getDbName()).thenReturn("account"); + when(account.getStoreRoot()).thenReturn(hash(61)); + PathStateSnapshotDelta.Mutation flat = mockMutation(new byte[]{3}, new byte[]{4}); + PathStateSnapshotDelta.Mutation node = mockMutation(new byte[]{5}, new byte[]{6}); + PathStateSnapshotDelta.Mutation superNode = mockMutation(new byte[]{7}, new byte[]{8}); + when(account.getFlatMutations()).thenReturn(Collections.singletonList(flat)); + when(account.getNodeMutations()).thenReturn(Collections.singletonList(node)); + PathStateFlushTarget pathState = mock(PathStateFlushTarget.class); + when(pathState.getBlocks()).thenReturn(Collections.singletonList(binding)); + when(pathState.getParentStateRoot()).thenReturn(hash(10)); + when(pathState.getStateRoot()).thenReturn(hash(11)); + when(pathState.getStores()).thenReturn(Collections.singletonList(account)); + when(pathState.getSuperNodeMutations()).thenReturn(Collections.singletonList(superNode)); + List chainbase = Collections.singletonList( + new CommonCheckpointPayload.StoreMutations("code", Collections.singletonList( + new CommonCheckpointPayload.Mutation(new byte[]{1}, new byte[]{2})))); + return CommonCheckpointPayload.create(format, pathState, + Collections.singletonList(new BlockReverseDiff(meta, Collections.emptyList(), view)), + chainbase); + } + + private static PathStateSnapshotDelta.Mutation mockMutation(byte[] key, byte[] value) { + PathStateSnapshotDelta.Mutation mutation = mock(PathStateSnapshotDelta.Mutation.class); + when(mutation.getKey()).thenReturn(key); + when(mutation.getValue()).thenReturn(value); + return mutation; + } + + private static SnapshotImpl append(Chainbase database, BlockSnapshotMeta meta, + BlockReverseDiff archive, PathStateSnapshotDelta path) { + SnapshotImpl layer = (SnapshotImpl) database.getHead().advance(); + layer.attachBlockArtifacts(meta, archive, path); + database.setHead(layer); + return layer; + } + + private static PathStateSnapshotDelta pathDelta(BlockSnapshotMeta meta, byte[] parentRoot, + byte[] stateRoot, byte[] view) { + PathStateSnapshotDelta path = mock(PathStateSnapshotDelta.class); + when(path.getMeta()).thenReturn(meta); + when(path.getParentStateRoot()).thenReturn(parentRoot); + when(path.getStateRoot()).thenReturn(stateRoot); + when(path.getTransitionPayloadDigest()).thenReturn(hash(50 + (int) meta.getBlockNumber())); + when(path.getMutationViewDigest()).thenReturn(view); + when(path.getStores()).thenReturn(Collections.emptyList()); + when(path.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + return path; + } + + private static final class Fixture { + + private final java.nio.file.Path root; + private final MemoryDb code; + private final MemoryDb storage; + private final List databases; + private final byte[] format; + private final CommonCheckpointPayload payload; + private final CommonCheckpointTarget target; + private final ChainbaseCheckpointMaterializer materializer; + + private Fixture(java.nio.file.Path root, MemoryDb code, MemoryDb storage, + List databases, byte[] format, CommonCheckpointPayload payload, + ChainbaseCheckpointMaterializer materializer) { + this.root = root; + this.code = code; + this.storage = storage; + this.databases = databases; + this.format = format; + this.payload = payload; + this.target = CommonCheckpointTarget.from(payload); + this.materializer = materializer; + } + } + + private static final class PublishingMaterializer implements CommonCheckpointMaterializer { + + private final Authority authority; + private Status status = Status.NEEDS_MATERIALIZATION; + private CommonCheckpointTarget target; + + private PublishingMaterializer(Authority authority) { + this.authority = authority; + } + + @Override + public Authority authority() { + return authority; + } + + @Override + public Status inspect(CommonCheckpointTarget expected) throws IOException { + if (target != null && !target.equals(expected)) { + throw new IOException("test materializer target mismatch"); + } + return status; + } + + @Override + public void materialize(CommonCheckpointPayload payload, CommonCheckpointTarget expected) { + target = expected; + status = Status.MATERIALIZED; + } + + @Override + public void publish(CommonCheckpointTarget expected) throws IOException { + if (status != Status.MATERIALIZED || !expected.equals(target)) { + throw new IOException("test materializer publish without materialization"); + } + status = Status.PUBLISHED; + } + } + + private static final class TestLatest implements PinnedLatestState { + + private final MemoryDb database; + private final long blockNumber; + private final byte[] blockHash; + + private TestLatest(MemoryDb database, long blockNumber, byte[] blockHash) { + this.database = database; + this.blockNumber = blockNumber; + this.blockHash = Arrays.copyOf(blockHash, blockHash.length); + } + + @Override + public long getBlockNumber() { + return blockNumber; + } + + @Override + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + @Override + public OldValue get(String dbName, byte[] physicalRawKey) { + if (!"code".equals(dbName)) { + throw new IllegalArgumentException("unexpected test Store " + dbName); + } + return OldValue.fromNullable(database.get(physicalRawKey)); + } + + @Override + public List range(String dbName, byte[] lowerInclusive, + byte[] upperExclusive, int maxEntries) { + throw new UnsupportedOperationException("point-only test latest"); + } + + @Override + public void close() { + } + } + + private static final class MemoryDb implements DB, Flusher { + + private final String name; + private final Map values = new LinkedHashMap<>(); + private int syncedFlushes; + private int getCalls; + + private MemoryDb(String name) { + this.name = name; + } + + @Override + public byte[] get(byte[] key) { + getCalls++; + return values.get(WrappedByteArray.of(key)); + } + + @Override + public void put(byte[] key, byte[] value) { + values.put(WrappedByteArray.of(key), value); + } + + @Override + public long size() { + return values.size(); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + @Override + public void remove(byte[] key) { + values.remove(WrappedByteArray.of(key)); + } + + @Override + public Iterator> iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + } + + @Override + public String getDbName() { + return name; + } + + @Override + public void stat() { + } + + @Override + public DB newInstance() { + throw new UnsupportedOperationException(); + } + + @Override + public void flush(Map batch) { + apply(batch); + } + + @Override + public void flushSynced(Map batch) { + syncedFlushes++; + apply(batch); + } + + @Override + public void reset() { + values.clear(); + } + + private void apply(Map batch) { + batch.forEach((key, value) -> { + if (value.getBytes() == null) { + values.remove(key); + } else { + values.put(key, value.getBytes()); + } + }); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointFileTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointFileTest.java new file mode 100644 index 00000000000..03201e732e0 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointFileTest.java @@ -0,0 +1,137 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.stateroot.PathStateFlushTarget; + +public class CommonCheckpointFileTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void publishesLoadsRetriesAndRetiresOneImmutablePayload() throws Exception { + Path directory = temporaryFolder.getRoot().toPath().resolve("common-checkpoint"); + CommonCheckpointFile file = new CommonCheckpointFile(directory); + CommonCheckpointPayload payload = payload(7); + + assertNull(file.loadIfPresent()); + file.publish(payload); + assertTrue(Files.isRegularFile(file.getCheckpointPath())); + assertFalse(Files.exists(file.getTemporaryPath())); + assertArrayEquals(payload.getStateRoot(), file.loadRequired().getStateRoot()); + byte[] firstBytes = Files.readAllBytes(file.getCheckpointPath()); + + file.publish(payload); + assertArrayEquals(firstBytes, Files.readAllBytes(file.getCheckpointPath())); + assertThrows(IOException.class, () -> file.publish(payload(8))); + file.retire(); + assertNull(file.loadIfPresent()); + file.retire(); + } + + @Test + public void retriesEveryPublishCrashBoundaryWithoutAcceptingTemporaryAuthority() + throws Exception { + for (CommonCheckpointFile.Stage stage : new CommonCheckpointFile.Stage[]{ + CommonCheckpointFile.Stage.AFTER_TEMPORARY_FORCE, + CommonCheckpointFile.Stage.AFTER_ATOMIC_PUBLISH, + CommonCheckpointFile.Stage.AFTER_DIRECTORY_FORCE}) { + Path directory = temporaryFolder.getRoot().toPath().resolve("publish-" + stage); + CommonCheckpointPayload payload = payload(7); + CommonCheckpointFile interrupted = new CommonCheckpointFile(directory, + CommonCheckpointPayloadCodec.DEFAULT_MAX_ENCODED_LENGTH, failAt(stage)); + assertThrows(IOException.class, () -> interrupted.publish(payload)); + + CommonCheckpointFile recovered = new CommonCheckpointFile(directory); + if (stage == CommonCheckpointFile.Stage.AFTER_TEMPORARY_FORCE) { + assertNull(recovered.loadIfPresent()); + assertTrue(Files.isRegularFile(recovered.getTemporaryPath())); + } else { + assertNotNull(recovered.loadRequired()); + } + recovered.publish(payload); + assertNotNull(recovered.loadRequired()); + assertFalse(Files.exists(recovered.getTemporaryPath())); + } + } + + @Test + public void rejectsCorruptOrOversizedFilesAndRecoversInterruptedRetire() throws Exception { + Path directory = temporaryFolder.getRoot().toPath().resolve("retire"); + CommonCheckpointPayload payload = payload(7); + CommonCheckpointFile file = new CommonCheckpointFile(directory); + file.publish(payload); + byte[] corrupt = Files.readAllBytes(file.getCheckpointPath()); + corrupt[corrupt.length - 1] ^= 1; + Files.write(file.getCheckpointPath(), corrupt); + assertThrows(IOException.class, file::loadRequired); + + file.retire(); + file.publish(payload); + CommonCheckpointFile interrupted = new CommonCheckpointFile(directory, + CommonCheckpointPayloadCodec.DEFAULT_MAX_ENCODED_LENGTH, + failAt(CommonCheckpointFile.Stage.AFTER_RETIRE_DELETE)); + assertThrows(IOException.class, interrupted::retire); + assertNull(new CommonCheckpointFile(directory).loadIfPresent()); + new CommonCheckpointFile(directory).retire(); + + CommonCheckpointFile bounded = new CommonCheckpointFile(directory, + CommonCheckpointPayloadCodec.HEADER_LENGTH + 1, (stage, path) -> { }); + assertThrows(IllegalArgumentException.class, () -> bounded.publish(payload)); + } + + private static CommonCheckpointFile.FaultHook failAt(CommonCheckpointFile.Stage expected) { + return (actual, path) -> { + if (actual == expected) { + throw new IOException("injected " + expected); + } + }; + } + + private static CommonCheckpointPayload payload(int seed) { + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); + byte[] viewDigest = hash(4); + PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); + when(binding.getMeta()).thenReturn(meta); + when(binding.getParentStateRoot()).thenReturn(hash(5)); + when(binding.getStateRoot()).thenReturn(hash(6)); + when(binding.getTransitionPayloadDigest()).thenReturn(hash(3)); + when(binding.getMutationViewDigest()).thenReturn(viewDigest); + PathStateFlushTarget target = mock(PathStateFlushTarget.class); + when(target.getBlocks()).thenReturn(Collections.singletonList(binding)); + when(target.getParentStateRoot()).thenReturn(hash(5)); + when(target.getStateRoot()).thenReturn(hash(6)); + when(target.getStores()).thenReturn(Collections.emptyList()); + when(target.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList(), viewDigest); + return CommonCheckpointPayload.create(hash(seed), target, + Collections.singletonList(archive), Collections.singletonList( + new CommonCheckpointPayload.StoreMutations("code", Collections.singletonList( + new CommonCheckpointPayload.Mutation(new byte[]{1}, new byte[]{2}))))); + } + + private static byte[] hash(int seed) { + byte[] hash = new byte[32]; + for (int index = 0; index < hash.length; index++) { + hash[index] = (byte) (seed + index); + } + return hash; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java new file mode 100644 index 00000000000..73f845184e2 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java @@ -0,0 +1,279 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Status; +import org.tron.core.db2.core.CommonCheckpointRedoCoordinator.RecoveryAction; +import org.tron.core.db2.stateroot.PathStateFlushTarget; + +public class CommonCheckpointRedoCoordinatorTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void appliesTwoBarriersThenRetiresAndSecondRecoveryDoesNothing() throws Exception { + Fixture fixture = fixture("normal", null); + + assertEquals(RecoveryAction.COMPLETED_REDO, fixture.coordinator.apply(fixture.payload)); + assertEquals(list("materialize-CHAINBASE", "materialize-PATH_STATE", + "materialize-STATE_ARCHIVE", "publish-CHAINBASE", "publish-PATH_STATE", + "publish-STATE_ARCHIVE"), fixture.actions); + assertFalse(Files.exists(fixture.file.getCheckpointPath())); + int actionCount = fixture.actions.size(); + assertEquals(RecoveryAction.NO_CHECKPOINT, fixture.coordinator.recover()); + assertEquals(actionCount, fixture.actions.size()); + } + + @Test + public void resumesEveryCoordinatorBoundaryAgainstTheSameTarget() throws Exception { + for (CommonCheckpointRedoCoordinator.Stage stage + : CommonCheckpointRedoCoordinator.Stage.values()) { + Fixture interrupted = fixture("failure-" + stage, stage); + assertThrows(IOException.class, () -> interrupted.coordinator.apply(interrupted.payload)); + + CommonCheckpointRedoCoordinator recovered = interrupted.coordinator(null); + RecoveryAction expected = stage + == CommonCheckpointRedoCoordinator.Stage.AFTER_CHECKPOINT_RETIRE + ? RecoveryAction.NO_CHECKPOINT : RecoveryAction.COMPLETED_REDO; + assertEquals(expected, recovered.recover()); + assertFalse(Files.exists(interrupted.file.getCheckpointPath())); + assertEquals(RecoveryAction.NO_CHECKPOINT, recovered.recover()); + for (FakeMaterializer materializer : interrupted.materializers) { + assertEquals(Status.PUBLISHED, materializer.status); + assertEquals(CommonCheckpointTarget.from(interrupted.payload), materializer.target); + } + } + } + + @Test + public void rejectsPublishedAuthorityBeforeGlobalMaterializationBarrier() throws Exception { + Fixture fixture = fixture("invalid-partial-publish", null); + fixture.materializers.get(0).status = Status.PUBLISHED; + + assertThrows(IOException.class, () -> fixture.coordinator.apply(fixture.payload)); + assertTrue(Files.isRegularFile(fixture.file.getCheckpointPath())); + assertTrue(fixture.actions.isEmpty()); + } + + @Test + public void rejectsWrongAuthorityAndMaterializerThatDoesNotReachExactStatus() throws Exception { + Fixture fixture = fixture("invalid-materializer", null); + assertThrows(IllegalArgumentException.class, () -> new CommonCheckpointRedoCoordinator( + fixture.file, fixture.materializers.get(1), fixture.materializers.get(0), + fixture.materializers.get(2))); + + fixture.materializers.get(0).advanceAfterMaterialize = false; + assertThrows(IOException.class, () -> fixture.coordinator.apply(fixture.payload)); + assertTrue(Files.isRegularFile(fixture.file.getCheckpointPath())); + assertEquals(Collections.singletonList("materialize-CHAINBASE"), fixture.actions); + } + + @Test + public void runtimeOwnerRequiresStartupRecoveryAndGatesReadsAroundApply() throws Exception { + Fixture fixture = fixture("runtime-owner", null); + CommonCheckpointRuntimeOwner owner = new CommonCheckpointRuntimeOwner(fixture.coordinator); + + assertEquals(CommonCheckpointRuntimeOwner.State.NEW, owner.getState()); + assertThrows(IOException.class, () -> owner.read(() -> "unreachable")); + assertEquals(RecoveryAction.NO_CHECKPOINT, owner.recoverBeforeServing()); + assertEquals("ready", owner.read(() -> "ready")); + assertEquals(RecoveryAction.COMPLETED_REDO, owner.apply(fixture.payload)); + assertEquals(CommonCheckpointRuntimeOwner.State.READY, owner.getState()); + assertEquals("published", owner.read(() -> "published")); + owner.close(); + assertThrows(IOException.class, () -> owner.read(() -> "unreachable")); + } + + @Test + public void runtimeOwnerRequestLeaseBlocksCheckpointUntilClosed() throws Exception { + Fixture fixture = fixture("runtime-owner-request-lease", null); + CommonCheckpointRuntimeOwner owner = new CommonCheckpointRuntimeOwner(fixture.coordinator); + assertEquals(RecoveryAction.NO_CHECKPOINT, owner.recoverBeforeServing()); + + CommonCheckpointRuntimeOwner.ReadLease lease = owner.acquireReadLease(); + CountDownLatch started = new CountDownLatch(1); + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future checkpoint = executor.submit(() -> { + started.countDown(); + return owner.apply(fixture.payload); + }); + try { + assertTrue(started.await(5, TimeUnit.SECONDS)); + assertThrows(TimeoutException.class, + () -> checkpoint.get(100, TimeUnit.MILLISECONDS)); + } finally { + lease.close(); + } + try { + assertEquals(RecoveryAction.COMPLETED_REDO, checkpoint.get(5, TimeUnit.SECONDS)); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void runtimeOwnerFailsClosedThenFreshOwnerRedoesDurableCheckpoint() throws Exception { + Fixture fixture = fixture("runtime-owner-failure", + CommonCheckpointRedoCoordinator.Stage.AFTER_CHAINBASE_MATERIALIZE); + CommonCheckpointRuntimeOwner failed = new CommonCheckpointRuntimeOwner(fixture.coordinator); + assertEquals(RecoveryAction.NO_CHECKPOINT, failed.recoverBeforeServing()); + assertThrows(IOException.class, () -> failed.apply(fixture.payload)); + assertEquals(CommonCheckpointRuntimeOwner.State.FAILED, failed.getState()); + assertThrows(IOException.class, () -> failed.read(() -> "unreachable")); + + CommonCheckpointRuntimeOwner recovered = new CommonCheckpointRuntimeOwner( + fixture.coordinator(null)); + assertEquals(RecoveryAction.COMPLETED_REDO, recovered.recoverBeforeServing()); + assertEquals(CommonCheckpointRuntimeOwner.State.READY, recovered.getState()); + assertEquals("recovered", recovered.read(() -> "recovered")); + } + + private Fixture fixture(String name, CommonCheckpointRedoCoordinator.Stage failure) { + CommonCheckpointPayload payload = payload(); + CommonCheckpointFile file = new CommonCheckpointFile( + temporaryFolder.getRoot().toPath().resolve(name)); + List actions = new ArrayList<>(); + List materializers = new ArrayList<>(); + for (Authority authority : Authority.values()) { + materializers.add(new FakeMaterializer(authority, actions)); + } + return new Fixture(payload, file, actions, materializers, failure); + } + + private static CommonCheckpointPayload payload() { + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); + byte[] viewDigest = hash(4); + PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); + when(binding.getMeta()).thenReturn(meta); + when(binding.getParentStateRoot()).thenReturn(hash(5)); + when(binding.getStateRoot()).thenReturn(hash(6)); + when(binding.getTransitionPayloadDigest()).thenReturn(hash(3)); + when(binding.getMutationViewDigest()).thenReturn(viewDigest); + PathStateFlushTarget target = mock(PathStateFlushTarget.class); + when(target.getBlocks()).thenReturn(Collections.singletonList(binding)); + when(target.getParentStateRoot()).thenReturn(hash(5)); + when(target.getStateRoot()).thenReturn(hash(6)); + when(target.getStores()).thenReturn(Collections.emptyList()); + when(target.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + return CommonCheckpointPayload.create(hash(7), target, + Collections.singletonList(new BlockReverseDiff(meta, Collections.emptyList(), viewDigest)), + Collections.emptyList()); + } + + private static List list(String... values) { + List result = new ArrayList<>(); + Collections.addAll(result, values); + return result; + } + + private static byte[] hash(int seed) { + byte[] hash = new byte[32]; + for (int index = 0; index < hash.length; index++) { + hash[index] = (byte) (seed + index); + } + return hash; + } + + private static final class Fixture { + + private final CommonCheckpointPayload payload; + private final CommonCheckpointFile file; + private final List actions; + private final List materializers; + private final CommonCheckpointRedoCoordinator coordinator; + + private Fixture(CommonCheckpointPayload payload, CommonCheckpointFile file, + List actions, List materializers, + CommonCheckpointRedoCoordinator.Stage failure) { + this.payload = payload; + this.file = file; + this.actions = actions; + this.materializers = materializers; + this.coordinator = coordinator(failure); + } + + private CommonCheckpointRedoCoordinator coordinator( + CommonCheckpointRedoCoordinator.Stage failedStage) { + return new CommonCheckpointRedoCoordinator(file, materializers.get(0), + materializers.get(1), materializers.get(2), failAt(failedStage)); + } + } + + private static CommonCheckpointRedoCoordinator.FaultHook failAt( + CommonCheckpointRedoCoordinator.Stage failedStage) { + return stage -> { + if (stage == failedStage) { + throw new IOException("injected " + stage); + } + }; + } + + private static final class FakeMaterializer implements CommonCheckpointMaterializer { + + private final Authority authority; + private final List actions; + private Status status = Status.NEEDS_MATERIALIZATION; + private CommonCheckpointTarget target; + private boolean advanceAfterMaterialize = true; + + private FakeMaterializer(Authority authority, List actions) { + this.authority = authority; + this.actions = actions; + } + + @Override + public Authority authority() { + return authority; + } + + @Override + public Status inspect(CommonCheckpointTarget expected) throws IOException { + if (target != null && !target.equals(expected)) { + throw new IOException("target mismatch"); + } + return status; + } + + @Override + public void materialize(CommonCheckpointPayload payload, CommonCheckpointTarget expected) { + actions.add("materialize-" + authority); + target = expected; + if (advanceAfterMaterialize) { + status = Status.MATERIALIZED; + } + } + + @Override + public void publish(CommonCheckpointTarget expected) throws IOException { + if (status != Status.MATERIALIZED || !expected.equals(target)) { + throw new IOException("publish without exact materialization"); + } + actions.add("publish-" + authority); + status = Status.PUBLISHED; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java new file mode 100644 index 00000000000..9eb21874365 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java @@ -0,0 +1,112 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; + +public class CommonCheckpointRuntimeAttachmentTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void disabledAttachmentDoesNotConstructRuntimeOrCreateDirectory() throws Exception { + Path root = temporaryFolder.getRoot().toPath().resolve("disabled"); + AtomicBoolean invoked = new AtomicBoolean(); + CommonCheckpointRuntimeAttachment attachment = CommonCheckpointRuntimeAttachment.open(false, + () -> { + invoked.set(true); + Files.createDirectories(root); + return runtime(root, mock(Chainbase.class)); + }); + + assertFalse(invoked.get()); + assertFalse(Files.exists(root)); + assertFalse(attachment.isEnabled()); + assertEquals(CommonCheckpointRuntimeAttachment.State.DISABLED, attachment.getState()); + assertThrows(IllegalStateException.class, () -> attachment.checkpointAndRebase(1)); + assertThrows(IllegalStateException.class, () -> attachment.pinPoint(0)); + attachment.close(); + assertEquals(CommonCheckpointRuntimeAttachment.State.CLOSED, attachment.getState()); + assertFalse(Files.exists(root)); + } + + @Test + public void enabledAttachmentRecoversBeforeReadyAndOwnsClose() throws Exception { + Path root = temporaryFolder.getRoot().toPath().resolve("enabled"); + CommonCheckpointRuntime runtime = runtime(root, mock(Chainbase.class)); + CommonCheckpointRuntimeAttachment attachment = CommonCheckpointRuntimeAttachment.open(true, + () -> runtime); + + assertTrue(attachment.isEnabled()); + assertEquals(CommonCheckpointRuntimeAttachment.State.READY, attachment.getState()); + assertEquals(CommonCheckpointRuntimeOwner.State.READY, runtime.getState()); + assertThrows(IOException.class, () -> attachment.pinPoint(0)); + assertEquals(CommonCheckpointRuntimeAttachment.State.READY, attachment.getState()); + attachment.close(); + attachment.close(); + assertEquals(CommonCheckpointRuntimeAttachment.State.CLOSED, attachment.getState()); + assertEquals(CommonCheckpointRuntimeOwner.State.CLOSED, runtime.getState()); + } + + @Test + public void startupAndCheckpointFailuresRemainFailClosed() throws Exception { + Path corruptRoot = temporaryFolder.getRoot().toPath().resolve("corrupt"); + Path wal = corruptRoot.resolve("wal"); + Files.createDirectories(wal); + Files.write(wal.resolve(CommonCheckpointFile.FILE_NAME), new byte[]{1}); + CommonCheckpointRuntime corrupt = runtime(corruptRoot, mock(Chainbase.class)); + assertThrows(IOException.class, + () -> CommonCheckpointRuntimeAttachment.open(true, () -> corrupt)); + assertEquals(CommonCheckpointRuntimeOwner.State.CLOSED, corrupt.getState()); + + Path failureRoot = temporaryFolder.getRoot().toPath().resolve("checkpoint-failure"); + Chainbase database = mock(Chainbase.class); + when(database.getHead()).thenThrow(new IllegalStateException("injected capture failure")); + CommonCheckpointRuntimeAttachment attachment = CommonCheckpointRuntimeAttachment.open(true, + () -> runtime(failureRoot, database)); + assertThrows(IllegalStateException.class, () -> attachment.checkpointAndRebase(1)); + assertEquals(CommonCheckpointRuntimeAttachment.State.FAILED, attachment.getState()); + assertThrows(IllegalStateException.class, () -> attachment.pinPoint(0)); + attachment.close(); + assertEquals(CommonCheckpointRuntimeAttachment.State.CLOSED, attachment.getState()); + } + + private static CommonCheckpointRuntime runtime(Path root, Chainbase database) { + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( + new CommonCheckpointFile(root.resolve("wal")), materializer(Authority.CHAINBASE), + materializer(Authority.PATH_STATE), materializer(Authority.STATE_ARCHIVE)); + return new CommonCheckpointRuntime(new CommonCheckpointRuntimeOwner(coordinator), + Collections.singletonList(database), root.resolve("archive"), hash(1), + (blockNumber, blockHash) -> { + throw new IOException("latest state is intentionally unavailable"); + }); + } + + private static CommonCheckpointMaterializer materializer(Authority authority) { + CommonCheckpointMaterializer materializer = mock(CommonCheckpointMaterializer.class); + when(materializer.authority()).thenReturn(authority); + return materializer; + } + + private static byte[] hash(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java index d95fd587da3..bd75167cc3e 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java @@ -8,9 +8,13 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.bouncycastle.util.encoders.Hex; import org.junit.Test; import org.tron.common.crypto.Hash; @@ -174,6 +178,161 @@ public void restoresRootAndLoadsOnlyTheChangedPath() { assertTrue(restored.getLastNodePuts() <= PathMerkleTrie.SECURE_KEY_LENGTH * 2 + 1); } + @Test + public void leavesHashedSiblingsUnresolvedUntilTheirPathIsVisited() { + byte[][] keys = new byte[16][]; + byte[][] values = new byte[16][]; + InMemoryPathNodeStore sourceStore = new InMemoryPathNodeStore(); + PathMerkleTrie source = new PathMerkleTrie(sourceStore); + for (int index = 0; index < keys.length; index++) { + keys[index] = filledKey(index << 4); + values[index] = value("hashed-child-value-that-is-long-enough-" + index); + source.put(keys[index], values[index]); + } + byte[] root = source.rootHash(); + + InMemoryPathNodeStore restoredStore = new InMemoryPathNodeStore(); + restoredStore.nodes.putAll(sourceStore.nodes); + PathMerkleTrie restored = new PathMerkleTrie(restoredStore); + restored.restoreRoot(root); + int readsAfterRoot = restoredStore.gets; + String corruptSibling = Hex.toHexString(new byte[]{(byte) 0x0a}); + assertTrue(restoredStore.nodes.containsKey(corruptSibling)); + restoredStore.nodes.put(corruptSibling, new byte[]{1}); + + assertArrayEquals(values[3], restored.get(keys[3])); + assertEquals(readsAfterRoot + 1, restoredStore.gets); + assertEquals(16, restored.getHashReferenceCreateCount()); + assertEquals(1, restored.getHashReferenceResolveCount()); + assertThrows(IllegalStateException.class, restored::verifyAllNodeStore); + } + + @Test + public void lazyDenseBranchHasAStableEagerReadBaseline() { + byte[][] keys = new byte[16][]; + byte[][] values = new byte[16][]; + InMemoryPathNodeStore sourceStore = new InMemoryPathNodeStore(); + PathMerkleTrie source = new PathMerkleTrie(sourceStore); + for (int index = 0; index < keys.length; index++) { + keys[index] = filledKey(index << 4); + values[index] = value("hashed-child-value-that-is-long-enough-" + index); + source.put(keys[index], values[index]); + } + byte[] root = source.rootHash(); + + InMemoryPathNodeStore eagerStore = new InMemoryPathNodeStore(); + eagerStore.nodes.putAll(copyNodeMap(sourceStore.nodes)); + PathMerkleTrie eager = new PathMerkleTrie(eagerStore, false); + eager.restoreRoot(root); + assertArrayEquals(values[3], eager.get(keys[3])); + + InMemoryPathNodeStore lazyStore = new InMemoryPathNodeStore(); + lazyStore.nodes.putAll(copyNodeMap(sourceStore.nodes)); + PathMerkleTrie lazy = new PathMerkleTrie(lazyStore); + lazy.restoreRoot(root); + assertArrayEquals(values[3], lazy.get(keys[3])); + + assertEquals(17, eagerStore.gets); + assertEquals(2, lazyStore.gets); + assertEquals(17, eager.getNodeHashVerifyCount()); + assertEquals(2, lazy.getNodeHashVerifyCount()); + assertEquals(eager.getNodeDecodeCount(), lazy.getNodeDecodeCount()); + assertEquals(16, lazy.getHashReferenceCreateCount()); + assertEquals(1, lazy.getHashReferenceResolveCount()); + + assertArrayEquals(values[3], lazy.get(keys[3])); + assertEquals(2, lazyStore.gets); + assertEquals(2, lazy.getNodeHashVerifyCount()); + assertEquals(1, lazy.getHashReferenceResolveCount()); + } + + @Test + public void lazyRestoreProducesTheSameUpdatedPathNodeSet() { + int leafCount = 128; + byte[][] keys = new byte[leafCount][]; + byte[][] values = new byte[leafCount][]; + InMemoryPathNodeStore sourceStore = new InMemoryPathNodeStore(); + PathMerkleTrie source = new PathMerkleTrie(sourceStore); + for (int index = 0; index < leafCount; index++) { + keys[index] = Hash.sha3(value("key-" + index)); + values[index] = value("initial-value-" + index); + source.put(keys[index], values[index]); + } + byte[] initialRoot = source.rootHash(); + Map initialNodes = copyNodeMap(sourceStore.nodes); + + values[17] = value("updated-value"); + InMemoryPathNodeStore eagerStore = new InMemoryPathNodeStore(); + eagerStore.nodes.putAll(copyNodeMap(initialNodes)); + PathMerkleTrie eager = new PathMerkleTrie(eagerStore, false); + eager.restoreRoot(initialRoot); + eager.put(keys[17], values[17]); + eager.delete(keys[33]); + byte[] expectedRoot = eager.rootHash(); + + InMemoryPathNodeStore lazyStore = new InMemoryPathNodeStore(); + lazyStore.nodes.putAll(initialNodes); + PathMerkleTrie lazy = new PathMerkleTrie(lazyStore); + lazy.restoreRoot(initialRoot); + lazy.put(keys[17], values[17]); + lazy.delete(keys[33]); + + assertArrayEquals(expectedRoot, lazy.rootHash()); + assertNodeMapsEqual(eagerStore.nodes, lazyStore.nodes); + } + + @Test + public void prefixBatchMatchesSequentialMixedChangesFromLazyRoot() { + int leafCount = 512; + byte[][] keys = new byte[leafCount][]; + byte[][] values = new byte[leafCount][]; + InMemoryPathNodeStore sourceStore = new InMemoryPathNodeStore(); + PathMerkleTrie source = new PathMerkleTrie(sourceStore); + for (int index = 0; index < leafCount; index++) { + keys[index] = Hash.sha3(value("batch-key-" + index)); + values[index] = value("batch-value-" + index); + source.put(keys[index], values[index]); + } + byte[] initialRoot = source.rootHash(); + Map initialNodes = copyNodeMap(sourceStore.nodes); + + InMemoryPathNodeStore sequentialStore = new InMemoryPathNodeStore(); + sequentialStore.nodes.putAll(copyNodeMap(initialNodes)); + PathMerkleTrie sequential = new PathMerkleTrie(sequentialStore); + sequential.restoreRoot(initialRoot); + List changes = new ArrayList<>(); + for (int index = 0; index < 40; index++) { + byte[] updated = value("batch-updated-" + index); + sequential.put(keys[index], updated); + changes.add(new PathMerkleTrie.BatchMutation(keys[index], updated)); + } + for (int index = 40; index < 60; index++) { + sequential.delete(keys[index]); + changes.add(new PathMerkleTrie.BatchMutation(keys[index], null)); + } + for (int index = 0; index < 40; index++) { + byte[] insertedKey = Hash.sha3(value("batch-inserted-key-" + index)); + byte[] insertedValue = value("batch-inserted-value-" + index); + sequential.put(insertedKey, insertedValue); + changes.add(new PathMerkleTrie.BatchMutation(insertedKey, insertedValue)); + } + byte[] expectedRoot = sequential.rootHash(); + + InMemoryPathNodeStore batchStore = new InMemoryPathNodeStore(); + batchStore.nodes.putAll(copyNodeMap(initialNodes)); + PathMerkleTrie batch = new PathMerkleTrie(batchStore); + batch.restoreRoot(initialRoot); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + batch.applyBatch(changes, executor); + assertArrayEquals(expectedRoot, batch.rootHash()); + assertNodeMapsEqual(sequentialStore.nodes, batchStore.nodes); + assertTrue(batchStore.gets <= sequentialStore.gets); + } finally { + executor.shutdownNow(); + } + } + private static byte[] referenceRoot(byte[][] keys, byte[][] values) { TrieImpl reference = new TrieImpl(); reference.setAsync(false); @@ -213,6 +372,14 @@ private static void assertNodeMapsEqual(Map expected, } } + private static Map copyNodeMap(Map source) { + Map copy = new LinkedHashMap<>(); + for (Map.Entry entry : source.entrySet()) { + copy.put(entry.getKey(), Arrays.copyOf(entry.getValue(), entry.getValue().length)); + } + return copy; + } + private static final class InMemoryPathNodeStore implements PathNodeStore { private final Map nodes = new LinkedHashMap<>(); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java index a25602fd45e..620be9d1e62 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateBlockTransitionTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import com.google.common.hash.Hashing; import java.io.ByteArrayOutputStream; @@ -124,6 +125,22 @@ public void ownsBlockAndMutationBytes() { () -> transition.getMutations().add(PathStateMutation.delete("proposal", new byte[]{3}))); } + @Test + public void ownsAnOptionalAuthoritativePreviousValue() { + byte[] previous = new byte[]{3, 4}; + PathStateMutation mutation = PathStateMutation.put( + "proposal", new byte[]{1}, new byte[]{2}).withPreviousPhysicalValue(previous); + + previous[0] = 9; + assertTrue(mutation.isPreviousValueKnown()); + assertArrayEquals(new byte[]{3, 4}, mutation.getPreviousPhysicalValue()); + mutation.getPreviousPhysicalValue()[0] = 8; + assertArrayEquals(new byte[]{3, 4}, mutation.getPreviousPhysicalValue()); + + PathStateMutation unknown = PathStateMutation.delete("proposal", new byte[]{1}); + assertThrows(IllegalStateException.class, unknown::getPreviousPhysicalValue); + } + private static PathStateBlockTransition transition(List mutations) { return new PathStateBlockTransition(42, BLOCK_HASH, PARENT_HASH, 1234, P66Phase.P66_ON, mutations); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializerTest.java new file mode 100644 index 00000000000..fe147ae0f16 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializerTest.java @@ -0,0 +1,220 @@ +package org.tron.core.db2.stateroot; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Status; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class PathStateCheckpointMaterializerTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void separatesSyncedMaterializationFromCurrentPublicationAcrossReopen() + throws Exception { + Fixture fixture = fixture("normal", null); + try { + assertEquals(Status.NEEDS_MATERIALIZATION, + fixture.materializer.inspect(fixture.target)); + fixture.materializer.materialize(fixture.payload, fixture.target); + assertEquals(Status.MATERIALIZED, fixture.materializer.inspect(fixture.target)); + assertFalse(Files.exists(fixture.root.resolve( + PathStateCheckpointMaterializer.CURRENT_FILE))); + assertArrayEquals(new byte[]{2}, fixture.stores.participant("account") + .getFlat(new byte[]{1})); + assertArrayEquals(new byte[]{4}, fixture.stores.participant("account") + .nodeStore().get(new byte[]{3})); + assertArrayEquals(new byte[]{6}, fixture.stores.superStore().nodeStore() + .get(new byte[]{5})); + long accountBatches = fixture.stores.participant("account").getSyncedWriteBatchCalls(); + long superBatches = fixture.stores.superStore().getSyncedWriteBatchCalls(); + fixture.materializer.materialize(fixture.payload, fixture.target); + assertEquals(accountBatches, + fixture.stores.participant("account").getSyncedWriteBatchCalls()); + assertEquals(superBatches, fixture.stores.superStore().getSyncedWriteBatchCalls()); + fixture.materializer.publish(fixture.target); + assertEquals(Status.PUBLISHED, fixture.materializer.inspect(fixture.target)); + } finally { + fixture.stores.close(); + } + + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.openExisting(fixture.root, + fixture.scope, Engine.ROCKSDB)) { + PathStateCheckpointMaterializer recovered = new PathStateCheckpointMaterializer(reopened, + fixture.scope, fixture.formatIdentity); + assertEquals(Status.PUBLISHED, recovered.inspect(fixture.target)); + recovered.publish(fixture.target); + + CommonCheckpointPayload child = payload(fixture.formatIdentity, 2, hash(1), hash(2), + hash(11), hash(12)); + CommonCheckpointTarget childTarget = CommonCheckpointTarget.from(child); + assertEquals(Status.NEEDS_MATERIALIZATION, recovered.inspect(childTarget)); + recovered.materialize(child, childTarget); + recovered.publish(childTarget); + assertEquals(Status.PUBLISHED, recovered.inspect(childTarget)); + } + } + + @Test + public void resumesEveryStoreAndMarkerBoundaryWithoutRepeatingExactBatches() + throws Exception { + for (PathStateCheckpointMaterializer.Stage stage + : PathStateCheckpointMaterializer.Stage.values()) { + Fixture fixture = fixture("fault-" + stage, stage); + try { + if (stage == PathStateCheckpointMaterializer.Stage.AFTER_CURRENT) { + fixture.materializer.materialize(fixture.payload, fixture.target); + assertThrows(IOException.class, () -> fixture.materializer.publish(fixture.target)); + } else { + assertThrows(IOException.class, + () -> fixture.materializer.materialize(fixture.payload, fixture.target)); + } + } finally { + fixture.stores.close(); + } + + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.openExisting(fixture.root, + fixture.scope, Engine.ROCKSDB)) { + PathStateCheckpointMaterializer recovered = new PathStateCheckpointMaterializer(reopened, + fixture.scope, fixture.formatIdentity); + Status status = recovered.inspect(fixture.target); + if (status == Status.NEEDS_MATERIALIZATION) { + recovered.materialize(fixture.payload, fixture.target); + } + recovered.publish(fixture.target); + assertEquals(Status.PUBLISHED, recovered.inspect(fixture.target)); + } + } + } + + @Test + public void rejectsForeignFormatCorruptCurrentAndNonParentTarget() throws Exception { + Fixture fixture = fixture("reject", null); + try { + CommonCheckpointPayload foreign = payload(hash(9), 1, hash(0), hash(1), hash(10), hash(11)); + assertThrows(IOException.class, () -> fixture.materializer.materialize(foreign, + CommonCheckpointTarget.from(foreign))); + + fixture.materializer.materialize(fixture.payload, fixture.target); + fixture.materializer.publish(fixture.target); + CommonCheckpointPayload nonChild = payload(fixture.formatIdentity, 3, hash(8), hash(9), + hash(12), hash(13)); + assertThrows(IOException.class, + () -> fixture.materializer.inspect(CommonCheckpointTarget.from(nonChild))); + + byte[] corrupt = Files.readAllBytes(fixture.root.resolve( + PathStateCheckpointMaterializer.CURRENT_FILE)); + corrupt[corrupt.length - 1] ^= 1; + Files.write(fixture.root.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), corrupt); + assertThrows(IOException.class, () -> fixture.materializer.inspect(fixture.target)); + assertTrue(Files.isRegularFile(fixture.root.resolve( + PathStateCheckpointMaterializer.CURRENT_FILE))); + } finally { + fixture.stores.close(); + } + } + + private Fixture fixture(String name, PathStateCheckpointMaterializer.Stage failedStage) + throws Exception { + Path root = temporaryFolder.newFolder(name).toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, Engine.ROCKSDB); + byte[] formatIdentity = hash(7); + CommonCheckpointPayload payload = payload(formatIdentity, 1, hash(0), hash(1), + hash(10), hash(11)); + PathStateCheckpointMaterializer materializer = new PathStateCheckpointMaterializer(stores, + scope, formatIdentity, failAt(failedStage)); + return new Fixture(root, scope, stores, formatIdentity, payload, materializer); + } + + private static PathStateCheckpointMaterializer.FaultHook failAt( + PathStateCheckpointMaterializer.Stage failedStage) { + return (stage, storeId) -> { + if (stage == failedStage) { + throw new IOException("injected " + stage + " at " + storeId); + } + }; + } + + private static CommonCheckpointPayload payload(byte[] formatIdentity, long blockNumber, + byte[] parentHash, byte[] blockHash, byte[] parentRoot, byte[] stateRoot) { + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(blockNumber, blockHash, parentHash, + blockNumber * 3_000L); + byte[] viewDigest = hash((int) blockNumber + 20); + PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); + when(binding.getMeta()).thenReturn(meta); + when(binding.getParentStateRoot()).thenReturn(parentRoot); + when(binding.getStateRoot()).thenReturn(stateRoot); + when(binding.getTransitionPayloadDigest()).thenReturn(hash((int) blockNumber + 30)); + when(binding.getMutationViewDigest()).thenReturn(viewDigest); + + PathStateFlushTarget.StoreTarget store = mock(PathStateFlushTarget.StoreTarget.class); + when(store.getStoreId()).thenReturn(4); + when(store.getDbName()).thenReturn("account"); + when(store.getStoreRoot()).thenReturn(hash((int) blockNumber + 40)); + when(store.getFlatMutations()).thenReturn(Collections.singletonList( + new PathStateSnapshotDelta.Mutation(new byte[]{1}, new byte[]{2}))); + when(store.getNodeMutations()).thenReturn(Collections.singletonList( + new PathStateSnapshotDelta.Mutation(new byte[]{3}, new byte[]{4}))); + + PathStateFlushTarget target = mock(PathStateFlushTarget.class); + when(target.getBlocks()).thenReturn(Collections.singletonList(binding)); + when(target.getParentStateRoot()).thenReturn(parentRoot); + when(target.getStateRoot()).thenReturn(stateRoot); + when(target.getStores()).thenReturn(Collections.singletonList(store)); + when(target.getSuperNodeMutations()).thenReturn(Collections.singletonList( + new PathStateSnapshotDelta.Mutation(new byte[]{5}, new byte[]{6}))); + return CommonCheckpointPayload.create(formatIdentity, target, + Collections.singletonList(new BlockReverseDiff(meta, Collections.emptyList(), viewDigest)), + Collections.emptyList()); + } + + private static byte[] hash(int seed) { + byte[] hash = new byte[32]; + for (int index = 0; index < hash.length; index++) { + hash[index] = (byte) (seed + index); + } + return hash; + } + + private static final class Fixture { + + private final Path root; + private final PathStateParticipantScope scope; + private final PathStatePhysicalStoreSet stores; + private final byte[] formatIdentity; + private final CommonCheckpointPayload payload; + private final CommonCheckpointTarget target; + private final PathStateCheckpointMaterializer materializer; + + private Fixture(Path root, PathStateParticipantScope scope, + PathStatePhysicalStoreSet stores, byte[] formatIdentity, + CommonCheckpointPayload payload, PathStateCheckpointMaterializer materializer) { + this.root = root; + this.scope = scope; + this.stores = stores; + this.formatIdentity = formatIdentity; + this.payload = payload; + this.target = CommonCheckpointTarget.from(payload); + this.materializer = materializer; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java index c897561e15a..d82efb13e87 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateLayerTest.java @@ -91,6 +91,38 @@ public void staleParentFailsBeforeCreatingLayerDirectory() throws Exception { assertFalse(Files.exists(rejected)); } + @Test + public void preparedLayerValidationDoesNotReadNodeStores() throws Exception { + Fixture fixture = publishedBase("prepared-zero-read", Engine.ROCKSDB); + PathStateNodeStoreSet parentStores = PathStateNodeStoreSet.openPublished( + fixture.manifest, fixture.base); + PathStateNodeStoreSet childStores = null; + try { + PathStateRoot parentRoot = parentStores.createRoot(); + PathStateBlockTransition transition = new PathStateBlockTransition(101, bytes(11), + fixture.base.getBlockHash(), 303, P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5}))); + PreparedPathStateTransition prepared = PreparedPathStateTransition.prepare( + fixture.base, parentRoot.snapshot(), transition); + PathStateRootMetadata identity = PathStateRootMetadata.layer(101, bytes(11), + fixture.base.getBlockHash(), 303, P66Phase.P66_ON, + fixture.manifest.getIdentityDigest(), fixture.base.getStateRoot(), + prepared.getStateRoot(), transition.getPayloadDigest()); + childStores = PathStateNodeStoreSet.beginLayer(fixture.manifest, identity, parentStores); + long readsBefore = childStores.nodeStoreGetCalls(); + + assertArrayEquals(prepared.getStateRoot(), + childStores.createRootFrom(prepared).rootHash()); + assertEquals(readsBefore, childStores.nodeStoreGetCalls()); + } finally { + if (childStores != null) { + childStores.close(); + } else { + parentStores.close(); + } + } + } + @Test public void currentLayerRestoreFailsClosedWhenDurableLeafIsMissing() throws Exception { Fixture fixture = publishedBase("corrupt-layer", Engine.ROCKSDB); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index 3b296919834..3642e27d00d 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -27,6 +27,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.tron.common.arch.Arch; +import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.EntryConsumer; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; @@ -645,7 +646,11 @@ public void physicalBlockFinalTransitionPreviewsPublishesAndRestarts() throws Ex 6, P66Phase.P66_ON, Arrays.asList( PathStateMutation.put("code", key, new byte[]{6}), PathStateMutation.put("proposal", new byte[]{7}, new byte[]{8}))); - assertArrayEquals(head.preview(update), head.advance(update).getStateRoot()); + byte[] preview = head.preview(update); + PathStateSnapshotDelta delta = head.prepareSnapshotDelta( + BlockSnapshotMeta.forBlock(2, bytes(32), blockHash, 6), update); + assertArrayEquals(preview, delta.getStateRoot()); + assertArrayEquals(delta.getStateRoot(), head.advance(update).getStateRoot()); PathStateBlockTransition delete = new PathStateBlockTransition(3, bytes(33), bytes(32), 9, P66Phase.P66_ON, Collections.singletonList(PathStateMutation.delete("code", key))); @@ -663,6 +668,137 @@ public void physicalBlockFinalTransitionPreviewsPublishesAndRestarts() throws Ex } } + @Test + public void physicalSnapshotDeltaPreparesWithoutWritesAndReusesPlanForPublication() + throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-snapshot-delta").toPath(); + preparePublishedPhysicalTarget(root, scope); + byte[] key = new byte[]{1, 2, 3}; + byte[] blockHash = bytes(35); + + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, scope, + Engine.ROCKSDB)) { + PathStateBlockTransition transition = new PathStateBlockTransition(1, blockHash, + new byte[32], 3, P66Phase.P66_ON, + Collections.singletonList(PathStateMutation.put("code", key, new byte[]{4, 5}))); + PathStatePhysicalStoreSet.PreparedPhysicalTransition prepared = + stores.prepareSnapshotDelta(BlockSnapshotMeta.forBlock(1, blockHash, new byte[32], 3), + transition); + PathStateSnapshotDelta delta = prepared.getSnapshotDelta(); + + assertEquals(0, stores.currentMetadata().getBlockNumber()); + assertEquals(0, stores.participant("code").getUnsyncedWriteBatchCalls()); + assertEquals(0, stores.superStore().getUnsyncedWriteBatchCalls()); + assertEquals(1, delta.getStores().size()); + assertEquals("code", delta.getStores().get(0).getDbName()); + assertArrayEquals(PathStateCommitmentCodec.storeLeafKey( + scope.require("code").getStoreId(), key), + delta.getStores().get(0).getFlatMutations().get(0).getKey()); + assertArrayEquals(delta.getStateRoot(), delta.getTrieSnapshot().getStateRoot()); + + PathStateRootMetadata committed = stores.applyAndPublish(prepared, + PathStateLayerLimits.defaults()); + assertArrayEquals(delta.getStateRoot(), committed.getStateRoot()); + assertEquals(1, stores.participant("code").getUnsyncedWriteBatchCalls()); + assertEquals(1, stores.superStore().getUnsyncedWriteBatchCalls()); + + PathStateBlockTransition next = new PathStateBlockTransition(2, bytes(36), blockHash, + 6, P66Phase.P66_ON, + Collections.singletonList(PathStateMutation.put("code", key, new byte[]{6}))); + PathStatePhysicalStoreSet.PreparedPhysicalTransition nextPrepared = + stores.prepareSnapshotDelta(BlockSnapshotMeta.forBlock(2, bytes(36), blockHash, 6), + next); + assertTrue(nextPrepared.reusedTrieSnapshot()); + assertEquals(1, stores.participant("code").getUnsyncedWriteBatchCalls()); + stores.applyAndPublish(nextPrepared, PathStateLayerLimits.defaults()); + assertEquals(2, stores.participant("code").getUnsyncedWriteBatchCalls()); + } + } + + @Test + public void volatileOverlayAdvancesAndRewindsWithoutJournalOrDurableWrites() + throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-volatile-overlay").toPath(); + Path legacyRoot = temporaryFolder.newFolder("physical-volatile-overlay-legacy").toPath(); + preparePublishedPhysicalTarget(root, scope); + preparePublishedPhysicalTarget(legacyRoot, scope); + byte[] durableCurrent = Files.readAllBytes( + root.resolve(PathStatePhysicalStoreSet.CURRENT_FILE)); + byte[] firstHash = bytes(37); + byte[] secondHash = bytes(38); + + try (PathStatePhysicalOverlayHead head = PathStatePhysicalOverlayHead.open(root, + Engine.ROCKSDB, new PathStateLayerLimits(4, 1L << 20)); + PathStatePhysicalSnapshotHead legacy = PathStatePhysicalSnapshotHead.open(legacyRoot, + Engine.ROCKSDB, new PathStateLayerLimits(4, 1L << 20))) { + PathStateBlockTransition first = new PathStateBlockTransition(1, firstHash, + new byte[32], 3, P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{2}))); + PathStateSnapshotDelta firstDelta = head.prepareSnapshotDelta( + BlockSnapshotMeta.forBlock(1, firstHash, new byte[32], 3), first); + PathStateRootMetadata firstOverlay = head.advance(first); + assertArrayEquals(firstDelta.getStateRoot(), firstOverlay.getStateRoot()); + assertArrayEquals(legacy.advance(first).getStateRoot(), firstOverlay.getStateRoot()); + + PathStateBlockTransition second = new PathStateBlockTransition(2, secondHash, + firstHash, 6, P66Phase.P66_ON, Arrays.asList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{3}), + PathStateMutation.put("proposal", new byte[]{4}, new byte[]{5}))); + head.prepareSnapshotDelta(BlockSnapshotMeta.forBlock(2, secondHash, firstHash, 6), second); + PathStateRootMetadata secondOverlay = head.advance(second); + assertEquals(2, secondOverlay.getBlockNumber()); + assertArrayEquals(legacy.advance(second).getStateRoot(), secondOverlay.getStateRoot()); + assertEquals(0, head.durableWriteBatchCalls()); + assertArrayEquals(durableCurrent, + Files.readAllBytes(root.resolve(PathStatePhysicalStoreSet.CURRENT_FILE))); + Path reverse = root.resolve("reverse"); + assertFalse(Files.exists(reverse)); + + PathStateRootMetadata rewound = head.rewindTo(1, firstHash); + assertEquals(1, rewound.getBlockNumber()); + assertArrayEquals(firstDelta.getStateRoot(), rewound.getStateRoot()); + assertEquals(0, head.durableWriteBatchCalls()); + } + + try (PathStatePhysicalStoreSet reopened = PathStatePhysicalStoreSet.openExisting(root, scope, + Engine.ROCKSDB)) { + assertEquals(0, reopened.currentMetadata().getBlockNumber()); + assertNull(reopened.participant("code").getFlat( + PathStateCommitmentCodec.storeLeafKey(scope.require("code").getStoreId(), + new byte[]{1}))); + } + } + + @Test + public void asyncPrepareQueuesTransitionAndCompletesOffCallerThread() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-async-overlay").toPath(); + preparePublishedPhysicalTarget(root, scope); + byte[] durableCurrent = Files.readAllBytes( + root.resolve(PathStatePhysicalStoreSet.CURRENT_FILE)); + byte[] blockHash = bytes(39); + PathStateBlockTransition transition = new PathStateBlockTransition(1, blockHash, + new byte[32], 3, P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{2}))); + + PathStatePhysicalOverlayHead overlay = PathStatePhysicalOverlayHead.open(root, + Engine.ROCKSDB, new PathStateLayerLimits(4, 1L << 20)); + try (PathStateAsyncPrepareHead async = new PathStateAsyncPrepareHead(overlay, 2)) { + assertNull(async.prepareSnapshotDelta( + BlockSnapshotMeta.forBlock(1, blockHash, new byte[32], 3), transition)); + assertEquals(0, async.advance(transition).getBlockNumber()); + PathStateRootMetadata completed = async.flushBaseThrough(1, blockHash); + assertEquals(1, completed.getBlockNumber()); + assertArrayEquals(blockHash, async.getHead().getBlockHash()); + assertEquals(0, overlay.durableWriteBatchCalls()); + assertArrayEquals(durableCurrent, + Files.readAllBytes(root.resolve(PathStatePhysicalStoreSet.CURRENT_FILE))); + assertFalse(Files.exists(root.resolve("reverse"))); + } + } + @Test public void physicalBlockFinalCrashAfterSuperCompletesIntentOnRestart() throws Exception { PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java index f296ebc7316..b235815ae45 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java @@ -216,26 +216,34 @@ public void gethStyleParticipantAndRootBranchBatchMatchesSequentialRoot() { List changes = new ArrayList<>(); for (int i = 0; i < 32; i++) { changes.add(PathStateMutation.put("account", bytes("initial-" + i), - bytes("updated-" + i))); + bytes("updated-" + i)).withPreviousPhysicalValue(bytes("value-" + i))); } for (int i = 32; i < 48; i++) { - changes.add(PathStateMutation.delete("account", bytes("initial-" + i))); + changes.add(PathStateMutation.delete("account", bytes("initial-" + i)) + .withPreviousPhysicalValue(bytes("value-" + i))); } for (int i = 0; i < 24; i++) { changes.add(PathStateMutation.put("storage-row", bytes("slot-" + i), - bytes("storage-" + i))); + bytes("storage-" + i)).withPreviousPhysicalValue(null)); changes.add(PathStateMutation.put("abi", bytes("contract-" + i), - bytes("abi-" + i))); + bytes("abi-" + i)).withPreviousPhysicalValue(null)); } sequential.apply(changes); ExecutorService participants = Executors.newFixedThreadPool(4); ExecutorService branches = Executors.newFixedThreadPool(8); + PathStateRoot.ParallelApplyStats stats; try { - parallel.applyParallel(changes, participants, branches); + stats = parallel.applyParallel(changes, participants, branches); } finally { participants.shutdownNow(); branches.shutdownNow(); } + assertEquals(3, stats.participantCount()); + assertEquals(96, stats.mutationCount()); + assertEquals(96, stats.authoritativePreviousValues()); + assertEquals(48, stats.maxParticipantMutations()); + assertTrue(stats.participantWorkMillis() >= stats.maxParticipantMillis()); + assertTrue(stats.wallMillis() >= 0); assertArrayEquals(sequential.rootHash(), parallel.rootHash()); assertEquals(sequential.pendingLeafMutations().size(), parallel.pendingLeafMutations().size()); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java index e567a9fecca..836f1fbf6a2 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java @@ -1,13 +1,17 @@ package org.tron.core.db2.stateroot; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.stream.Stream; @@ -15,6 +19,13 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.tron.common.arch.Arch; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.OldValue; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointPayload.Mutation; +import org.tron.core.db2.core.CommonCheckpointPayload.StoreMutations; +import org.tron.core.db2.core.CommonCheckpointPayloadCodec; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; @@ -77,6 +88,146 @@ public void commitAdmissionFailureKeepsOwnedSnapshotAndCurrent() throws Exceptio assertFalse(owner.isFailed()); } + @Test + public void preparedTransitionFreezesAnImmutableSnapshotForwardDelta() throws Exception { + Fixture fixture = fixture("snapshot-delta", Engine.ROCKSDB); + PathStateSnapshotHead owner = PathStateSnapshotHead.open( + fixture.manifest, PathStateLayerLimits.defaults()); + PathStateBlockTransition transition = transition(101, 11, fixture.base.getBlockHash(), + Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5}))); + PreparedPathStateTransition prepared = owner.prepare(transition); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(transition.getBlockNumber(), + transition.getBlockHash(), transition.getParentHash(), transition.getTimestamp()); + + PathStateSnapshotDelta delta = prepared.toSnapshotDelta(meta); + + assertEquals(meta, delta.getMeta()); + assertArrayEquals(fixture.base.getStateRoot(), delta.getParentStateRoot()); + assertArrayEquals(prepared.getStateRoot(), delta.getStateRoot()); + assertArrayEquals(transition.getPayloadDigest(), delta.getTransitionPayloadDigest()); + assertArrayEquals(transition.getMutationViewDigest(), delta.getMutationViewDigest()); + assertEquals(1, delta.getStores().size()); + PathStateSnapshotDelta.StoreDelta store = delta.getStores().get(0); + assertEquals("proposal", store.getDbName()); + assertEquals(1, store.getFlatMutations().size()); + assertTrue(store.getNodeMutations().size() > 0); + assertTrue(delta.getSuperNodeMutations().size() > 0); + + byte[] exposedRoot = delta.getStateRoot(); + exposedRoot[0] ^= 1; + assertArrayEquals(prepared.getStateRoot(), delta.getStateRoot()); + byte[] exposedKey = store.getFlatMutations().get(0).getKey(); + exposedKey[0] ^= 1; + assertArrayEquals(PathStateCommitmentCodec.storeLeafKey(store.getStoreId(), new byte[]{1}), + store.getFlatMutations().get(0).getKey()); + + BlockSnapshotMeta wrongMeta = BlockSnapshotMeta.forBlock(101, bytes(12), + fixture.base.getBlockHash(), transition.getTimestamp()); + assertThrows(IllegalArgumentException.class, () -> prepared.toSnapshotDelta(wrongMeta)); + } + + @Test + public void coalescesConsecutiveSnapshotDeltasAndRetainsEveryBlockBinding() throws Exception { + Fixture fixture = fixture("snapshot-delta-coalesce", Engine.ROCKSDB); + PathStateSnapshotHead owner = PathStateSnapshotHead.open( + fixture.manifest, PathStateLayerLimits.defaults()); + + PathStateBlockTransition firstTransition = transition(101, 11, + fixture.base.getBlockHash(), Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{5}))); + PreparedPathStateTransition firstPrepared = owner.prepare(firstTransition); + PathStateSnapshotDelta first = firstPrepared.toSnapshotDelta(BlockSnapshotMeta.forBlock( + 101, firstTransition.getBlockHash(), firstTransition.getParentHash(), 303)); + owner.advancePrepared(firstPrepared); + + PathStateBlockTransition secondTransition = transition(102, 12, + firstTransition.getBlockHash(), Arrays.asList( + PathStateMutation.put("proposal", new byte[]{1}, new byte[]{6}), + PathStateMutation.delete("account", new byte[]{3}))); + PreparedPathStateTransition secondPrepared = owner.prepare(secondTransition); + PathStateSnapshotDelta second = secondPrepared.toSnapshotDelta(BlockSnapshotMeta.forBlock( + 102, secondTransition.getBlockHash(), secondTransition.getParentHash(), 306)); + owner.advancePrepared(secondPrepared); + + PathStateBlockTransition thirdTransition = transition(103, 13, + secondTransition.getBlockHash(), Collections.emptyList()); + PathStateSnapshotDelta third = owner.prepare(thirdTransition).toSnapshotDelta( + BlockSnapshotMeta.forBlock(103, thirdTransition.getBlockHash(), + thirdTransition.getParentHash(), 309)); + + PathStateFlushTarget target = PathStateFlushTarget.coalesce( + Arrays.asList(first, second, third)); + + assertEquals(3, target.getBlocks().size()); + assertEquals(101, target.getBlocks().get(0).getMeta().getBlockNumber()); + assertEquals(103, target.getBlocks().get(2).getMeta().getBlockNumber()); + assertArrayEquals(first.getParentStateRoot(), target.getParentStateRoot()); + assertArrayEquals(third.getStateRoot(), target.getStateRoot()); + assertEquals(2, target.getStores().size()); + PathStateFlushTarget.StoreTarget proposal = target.getStores().stream() + .filter(store -> "proposal".equals(store.getDbName())).findFirst().get(); + assertEquals(1, proposal.getFlatMutations().size()); + assertArrayEquals(PathStateCommitmentCodec.presentLeafValue(new byte[]{6}), + proposal.getFlatMutations().get(0).getValue()); + PathStateFlushTarget.StoreTarget account = target.getStores().stream() + .filter(store -> "account".equals(store.getDbName())).findFirst().get(); + assertEquals(1, account.getFlatMutations().size()); + assertTrue(account.getFlatMutations().get(0).isDelete()); + assertTrue(target.getMutationBytes() > 0); + + java.util.List archiveBlocks = new ArrayList<>(); + for (PathStateFlushTarget.BlockBinding block : target.getBlocks()) { + archiveBlocks.add(new BlockReverseDiff(block.getMeta(), Collections.singletonList( + new BlockReverseDiff.DbGroup("proposal", Collections.singletonList( + new BlockReverseDiff.Entry(new byte[]{1}, OldValue.present(new byte[]{2}))))), + block.getMutationViewDigest())); + } + CommonCheckpointPayload payload = CommonCheckpointPayload.create(bytes(77), target, + archiveBlocks, Arrays.asList( + new StoreMutations("proposal", Collections.singletonList( + new Mutation(new byte[]{1}, new byte[]{6}))), + new StoreMutations("account", Collections.singletonList( + new Mutation(new byte[]{3}, null))))); + CommonCheckpointPayloadCodec codec = new CommonCheckpointPayloadCodec(); + byte[] encoded = codec.encode(payload); + assertArrayEquals(encoded, codec.encode(payload)); + CommonCheckpointPayload decoded = codec.decode(encoded); + assertArrayEquals(payload.getFormatIdentity(), decoded.getFormatIdentity()); + assertArrayEquals(payload.getParentStateRoot(), decoded.getParentStateRoot()); + assertArrayEquals(payload.getStateRoot(), decoded.getStateRoot()); + assertEquals(3, decoded.getBlocks().size()); + assertEquals(2, decoded.getChainbaseStores().size()); + assertEquals(2, decoded.getPathStores().size()); + assertArrayEquals(target.getBlocks().get(1).getMutationViewDigest(), + decoded.getBlocks().get(1).getArchiveDiff().getMutationViewDigest()); + assertArrayEquals(codec.digest(payload), codec.digest(decoded)); + byte[] corrupt = Arrays.copyOf(encoded, encoded.length); + corrupt[corrupt.length - 1] ^= 1; + assertThrows(IllegalArgumentException.class, () -> codec.decode(corrupt)); + assertThrows(IllegalArgumentException.class, + () -> codec.decode(Arrays.copyOf(encoded, encoded.length - 1))); + byte[] wrongVersion = Arrays.copyOf(encoded, encoded.length); + wrongVersion[5]++; + assertThrows(IllegalArgumentException.class, () -> codec.decode(wrongVersion)); + assertThrows(IllegalArgumentException.class, + () -> new CommonCheckpointPayloadCodec(encoded.length - 1).decode(encoded)); + java.util.List mismatchedArchive = new ArrayList<>(archiveBlocks); + PathStateFlushTarget.BlockBinding firstBlock = target.getBlocks().get(0); + mismatchedArchive.set(0, new BlockReverseDiff(firstBlock.getMeta(), + Collections.emptyList(), bytes(88))); + assertThrows(IllegalArgumentException.class, () -> CommonCheckpointPayload.create( + bytes(77), target, mismatchedArchive, Collections.emptyList())); + + assertThrows(IllegalArgumentException.class, + () -> PathStateFlushTarget.coalesce(Collections.emptyList())); + PathStateSnapshotDelta wrongRoot = mock(PathStateSnapshotDelta.class); + when(wrongRoot.getMeta()).thenReturn(third.getMeta()); + when(wrongRoot.getParentStateRoot()).thenReturn(bytes(99)); + assertThrows(IllegalArgumentException.class, + () -> PathStateFlushTarget.coalesce(Arrays.asList(second, wrongRoot))); + } + @Test public void rewindsOwnedHeadThenBuildsCanonicalSibling() throws Exception { for (Engine engine : availableEngines()) { From bef5e53cda3090f0d5426183cfbf9f4486f44eb1 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 4 Sep 2026 18:17:56 +0800 Subject: [PATCH 110/161] perf(trie): reduce path state block overhead Defer Account and AccountAsset node encoding until the final block-scoped graph is frozen, while retaining strict canonical RLP validation and resolved subtree reuse across snapshots. Avoid synchronous PathState owner reads when header diagnostics cannot observe an exact ready identity. --- .../core/db2/stateroot/PathMerkleTrie.java | 174 +++++++++++++++--- .../PathStatePhysicalOverlayHead.java | 15 +- .../core/db2/stateroot/PathStateRoot.java | 13 +- .../stateroot/PathStateRuntimeAttachment.java | 10 + .../main/java/org/tron/core/db/Manager.java | 19 +- .../SnapshotOldValueCollectorTest.java | 7 + .../db2/stateroot/PathMerkleTrieTest.java | 114 ++++++++++++ .../core/db2/stateroot/PathStateRootTest.java | 9 + 8 files changed, 329 insertions(+), 32 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index 638f6f78974..da8dd8f9346 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -38,7 +38,10 @@ public final class PathMerkleTrie { private final PathNodeStore nodeStore; private static final AtomicLong NODE_CREATE_COUNT = new AtomicLong(); + private static final AtomicLong NODE_ENCODE_COUNT = new AtomicLong(); private static final AtomicLong NODE_KECCAK_COUNT = new AtomicLong(); + private static final ThreadLocal DEFER_NODE_ENCODING = + ThreadLocal.withInitial(() -> Boolean.FALSE); private final boolean lazyHashReferences; private final Map leaves = new TreeMap<>(UNSIGNED_KEY_COMPARATOR); @@ -98,7 +101,22 @@ public synchronized void delete(byte[] secureKey) { } synchronized void applyBatch(List mutations, ExecutorService executor) { + applyBatch(mutations, executor, false); + } + + synchronized void applyBatch(List mutations, ExecutorService executor, + boolean deferNodeEncoding) { requireMutable(); + if (deferNodeEncoding) { + try (BlockNodeEncodingScope ignored = BlockNodeEncodingScope.open()) { + applyBatchInternal(mutations, executor); + } + return; + } + applyBatchInternal(mutations, executor); + } + + private void applyBatchInternal(List mutations, ExecutorService executor) { List batch = new ArrayList<>(Objects.requireNonNull(mutations, "mutations")); Node resolvedRoot = resolve(rootNode, EMPTY_PATH); rootNode = resolvedRoot; @@ -139,10 +157,11 @@ synchronized void applyBatch(List mutations, ExecutorService exec return compared != 0 ? compared : Integer.compare(left.position, right.position); }); List> futures = new ArrayList<>(work.size()); + boolean deferNodeEncoding = DEFER_NODE_ENCODING.get(); for (SubtreeWork subtree : work) { futures.add(Objects.requireNonNull(executor, "executor").submit( () -> applySubtree(root.children[subtree.position], subtree.position, - subtree.mutations))); + subtree.mutations, deferNodeEncoding))); } Node[] children = Arrays.copyOf(root.children, root.children.length); Map previous = new LinkedHashMap<>(); @@ -218,6 +237,20 @@ private void applySequentialBatch(List mutations) { } private SubtreeResult applySubtree(Node initial, int nibble, + List mutations, boolean deferNodeEncoding) { + if (deferNodeEncoding) { + try (BlockNodeEncodingScope ignored = BlockNodeEncodingScope.open()) { + SubtreeResult result = applySubtreeInternal(initial, nibble, mutations); + if (result.node != null) { + nodeReference(result.node); + } + return result; + } + } + return applySubtreeInternal(initial, nibble, mutations); + } + + private SubtreeResult applySubtreeInternal(Node initial, int nibble, List mutations) { Node node = initial; byte[] path = new byte[]{(byte) nibble}; @@ -440,6 +473,11 @@ static long nodeCreateCountTotal() { return NODE_CREATE_COUNT.get(); } + /** Cumulative count of canonical RLP encodes, excluding already encoded durable nodes. */ + static long nodeEncodeCountTotal() { + return NODE_ENCODE_COUNT.get(); + } + /** Cumulative count of actual Keccak computations on node RLP (cache misses). */ static long nodeKeccakCountTotal() { return NODE_KECCAK_COUNT.get(); @@ -1081,9 +1119,7 @@ private Node resolve(Node node, byte[] expectedPath) { } else { throw new IllegalStateException("path-state durable node has invalid arity"); } - if (!Arrays.equals(encodedNode(decoded), storedEncoding)) { - throw new IllegalStateException("path-state durable node is not canonically encoded"); - } + decoded.bindEncoded(storedEncoding); if (stored.knownHash != null) { decoded.bindCachedHash(stored.knownHash); } @@ -1103,6 +1139,9 @@ private Node storedChild(RlpElement reference, byte[] path) { return null; } if (reference.list) { + if (reference.encoded.length >= SECURE_KEY_LENGTH) { + throw new IllegalStateException("path-state inline child is not canonically encoded"); + } Node stored = new StoredNode(reference.encoded, path); rememberMaterialized(stored, path); return stored; @@ -1155,17 +1194,27 @@ private static RlpElement decodeElement(byte[] encoded, int offset) { if (marker <= longBase) { payloadOffset = offset + 1; payloadLength = marker - shortBase; + if (!list && payloadLength == 1 && payloadOffset < encoded.length + && (encoded[payloadOffset] & 0xff) < 0x80) { + throw new IllegalStateException("path-state RLP item uses a non-canonical short form"); + } } else { int lengthBytes = marker - longBase; if (lengthBytes > Integer.BYTES || offset + 1 + lengthBytes > encoded.length) { throw new IllegalStateException("path-state RLP length is invalid"); } + if (encoded[offset + 1] == 0) { + throw new IllegalStateException("path-state RLP length has leading zeroes"); + } payloadOffset = offset + 1 + lengthBytes; payloadLength = 0; for (int index = offset + 1; index < payloadOffset; index++) { payloadLength = Math.addExact(Math.multiplyExact(payloadLength, 256), encoded[index] & 0xff); } + if (payloadLength < 56) { + throw new IllegalStateException("path-state RLP uses a non-canonical long form"); + } } int end = Math.addExact(payloadOffset, payloadLength); if (end > encoded.length) { @@ -1199,11 +1248,7 @@ private static Compact decodeCompact(byte[] encoded) { } private static byte[] encodedNode(Node node) { - byte[] encoded = Objects.requireNonNull(node, "node").encoded; - if (encoded == null) { - throw new IllegalStateException("unresolved path-state hash reference has no node encoding"); - } - return encoded; + return Objects.requireNonNull(node, "node").encoded(); } private static byte[] nodeHash(Node node) { @@ -1314,7 +1359,7 @@ private static byte[] concatenate(byte[] first, byte[] second) { private abstract static class Node { - private final byte[] encoded; + private volatile byte[] encoded; private volatile byte[] hash; private volatile BytesKey materializedPath; @@ -1322,6 +1367,45 @@ private Node(byte[] encoded) { this.encoded = encoded; } + private byte[] encoded() { + byte[] cached = encoded; + if (cached == null) { + synchronized (this) { + cached = encoded; + if (cached == null) { + cached = encodeNode(); + if (cached == null) { + throw new IllegalStateException( + "unresolved path-state hash reference has no node encoding"); + } + encoded = cached; + NODE_ENCODE_COUNT.incrementAndGet(); + } + } + } + return cached; + } + + protected final void encodeEagerlyUnlessDeferred() { + if (!DEFER_NODE_ENCODING.get()) { + encoded(); + } + } + + private void bindEncoded(byte[] knownEncoding) { + byte[] known = Arrays.copyOf(Objects.requireNonNull(knownEncoding, "knownEncoding"), + knownEncoding.length); + synchronized (this) { + if (encoded == null) { + encoded = known; + } else if (!Arrays.equals(encoded, known)) { + throw new IllegalStateException("path-state durable node is not canonically encoded"); + } + } + } + + protected abstract byte[] encodeNode(); + private byte[] cachedHash() { byte[] cached = hash; if (cached == null) { @@ -1392,6 +1476,11 @@ private StoredNode(byte[] encoded, byte[] path, byte[] knownHash) { throw new IllegalArgumentException("knownHash must contain exactly 32 bytes"); } } + + @Override + protected byte[] encodeNode() { + throw new IllegalStateException("stored path-state node lost its encoding"); + } } private static final class HashRefNode extends Node { @@ -1408,6 +1497,11 @@ private HashRefNode(byte[] path, byte[] expectedHash) { throw new IllegalArgumentException("expectedHash must contain exactly 32 bytes"); } } + + @Override + protected byte[] encodeNode() { + return null; + } } private static final class LeafNode extends Node { @@ -1416,10 +1510,16 @@ private static final class LeafNode extends Node { private final byte[] value; private LeafNode(byte[] path, byte[] value) { - super(rlpList(rlpItem(compactPath(path, true)), rlpItem(value))); - NODE_CREATE_COUNT.incrementAndGet(); + super(null); this.path = Arrays.copyOf(path, path.length); this.value = Arrays.copyOf(value, value.length); + NODE_CREATE_COUNT.incrementAndGet(); + encodeEagerlyUnlessDeferred(); + } + + @Override + protected byte[] encodeNode() { + return rlpList(rlpItem(compactPath(path, true)), rlpItem(value)); } } @@ -1430,18 +1530,19 @@ private static final class ExtensionNode extends Node { private final Node child; private ExtensionNode(byte[] path, Node child) { - super(encode(path, child)); - NODE_CREATE_COUNT.incrementAndGet(); + super(null); if (path.length == 0) { throw new IllegalArgumentException("extension path must not be empty"); } this.path = Arrays.copyOf(path, path.length); this.child = Objects.requireNonNull(child, "child"); + NODE_CREATE_COUNT.incrementAndGet(); + encodeEagerlyUnlessDeferred(); } - private static byte[] encode(byte[] path, Node child) { - Node present = Objects.requireNonNull(child, "child"); - return rlpList(rlpItem(compactPath(path, false)), nodeReference(present)); + @Override + protected byte[] encodeNode() { + return rlpList(rlpItem(compactPath(path, false)), nodeReference(child)); } } @@ -1450,15 +1551,17 @@ private static final class BranchNode extends Node { private final Node[] children; private BranchNode(Node[] children) { - super(encode(children)); - NODE_CREATE_COUNT.incrementAndGet(); - this.children = Arrays.copyOf(children, children.length); - } - - private static byte[] encode(Node[] children) { + super(null); if (children.length != 16) { throw new IllegalArgumentException("branch must contain 16 child slots"); } + this.children = Arrays.copyOf(children, children.length); + NODE_CREATE_COUNT.incrementAndGet(); + encodeEagerlyUnlessDeferred(); + } + + @Override + protected byte[] encodeNode() { List encodedChildren = new ArrayList<>(Collections.nCopies(17, EMPTY_RLP_ITEM)); for (int i = 0; i < children.length; i++) { if (children[i] != null) { @@ -1469,6 +1572,31 @@ private static byte[] encode(Node[] children) { } } + /** Defers node encoding while one participant folds a block's sorted mutation batch. */ + private static final class BlockNodeEncodingScope implements AutoCloseable { + + private final boolean previous; + + private BlockNodeEncodingScope(boolean previous) { + this.previous = previous; + } + + private static BlockNodeEncodingScope open() { + boolean previous = DEFER_NODE_ENCODING.get(); + DEFER_NODE_ENCODING.set(Boolean.TRUE); + return new BlockNodeEncodingScope(previous); + } + + @Override + public void close() { + if (previous) { + DEFER_NODE_ENCODING.set(Boolean.TRUE); + } else { + DEFER_NODE_ENCODING.remove(); + } + } + } + private static final class Leaf { private final byte[] nibbles; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java index 068cea3ff49..62b39688064 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java @@ -121,7 +121,8 @@ public synchronized PathStateRootMetadata advance(PathStateBlockTransition trans + "participantWallMs={}, prepareMs={}, trieMs={}, artifactMs={}, " + "nodePlanWorkMs={}, nodeStoreWorkMs={}, nodeFinalizeWorkMs={}, " + "nodePuts={}, nodeDeletes={}, nodeRlpBytes={}, nodeRlpFinalBytes={}, " - + "uniqueNodePaths={}, overwriteWrites={}, nodeCreates={}, nodeKeccaks={}, " + + "uniqueNodePaths={}, overwriteWrites={}, nodeCreates={}, nodeEncodes={}, " + + "avoidedNodeEncodes={}, unencodedCreatesAfterTopLevelDecodes={}, nodeKeccaks={}, " + "nodeDecodes={}, nodeHashVerifies={}, hashRefsCreated={}, hashRefsResolved={}, " + "durableWrites=0, journal=0", head.getBlockNumber(), admitted.getMutations().size(), pending.nodeMutations, @@ -136,7 +137,10 @@ public synchronized PathStateRootMetadata advance(PathStateBlockTransition trans pending.nodeFinalizeWorkMillis, pending.stats.nodePuts, pending.stats.nodeDeletes, pending.stats.nodeRlpBytes, pending.stats.nodeRlpFinalBytes, pending.stats.uniqueNodePaths, - pending.stats.overwriteWrites(), pending.nodeCreates, pending.nodeKeccaks, + pending.stats.overwriteWrites(), pending.nodeCreates, pending.nodeEncodes, + Math.max(0L, pending.nodeCreates - pending.nodeEncodes), + Math.max(0L, pending.nodeCreates - pending.nodeEncodes - pending.nodeDecodes), + pending.nodeKeccaks, pending.nodeDecodes, pending.nodeHashVerifies, pending.hashRefsCreated, pending.hashRefsResolved); logger.info("Path-state artifact stores: head={}, perStore={}", @@ -225,6 +229,7 @@ private PreparedOverlay prepare(BlockSnapshotMeta meta, PathStateBlockTransition requireChild(transition); long startedNanos = System.nanoTime(); long nodeCreatesBefore = PathMerkleTrie.nodeCreateCountTotal(); + long nodeEncodesBefore = PathMerkleTrie.nodeEncodeCountTotal(); long nodeKeccaksBefore = PathMerkleTrie.nodeKeccakCountTotal(); Map recordings = new LinkedHashMap<>(); PathStateRoot candidate = PathStateRoot.fromSnapshot(scope, @@ -280,6 +285,7 @@ private PreparedOverlay prepare(BlockSnapshotMeta meta, PathStateBlockTransition TimeUnit.NANOSECONDS.toMillis(candidate.nodeCommitStoreNanos()), TimeUnit.NANOSECONDS.toMillis(candidate.nodeCommitFinalizeNanos()), PathMerkleTrie.nodeCreateCountTotal() - nodeCreatesBefore, + PathMerkleTrie.nodeEncodeCountTotal() - nodeEncodesBefore, PathMerkleTrie.nodeKeccakCountTotal() - nodeKeccaksBefore, candidate.nodeDecodeCount(), candidate.nodeHashVerifyCount(), candidate.hashReferenceCreateCount(), candidate.hashReferenceResolveCount(), stats); @@ -358,6 +364,7 @@ private static final class PreparedOverlay { private final long nodeStoreWorkMillis; private final long nodeFinalizeWorkMillis; private final long nodeCreates; + private final long nodeEncodes; private final long nodeKeccaks; private final long nodeDecodes; private final long nodeHashVerifies; @@ -370,7 +377,8 @@ private PreparedOverlay(PathStateBlockTransition transition, PathStateRootMetada long nativeNodeReads, PathStateRoot.ParallelApplyStats parallelStats, long prepareMillis, long trieMillis, long artifactMillis, long nodePlanWorkMillis, long nodeStoreWorkMillis, long nodeFinalizeWorkMillis, long nodeCreates, - long nodeKeccaks, long nodeDecodes, long nodeHashVerifies, long hashRefsCreated, + long nodeEncodes, long nodeKeccaks, long nodeDecodes, long nodeHashVerifies, + long hashRefsCreated, long hashRefsResolved, RecordingStats stats) { this.transition = transition; this.metadata = metadata; @@ -395,6 +403,7 @@ private PreparedOverlay(PathStateBlockTransition transition, PathStateRootMetada this.nodeStoreWorkMillis = nodeStoreWorkMillis; this.nodeFinalizeWorkMillis = nodeFinalizeWorkMillis; this.nodeCreates = nodeCreates; + this.nodeEncodes = nodeEncodes; this.nodeKeccaks = nodeKeccaks; this.nodeDecodes = nodeDecodes; this.nodeHashVerifies = nodeHashVerifies; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index 988bbe229d3..9c2c9b2833f 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -132,8 +132,12 @@ synchronized ParallelApplyStats applyParallel(Collection muta batch.add(new PathMerkleTrie.BatchMutation(mutation.secureKey, mutation.encodedValue, mutation.previousValueKnown, mutation.previousEncodedValue)); } - participantTries.get(participantWork.participant.getDbName()).applyBatch(batch, - Objects.requireNonNull(branchExecutor, "branchExecutor")); + PathMerkleTrie participantTrie = participantTries.get( + participantWork.participant.getDbName()); + participantTrie.applyBatch(batch, + Objects.requireNonNull(branchExecutor, "branchExecutor"), + usesDeferredNodeEncoding(participantWork.participant)); + participantTrie.rootHash(); participantWork.elapsedNanos = System.nanoTime() - participantStartedNanos; })); } @@ -258,6 +262,11 @@ private ParticipantWork(List mutations) { } } + static boolean usesDeferredNodeEncoding(PathStateParticipant participant) { + String dbName = participant.getDbName(); + return "account".equals(dbName) || "account-asset".equals(dbName); + } + /** Applies one rebuild batch while locking only the participant tries touched by that batch. */ void applyRebuild(Collection mutations) { List prepared = prepare(mutations); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java index e30ac282f6a..aceae6cd33d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java @@ -274,6 +274,16 @@ observedBlockHash, failureStage, classify(failure), failure, headerDiagnostic, headerDiagnosticBlockNumber, headerDiagnosticBlockHash); } + /** True only when an owner root read cannot wait for a deferred predecessor transition. */ + public synchronized boolean isReadyForHeaderDiagnostic(long blockNumber, byte[] blockHash) { + byte[] admittedHash = Objects.requireNonNull(blockHash, "blockHash"); + return failure == null && pending == null && pendingView == null + && readyBlockNumber == observedBlockNumber + && readyBlockNumber == blockNumber + && Arrays.equals(readyBlockHash, observedBlockHash) + && Arrays.equals(readyBlockHash, admittedHash); + } + /** Records a non-blocking comparison of carried header metadata against the local READY root. */ public synchronized void diagnoseHeader(long blockNumber, byte[] blockHash, byte[] carriedRoot, byte[] localRoot) { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index ea91d47daec..57bca92066c 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1601,21 +1601,32 @@ private void diagnosePathStateHeader(BlockCapsule block) { if (runtime == null) { return; } + byte[] blockHash = block.getBlockId().getBytes(); + byte[] carriedRoot = block.getStateRoot(); + boolean carriedRootUsable = carriedRoot != null && carriedRoot.length == 32; + boolean ready = runtime.isReadyForHeaderDiagnostic(block.getNum(), blockHash); byte[] localRoot = null; + boolean ownerRead = false; + long ownerReadStartedNanos = System.nanoTime(); try { PathStateHead owner = pathStateSnapshotHead; - if (owner != null) { + if (carriedRootUsable && ready && owner != null) { + ownerRead = true; PathStateRootMetadata local = owner.getHead(); if (local.getBlockNumber() == block.getNum() - && Arrays.equals(local.getBlockHash(), block.getBlockId().getBytes())) { + && Arrays.equals(local.getBlockHash(), blockHash)) { localRoot = local.getStateRoot(); } } } catch (java.io.IOException | RuntimeException diagnosticFailure) { logger.warn("Path-state local root unavailable for header diagnostic", diagnosticFailure); } - runtime.diagnoseHeader(block.getNum(), block.getBlockId().getBytes(), block.getStateRoot(), - localRoot); + long ownerReadMicros = TimeUnit.NANOSECONDS.toMicros( + System.nanoTime() - ownerReadStartedNanos); + runtime.diagnoseHeader(block.getNum(), blockHash, carriedRoot, localRoot); + logger.info("Path-state header diagnostic access: head={}, ownerRead={}, " + + "skippedNotReady={}, carriedRootUsable={}, ownerReadMicros={}", + block.getNum(), ownerRead, !ready, carriedRootUsable, ownerReadMicros); } private void switchFork(BlockCapsule newHead) diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index e384f38d338..28fbb1886c1 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -113,10 +113,12 @@ public void deferredPathStateCaptureDoesNotWaitForCollectorAndDrainsOnClose() } assertTrue(collecting.await(5, TimeUnit.SECONDS)); assertNull(published.get()); + assertFalse(attachment.isReadyForHeaderDiagnostic(1L, hash(1))); release.countDown(); attachment.close(); assertEquals(1, published.get().getBlockNumber()); assertEquals(PathStateRuntimeAttachment.State.READY, attachment.status().getState()); + assertTrue(attachment.isReadyForHeaderDiagnostic(1L, hash(1))); assertSame(attachment, manager.detachPathStateRuntime(attachment)); manager.shutdown(); } @@ -266,6 +268,10 @@ public void disabledAndShadowCapturePreserveCanonicalBlockAndStateOutcome() thro owner::advance, (blockNumber, blockHash) -> { }, transition -> owner.prepare(transition).getStateRoot()); runtime.synchronizeReadyHead(base); + assertTrue(runtime.isReadyForHeaderDiagnostic(base.getBlockNumber(), base.getBlockHash())); + assertFalse(runtime.isReadyForHeaderDiagnostic(base.getBlockNumber() + 1, + base.getBlockHash())); + assertFalse(runtime.isReadyForHeaderDiagnostic(base.getBlockNumber(), hash(100))); shadow.attachPathStateRuntime(runtime); BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1L, canonicalBlockId, parentHash, 12L); @@ -288,6 +294,7 @@ public void disabledAndShadowCapturePreserveCanonicalBlockAndStateOutcome() thro assertEquals(((SnapshotImpl) controlCode.getHead()).getBlockSnapshotMeta(), ((SnapshotImpl) shadowCode.getHead()).getBlockSnapshotMeta()); assertEquals(PathStateRuntimeAttachment.State.READY, runtime.status().getState()); + assertTrue(runtime.isReadyForHeaderDiagnostic(1L, canonicalBlockId)); assertEquals(1L, owner.getHead().getBlockNumber()); assertFalse(Arrays.equals(base.getStateRoot(), owner.getHead().getStateRoot())); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java index bd75167cc3e..57cd57b5089 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java @@ -103,6 +103,22 @@ public void rejectsInvalidKeysAndEmptyValues() { assertThrows(NullPointerException.class, () -> trie.delete(null)); } + @Test + public void rejectsNonCanonicalRlpWithoutDecodeReencoding() { + InMemoryPathNodeStore store = new InMemoryPathNodeStore(); + byte[] nonCanonicalLeaf = new byte[37]; + nonCanonicalLeaf[0] = (byte) 0xe4; + nonCanonicalLeaf[1] = (byte) 0xa1; + nonCanonicalLeaf[2] = 0x20; + nonCanonicalLeaf[35] = (byte) 0x81; + nonCanonicalLeaf[36] = 0x01; + store.nodes.put("", nonCanonicalLeaf); + + PathMerkleTrie restored = new PathMerkleTrie(store); + restored.restoreRoot(); + assertThrows(IllegalStateException.class, () -> restored.get(new byte[32])); + } + @Test public void detectsMissingCorruptAndDirtyCommittedNodes() { InMemoryPathNodeStore corruptStore = new InMemoryPathNodeStore(); @@ -333,6 +349,104 @@ public void prefixBatchMatchesSequentialMixedChangesFromLazyRoot() { } } + @Test + public void blockScopedEncodingEncodesOnlyTheFrozenNodeGraph() { + int leafCount = 512; + byte[][] keys = new byte[leafCount][]; + InMemoryPathNodeStore sourceStore = new InMemoryPathNodeStore(); + PathMerkleTrie source = new PathMerkleTrie(sourceStore); + for (int index = 0; index < leafCount; index++) { + keys[index] = Hash.sha3(value("arena-key-" + index)); + source.put(keys[index], value("arena-value-" + index)); + } + byte[] initialRoot = source.rootHash(); + Map initialNodes = copyNodeMap(sourceStore.nodes); + List changes = new ArrayList<>(); + for (int index = 0; index < 96; index++) { + changes.add(new PathMerkleTrie.BatchMutation(keys[index], + value("arena-updated-" + index))); + } + for (int index = 96; index < 128; index++) { + changes.add(new PathMerkleTrie.BatchMutation(keys[index], null)); + } + + InMemoryPathNodeStore eagerStore = new InMemoryPathNodeStore(); + eagerStore.nodes.putAll(copyNodeMap(initialNodes)); + PathMerkleTrie eager = new PathMerkleTrie(eagerStore); + eager.restoreRoot(initialRoot); + ExecutorService eagerExecutor = Executors.newFixedThreadPool(4); + long eagerEncodesBefore = PathMerkleTrie.nodeEncodeCountTotal(); + long eagerEncodes; + try { + eager.applyBatch(changes, eagerExecutor, false); + eager.rootHash(); + eagerEncodes = PathMerkleTrie.nodeEncodeCountTotal() - eagerEncodesBefore; + } finally { + eagerExecutor.shutdownNow(); + } + + InMemoryPathNodeStore arenaStore = new InMemoryPathNodeStore(); + arenaStore.nodes.putAll(copyNodeMap(initialNodes)); + PathMerkleTrie arena = new PathMerkleTrie(arenaStore); + arena.restoreRoot(initialRoot); + ExecutorService arenaExecutor = Executors.newFixedThreadPool(4); + long arenaCreatesBefore = PathMerkleTrie.nodeCreateCountTotal(); + long arenaEncodesBefore = PathMerkleTrie.nodeEncodeCountTotal(); + long arenaCreates; + long arenaEncodes; + try { + arena.applyBatch(changes, arenaExecutor, true); + assertArrayEquals(eager.rootHash(), arena.rootHash()); + arenaCreates = PathMerkleTrie.nodeCreateCountTotal() - arenaCreatesBefore; + arenaEncodes = PathMerkleTrie.nodeEncodeCountTotal() - arenaEncodesBefore; + long encodesAfterFreeze = PathMerkleTrie.nodeEncodeCountTotal(); + arena.rootHash(); + arena.snapshot(); + assertEquals(encodesAfterFreeze, PathMerkleTrie.nodeEncodeCountTotal()); + } finally { + arenaExecutor.shutdownNow(); + } + + assertNodeMapsEqual(eagerStore.nodes, arenaStore.nodes); + assertTrue(arenaCreates > arenaEncodes); + assertTrue(arenaEncodes < eagerEncodes); + assertTrue(arenaEncodes >= arena.getLastNodePuts()); + } + + @Test + public void resolvedSubtreeSurvivesConsecutiveSnapshotSwitches() { + byte[][] keys = new byte[16][]; + byte[][] values = new byte[16][]; + InMemoryPathNodeStore store = new InMemoryPathNodeStore(); + PathMerkleTrie source = new PathMerkleTrie(store); + for (int index = 0; index < keys.length; index++) { + keys[index] = filledKey(index << 4); + values[index] = value("snapshot-value-long-enough-" + index); + source.put(keys[index], values[index]); + } + byte[] initialRoot = source.rootHash(); + + PathMerkleTrie restored = new PathMerkleTrie(store); + restored.restoreRoot(initialRoot); + assertArrayEquals(values[3], restored.get(keys[3])); + PathMerkleTrie.Snapshot parent = restored.snapshot(); + + PathMerkleTrie firstBlock = PathMerkleTrie.fromSnapshot(store, parent); + values[4] = value("snapshot-block-one-updated"); + firstBlock.put(keys[4], values[4]); + firstBlock.rootHash(); + PathMerkleTrie.Snapshot firstChild = firstBlock.snapshot(); + + int readsBeforeInheritedLookup = store.gets; + PathMerkleTrie secondBlock = PathMerkleTrie.fromSnapshot(store, firstChild); + assertArrayEquals(values[3], secondBlock.get(keys[3])); + assertEquals(readsBeforeInheritedLookup, store.gets); + + values[5] = value("snapshot-block-two-updated"); + secondBlock.put(keys[5], values[5]); + assertArrayEquals(referenceRoot(keys, values), secondBlock.rootHash()); + } + private static byte[] referenceRoot(byte[][] keys, byte[][] values) { TrieImpl reference = new TrieImpl(); reference.setAsync(false); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java index b235815ae45..8d3cc7cf7c3 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -249,6 +250,14 @@ public void gethStyleParticipantAndRootBranchBatchMatchesSequentialRoot() { parallel.pendingLeafMutations().size()); } + @Test + public void deferredNodeEncodingIsInitiallyLimitedToAccountParticipants() { + assertTrue(PathStateRoot.usesDeferredNodeEncoding(participant(4, "account"))); + assertTrue(PathStateRoot.usesDeferredNodeEncoding(participant(5, "account-asset"))); + assertFalse(PathStateRoot.usesDeferredNodeEncoding(participant(1, "abi"))); + assertFalse(PathStateRoot.usesDeferredNodeEncoding(participant(22, "storage-row"))); + } + @Test public void restoredTrieAttachesDecodedNodesForRepeatedReads() { CountingPathNodeStore store = new CountingPathNodeStore(); From c2f3029697ae4590c9218097ddaf48b41f71eec9 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 5 Sep 2026 13:03:26 +0800 Subject: [PATCH 111/161] fix(chainbase): honor archive index engine --- .../StateArchiveCheckpointServingIndex.java | 126 +++---- .../archive/StateArchiveIndexDatabase.java | 320 ++++++++++++++++++ .../StateArchiveIndexEngineManifest.java | 122 +++++++ ...tateArchiveCheckpointMaterializerTest.java | 23 +- .../StateArchiveIndexDatabaseTest.java | 53 +++ 5 files changed, 579 insertions(+), 65 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexEngineManifest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java index 3e037623062..001a7d04b6e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java @@ -11,20 +11,18 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.OptionalLong; -import org.rocksdb.Options; -import org.rocksdb.ReadOptions; -import org.rocksdb.RocksDB; -import org.rocksdb.RocksDBException; -import org.rocksdb.RocksIterator; -import org.rocksdb.WriteBatch; -import org.rocksdb.WriteOptions; +import org.tron.common.parameter.CommonParameter; import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; import org.tron.core.db2.archive.BlockReverseDiff.Entry; import org.tron.core.db2.core.CommonCheckpointPayload; import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** Persistent exact-key locator for next-format per-block checkpoint history files. */ final class StateArchiveCheckpointServingIndex { @@ -42,21 +40,23 @@ final class StateArchiveCheckpointServingIndex { private static final int LOCATION_LENGTH = DIGEST_LENGTH + Integer.BYTES + Long.BYTES + DIGEST_LENGTH; - static { - RocksDB.loadLibrary(); - } - private StateArchiveCheckpointServingIndex() { } static Status inspect(Path archiveDirectory, CommonCheckpointTarget target) throws IOException { + return inspect(archiveDirectory, target, configuredEngine()); + } + + static Status inspect(Path archiveDirectory, CommonCheckpointTarget target, Engine engine) + throws IOException { Path databasePath = databasePath(archiveDirectory); if (!Files.exists(databasePath, LinkOption.NOFOLLOW_LINKS)) { return Status.ABSENT; } - try (Options options = new Options().setCreateIfMissing(false); - RocksDB database = RocksDB.openReadOnly(options, databasePath.toString())) { + StateArchiveIndexEngineManifest.require(archiveDirectory.resolve(DIRECTORY), engine); + try (StateArchiveIndexDatabase.Reader database = + StateArchiveIndexDatabase.openReader(databasePath, engine)) { byte[] encoded = database.get(MARKER_KEY); if (encoded == null) { throw new IOException("State Archive checkpoint serving marker is missing"); @@ -68,14 +68,17 @@ static Status inspect(Path archiveDirectory, CommonCheckpointTarget target) } requireParent(marker, target); return Status.PARENT; - } catch (RocksDBException failure) { - throw new IOException("Failed to inspect State Archive checkpoint serving index", failure); } } static void apply(Path archiveDirectory, CommonCheckpointPayload payload, CommonCheckpointTarget target) throws IOException { - Status status = inspect(archiveDirectory, target); + apply(archiveDirectory, payload, target, configuredEngine()); + } + + static void apply(Path archiveDirectory, CommonCheckpointPayload payload, + CommonCheckpointTarget target, Engine engine) throws IOException { + Status status = inspect(archiveDirectory, target, engine); if (status == Status.EXACT) { return; } @@ -84,13 +87,12 @@ static void apply(Path archiveDirectory, CommonCheckpointPayload payload, if (!Files.isDirectory(indexDirectory, LinkOption.NOFOLLOW_LINKS)) { throw new IOException("State Archive checkpoint serving path is not a directory"); } + StateArchiveIndexEngineManifest.openOrCreate(indexDirectory, engine); long baseBlockNumber = target.getFirstBlock().getBlockNumber() - 1; byte[] baseBlockHash = target.getFirstBlock().getParentHash(); Path databasePath = databasePath(archiveDirectory); - try (Options options = new Options().setCreateIfMissing(true); - RocksDB database = RocksDB.open(options, databasePath.toString()); - WriteBatch batch = new WriteBatch(); - WriteOptions writes = new WriteOptions().setSync(true)) { + try (StateArchiveIndexDatabase.Writer database = + StateArchiveIndexDatabase.openWriter(databasePath, engine)) { byte[] existing = database.get(MARKER_KEY); if (existing != null) { Marker parent = decodeMarker(existing); @@ -98,30 +100,43 @@ static void apply(Path archiveDirectory, CommonCheckpointPayload payload, baseBlockNumber = parent.baseBlockNumber; baseBlockHash = parent.baseBlockHash; } + List mutations = new ArrayList<>(); for (int index = 0; index < payload.getBlocks().size(); index++) { CommonCheckpointPayload.BlockPayload block = payload.getBlocks().get(index); long blockNumber = block.getMeta().getBlockNumber(); for (DbGroup group : block.getArchiveDiff().getGroups()) { requireStateDatabase(group.getDbName()); for (Entry entry : group.getEntries()) { - batch.put(changeKey(group.getDbName(), entry.getKey(), blockNumber), new byte[]{1}); + mutations.add(StateArchiveIndexDatabase.put( + changeKey(group.getDbName(), entry.getKey(), blockNumber), new byte[]{1})); } } - batch.put(blockKey(blockNumber), encodeLocation(target.getPayloadDigest(), index, - block.getMeta())); + mutations.add(StateArchiveIndexDatabase.put(blockKey(blockNumber), + encodeLocation(target.getPayloadDigest(), index, block.getMeta()))); } - batch.put(MARKER_KEY, encodeMarker(target, baseBlockNumber, baseBlockHash)); - database.write(writes, batch); - } catch (RocksDBException failure) { - throw new IOException("Failed to materialize State Archive checkpoint serving index", - failure); + mutations.add(StateArchiveIndexDatabase.put(MARKER_KEY, + encodeMarker(target, baseBlockNumber, baseBlockHash))); + database.write(mutations); } HistorySegmentStore.syncDirectory(indexDirectory); } static Reader openReader(Path archiveDirectory, CommonCheckpointTarget target) throws IOException { - return new Reader(archiveDirectory, target); + return openReader(archiveDirectory, target, configuredEngine()); + } + + static Reader openReader(Path archiveDirectory, CommonCheckpointTarget target, Engine engine) + throws IOException { + return new Reader(archiveDirectory, target, engine); + } + + static Engine configuredEngine() { + org.tron.core.config.args.Storage storage = CommonParameter.getInstance().getStorage(); + if (storage == null || storage.getDbEngine() == null) { + return Engine.LEVELDB; + } + return Engine.valueOf(storage.getDbEngine().toUpperCase(Locale.ROOT)); } private static void requireParent(Marker marker, CommonCheckpointTarget target) @@ -277,20 +292,19 @@ enum Status { static final class Reader implements AutoCloseable { private final Path archiveDirectory; - private final Options options; - private final RocksDB database; + private final StateArchiveIndexDatabase.Reader database; private final Marker marker; private boolean closed; - private Reader(Path archiveDirectory, CommonCheckpointTarget target) throws IOException { + private Reader(Path archiveDirectory, CommonCheckpointTarget target, Engine engine) + throws IOException { this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); Objects.requireNonNull(target, "target"); - this.options = new Options().setCreateIfMissing(false); - RocksDB opened; + StateArchiveIndexEngineManifest.require(archiveDirectory.resolve(DIRECTORY), engine); + StateArchiveIndexDatabase.Reader opened; try { - opened = RocksDB.openReadOnly(options, databasePath(archiveDirectory).toString()); - } catch (RocksDBException | RuntimeException failure) { - options.close(); + opened = StateArchiveIndexDatabase.openReader(databasePath(archiveDirectory), engine); + } catch (IOException | RuntimeException failure) { throw new IOException("Failed to open State Archive checkpoint serving reader", failure); } this.database = opened; @@ -302,9 +316,8 @@ private Reader(Path archiveDirectory, CommonCheckpointTarget target) throws IOEx loaded.baseBlockHash))) { throw new IOException("State Archive checkpoint reader target differs"); } - } catch (IOException | RocksDBException | RuntimeException failure) { + } catch (IOException | RuntimeException failure) { opened.close(); - options.close(); if (failure instanceof IOException) { throw (IOException) failure; } @@ -328,19 +341,16 @@ OptionalLong firstChangeAfter(String dbName, byte[] rawKey, long targetBlock, byte[] prefix = changePrefix(dbName, rawKey); byte[] seek = ByteBuffer.allocate(prefix.length + Long.BYTES).put(prefix) .putLong(targetBlock + 1).array(); - try (ReadOptions reads = new ReadOptions(); - RocksIterator iterator = database.newIterator(reads)) { - iterator.seek(seek); - if (!iterator.isValid()) { - return OptionalLong.empty(); - } - byte[] found = iterator.key(); - if (found.length != prefix.length + Long.BYTES || !startsWith(found, prefix)) { - return OptionalLong.empty(); - } - long blockNumber = ByteBuffer.wrap(found, prefix.length, Long.BYTES).getLong(); - return blockNumber <= upperBound ? OptionalLong.of(blockNumber) : OptionalLong.empty(); + StateArchiveIndexDatabase.KeyValue foundEntry = database.seek(seek); + if (foundEntry == null) { + return OptionalLong.empty(); } + byte[] found = foundEntry.getKey(); + if (found.length != prefix.length + Long.BYTES || !startsWith(found, prefix)) { + return OptionalLong.empty(); + } + long blockNumber = ByteBuffer.wrap(found, prefix.length, Long.BYTES).getLong(); + return blockNumber <= upperBound ? OptionalLong.of(blockNumber) : OptionalLong.empty(); } OldValue readOldValue(String dbName, byte[] rawKey, long blockNumber) throws IOException { @@ -349,12 +359,7 @@ OldValue readOldValue(String dbName, byte[] rawKey, long blockNumber) throws IOE if (blockNumber <= marker.baseBlockNumber || blockNumber > marker.lastBlockNumber) { throw new IllegalArgumentException("checkpoint history block is outside coverage"); } - Location location; - try { - location = decodeLocation(database.get(blockKey(blockNumber))); - } catch (RocksDBException failure) { - throw new IOException("Failed to read State Archive checkpoint block location", failure); - } + Location location = decodeLocation(database.get(blockKey(blockNumber))); String fileName = StateArchiveCheckpointMaterializer.blockFileName(location.index, new BlockSnapshotMeta(location.epoch, blockNumber, location.blockHash, new byte[DIGEST_LENGTH], 0)); @@ -395,8 +400,11 @@ byte[] getHeadHash() { public void close() { if (!closed) { closed = true; - database.close(); - options.close(); + try { + database.close(); + } catch (IOException failure) { + throw new IllegalStateException("Failed to close Archive serving reader", failure); + } } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java new file mode 100644 index 00000000000..6f2f7c6cb00 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java @@ -0,0 +1,320 @@ +package org.tron.core.db2.archive; + +import static org.fusesource.leveldbjni.JniDBFactory.factory; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.iq80.leveldb.DB; +import org.iq80.leveldb.DBIterator; +import org.iq80.leveldb.ReadOptions; +import org.iq80.leveldb.Snapshot; +import org.tron.common.utils.DbOptionalsUtils; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Engine-neutral native store for the common-checkpoint Archive serving index. */ +final class StateArchiveIndexDatabase { + + private static final Map LEVEL_DATABASES = new HashMap<>(); + + private StateArchiveIndexDatabase() { + } + + static Reader openReader(Path directory, Engine engine) throws IOException { + Path path = normalize(directory); + return engine == Engine.LEVELDB ? new LevelReader(acquireLevel(path, false)) + : new RocksReader(path); + } + + static Writer openWriter(Path directory, Engine engine) throws IOException { + Path path = normalize(directory); + return engine == Engine.LEVELDB ? new LevelWriter(acquireLevel(path, true)) + : new RocksWriter(path); + } + + static Mutation put(byte[] key, byte[] value) { + return new Mutation(key, value); + } + + private static Path normalize(Path directory) { + return Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); + } + + private static synchronized SharedLevelDatabase acquireLevel(Path directory, boolean create) + throws IOException { + SharedLevelDatabase shared = LEVEL_DATABASES.get(directory); + if (shared == null) { + org.iq80.leveldb.Options options = DbOptionalsUtils.createDefaultDbOptions() + .createIfMissing(create); + try { + shared = new SharedLevelDatabase(directory, factory.open(directory.toFile(), options)); + } catch (IOException | RuntimeException failure) { + if (failure instanceof IOException) { + throw (IOException) failure; + } + throw failure; + } + LEVEL_DATABASES.put(directory, shared); + } + shared.references++; + return shared; + } + + private static synchronized void releaseLevel(SharedLevelDatabase shared) throws IOException { + if (--shared.references != 0) { + return; + } + LEVEL_DATABASES.remove(shared.directory); + shared.database.close(); + } + + interface Reader extends Closeable { + + byte[] get(byte[] key) throws IOException; + + KeyValue seek(byte[] key) throws IOException; + } + + interface Writer extends Closeable { + + byte[] get(byte[] key) throws IOException; + + void write(List mutations) throws IOException; + } + + static final class Mutation { + private final byte[] key; + private final byte[] value; + + private Mutation(byte[] key, byte[] value) { + this.key = Arrays.copyOf(Objects.requireNonNull(key, "key"), key.length); + this.value = Arrays.copyOf(Objects.requireNonNull(value, "value"), value.length); + } + } + + static final class KeyValue { + private final byte[] key; + private final byte[] value; + + private KeyValue(byte[] key, byte[] value) { + this.key = Arrays.copyOf(key, key.length); + this.value = Arrays.copyOf(value, value.length); + } + + byte[] getKey() { + return Arrays.copyOf(key, key.length); + } + + byte[] getValue() { + return Arrays.copyOf(value, value.length); + } + } + + private static final class SharedLevelDatabase { + private final Path directory; + private final DB database; + private int references; + + private SharedLevelDatabase(Path directory, DB database) { + this.directory = directory; + this.database = database; + } + } + + private static final class LevelReader implements Reader { + private final SharedLevelDatabase shared; + private final Snapshot snapshot; + private final ReadOptions reads; + private boolean closed; + + private LevelReader(SharedLevelDatabase shared) throws IOException { + this.shared = shared; + Snapshot openedSnapshot = null; + ReadOptions openedReads = null; + try { + openedSnapshot = shared.database.getSnapshot(); + openedReads = new ReadOptions().fillCache(false).snapshot(openedSnapshot); + } catch (RuntimeException failure) { + if (openedSnapshot != null) { + openedSnapshot.close(); + } + releaseLevel(shared); + throw failure; + } + this.snapshot = openedSnapshot; + this.reads = openedReads; + } + + @Override + public byte[] get(byte[] key) { + return shared.database.get(key, reads); + } + + @Override + public KeyValue seek(byte[] key) throws IOException { + try (DBIterator iterator = shared.database.iterator(reads)) { + iterator.seek(key); + if (!iterator.hasNext()) { + return null; + } + Map.Entry entry = iterator.next(); + return new KeyValue(entry.getKey(), entry.getValue()); + } + } + + @Override + public void close() throws IOException { + if (!closed) { + closed = true; + snapshot.close(); + releaseLevel(shared); + } + } + } + + private static final class LevelWriter implements Writer { + private final SharedLevelDatabase shared; + private final org.iq80.leveldb.WriteOptions writes = + new org.iq80.leveldb.WriteOptions().sync(true); + private boolean closed; + + private LevelWriter(SharedLevelDatabase shared) { + this.shared = shared; + } + + @Override + public byte[] get(byte[] key) { + return shared.database.get(key); + } + + @Override + public void write(List mutations) throws IOException { + try (org.iq80.leveldb.WriteBatch batch = shared.database.createWriteBatch()) { + for (Mutation mutation : mutations) { + batch.put(mutation.key, mutation.value); + } + shared.database.write(batch, writes); + } + } + + @Override + public void close() throws IOException { + if (!closed) { + closed = true; + releaseLevel(shared); + } + } + } + + private static final class RocksReader implements Reader { + static { + org.rocksdb.RocksDB.loadLibrary(); + } + + private final org.rocksdb.Options options = + new org.rocksdb.Options().setCreateIfMissing(false); + private final org.rocksdb.RocksDB database; + private boolean closed; + + private RocksReader(Path directory) throws IOException { + try { + database = org.rocksdb.RocksDB.openReadOnly(options, directory.toString()); + } catch (org.rocksdb.RocksDBException | RuntimeException failure) { + options.close(); + throw new IOException("Failed to open RocksDB Archive serving index", failure); + } + } + + @Override + public byte[] get(byte[] key) throws IOException { + try { + return database.get(key); + } catch (org.rocksdb.RocksDBException failure) { + throw new IOException("Failed to read RocksDB Archive serving index", failure); + } + } + + @Override + public KeyValue seek(byte[] key) throws IOException { + try (org.rocksdb.ReadOptions reads = new org.rocksdb.ReadOptions(); + org.rocksdb.RocksIterator iterator = database.newIterator(reads)) { + iterator.seek(key); + if (!iterator.isValid()) { + iterator.status(); + return null; + } + return new KeyValue(iterator.key(), iterator.value()); + } catch (org.rocksdb.RocksDBException failure) { + throw new IOException("Failed to seek RocksDB Archive serving index", failure); + } + } + + @Override + public void close() { + if (!closed) { + closed = true; + database.close(); + options.close(); + } + } + } + + private static final class RocksWriter implements Writer { + static { + org.rocksdb.RocksDB.loadLibrary(); + } + + private final org.rocksdb.Options options = + new org.rocksdb.Options().setCreateIfMissing(true) + .setCompressionType(org.rocksdb.CompressionType.NO_COMPRESSION); + private final org.rocksdb.WriteOptions writes = new org.rocksdb.WriteOptions().setSync(true); + private final org.rocksdb.RocksDB database; + private boolean closed; + + private RocksWriter(Path directory) throws IOException { + try { + database = org.rocksdb.RocksDB.open(options, directory.toString()); + } catch (org.rocksdb.RocksDBException | RuntimeException failure) { + writes.close(); + options.close(); + throw new IOException("Failed to open RocksDB Archive serving index", failure); + } + } + + @Override + public byte[] get(byte[] key) throws IOException { + try { + return database.get(key); + } catch (org.rocksdb.RocksDBException failure) { + throw new IOException("Failed to read RocksDB Archive serving index", failure); + } + } + + @Override + public void write(List mutations) throws IOException { + try (org.rocksdb.WriteBatch batch = new org.rocksdb.WriteBatch()) { + for (Mutation mutation : mutations) { + batch.put(mutation.key, mutation.value); + } + database.write(writes, batch); + } catch (org.rocksdb.RocksDBException failure) { + throw new IOException("Failed to write RocksDB Archive serving index", failure); + } + } + + @Override + public void close() { + if (!closed) { + closed = true; + database.close(); + writes.close(); + options.close(); + } + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexEngineManifest.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexEngineManifest.java new file mode 100644 index 00000000000..b40412a1853 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexEngineManifest.java @@ -0,0 +1,122 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Durable engine identity that prevents a checkpoint serving index from changing engine. */ +final class StateArchiveIndexEngineManifest { + + static final String FILE = "ENGINE"; + private static final String TEMP = "ENGINE.tmp"; + private static final int MAGIC = 0x53414945; // SAIE + private static final short VERSION = 1; + private static final int DIGEST_LENGTH = 32; + private static final int ENCODED_LENGTH = Integer.BYTES + 2 * Short.BYTES + DIGEST_LENGTH; + + private StateArchiveIndexEngineManifest() { + } + + static void openOrCreate(Path directory, Engine engine) throws IOException { + Path root = Objects.requireNonNull(directory, "directory"); + Engine selected = Objects.requireNonNull(engine, "engine"); + Files.createDirectories(root); + if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("State Archive serving-index path is not a directory"); + } + Path manifest = root.resolve(FILE); + if (Files.exists(manifest, LinkOption.NOFOLLOW_LINKS)) { + requireFile(manifest, selected); + return; + } + Path database = root.resolve("keys"); + if (Files.exists(database, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("State Archive serving index has no engine identity"); + } + byte[] encoded = encode(selected); + Path temporary = root.resolve(TEMP); + try { + Files.write(temporary, encoded, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, + StandardOpenOption.SYNC); + try { + Files.move(temporary, manifest, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException failure) { + throw new IOException("State Archive serving engine publication must be atomic", failure); + } + HistorySegmentStore.syncDirectory(root); + } catch (IOException | RuntimeException failure) { + Files.deleteIfExists(temporary); + throw failure; + } + } + + static void require(Path directory, Engine engine) throws IOException { + requireFile(Objects.requireNonNull(directory, "directory").resolve(FILE), + Objects.requireNonNull(engine, "engine")); + } + + private static void requireFile(Path manifest, Engine engine) throws IOException { + if (!Files.isRegularFile(manifest, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("State Archive serving engine identity is missing"); + } + byte[] encoded = Files.readAllBytes(manifest); + if (encoded.length != ENCODED_LENGTH) { + throw new IOException("State Archive serving engine identity length is invalid"); + } + int bodyLength = encoded.length - DIGEST_LENGTH; + byte[] body = Arrays.copyOf(encoded, bodyLength); + if (!Arrays.equals(Arrays.copyOfRange(encoded, bodyLength, encoded.length), + Hashing.sha256().hashBytes(body).asBytes())) { + throw new IOException("State Archive serving engine identity checksum differs"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(body))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION) { + throw new IOException("State Archive serving engine identity is unsupported"); + } + int tag = input.readUnsignedShort(); + if (tag != tag(engine)) { + throw new IOException("State Archive serving index engine differs: expected " + engine); + } + } + } + + private static byte[] encode(Engine engine) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(ENCODED_LENGTH); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(tag(engine)); + output.flush(); + byte[] body = bytes.toByteArray(); + output.write(Hashing.sha256().hashBytes(body).asBytes()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory serving engine encoding failed", impossible); + } + } + + private static int tag(Engine engine) { + switch (engine) { + case LEVELDB: + return 1; + case ROCKSDB: + return 2; + default: + throw new IllegalArgumentException("Unsupported State Archive serving engine: " + engine); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java index 6af3a629dd3..15bced40829 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java @@ -1,5 +1,6 @@ package org.tron.core.db2.archive; +import static org.fusesource.leveldbjni.JniDBFactory.factory; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -15,17 +16,18 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.iq80.leveldb.DB; +import org.iq80.leveldb.Options; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.rocksdb.Options; -import org.rocksdb.RocksDB; import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; import org.tron.core.db2.archive.BlockReverseDiff.Entry; import org.tron.core.db2.core.CommonCheckpointMaterializer.Status; import org.tron.core.db2.core.CommonCheckpointPayload; import org.tron.core.db2.core.CommonCheckpointTarget; import org.tron.core.db2.stateroot.PathStateFlushTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; public class StateArchiveCheckpointMaterializerTest { @@ -62,13 +64,22 @@ public void preservesEveryBlockBoundaryBeforePublishingReadableAcrossReopen() materializer.publish(target); assertEquals(Status.PUBLISHED, materializer.inspect(target)); try (StateArchiveCheckpointReadAdapter reader = - StateArchiveCheckpointReadAdapter.open(root, target)) { + StateArchiveCheckpointReadAdapter.open(root, target); + StateArchiveCheckpointReadAdapter concurrent = + StateArchiveCheckpointReadAdapter.open(root, target)) { assertEquals(0, reader.getIndexedFrom()); assertEquals(3, reader.getIndexedThrough()); assertArrayEquals(new byte[]{0}, reader.findOldValueAfter("code", new byte[]{1}, 0) .get().getValue()); assertFalse(reader.findOldValueAfter("code", new byte[]{2}, 2).isPresent()); + assertArrayEquals(new byte[]{0}, concurrent.findOldValueAfter("code", new byte[]{1}, 0) + .get().getValue()); } + assertTrue(Files.isRegularFile(root.resolve(StateArchiveCheckpointServingIndex.DIRECTORY) + .resolve(StateArchiveIndexEngineManifest.FILE))); + StateArchiveCheckpointMaterializer wrongEngine = new StateArchiveCheckpointMaterializer( + root, format, null, Engine.ROCKSDB); + assertThrows(IOException.class, () -> wrongEngine.inspect(target)); StateArchiveCheckpointMaterializer reopened = new StateArchiveCheckpointMaterializer(root, format); @@ -162,9 +173,9 @@ public void rejectsForeignFormatCorruptImmutableBlockAndNonParentReadable() () -> clean.inspect(CommonCheckpointTarget.from(nonChild))); assertTrue(Files.isRegularFile(cleanRoot.resolve( StateArchiveCheckpointMaterializer.READABLE_FILE))); - try (Options options = new Options().setCreateIfMissing(false); - RocksDB database = RocksDB.open(options, cleanRoot.resolve( - StateArchiveCheckpointServingIndex.DIRECTORY).resolve("keys").toString())) { + Options options = new Options().createIfMissing(false); + try (DB database = factory.open(cleanRoot.resolve( + StateArchiveCheckpointServingIndex.DIRECTORY).resolve("keys").toFile(), options)) { database.put(new byte[]{0}, new byte[]{1}); } assertThrows(IOException.class, () -> clean.inspect(target)); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java new file mode 100644 index 00000000000..2138a274306 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java @@ -0,0 +1,53 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class StateArchiveIndexDatabaseTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void supportsConfiguredEngineAndRejectsEngineDrift() throws Exception { + for (Engine engine : Engine.values()) { + Path root = temporaryFolder.newFolder(engine.name().toLowerCase()).toPath(); + Path database = root.resolve("keys"); + StateArchiveIndexEngineManifest.openOrCreate(root, engine); + try (StateArchiveIndexDatabase.Writer writer = + StateArchiveIndexDatabase.openWriter(database, engine)) { + writer.write(Arrays.asList( + StateArchiveIndexDatabase.put(new byte[]{1}, new byte[]{11}), + StateArchiveIndexDatabase.put(new byte[]{3}, new byte[]{33}))); + } + try (StateArchiveIndexDatabase.Reader reader = + StateArchiveIndexDatabase.openReader(database, engine)) { + assertArrayEquals(new byte[]{11}, reader.get(new byte[]{1})); + StateArchiveIndexDatabase.KeyValue found = reader.seek(new byte[]{2}); + assertNotNull(found); + assertArrayEquals(new byte[]{3}, found.getKey()); + assertArrayEquals(new byte[]{33}, found.getValue()); + } + Engine other = engine == Engine.LEVELDB ? Engine.ROCKSDB : Engine.LEVELDB; + assertThrows(IOException.class, () -> StateArchiveIndexEngineManifest.require(root, other)); + } + } + + @Test + public void rejectsExistingDatabaseWithoutEngineIdentity() throws Exception { + Path root = temporaryFolder.newFolder("missing-manifest").toPath(); + Files.createDirectory(root.resolve("keys")); + assertThrows(IOException.class, + () -> StateArchiveIndexEngineManifest.openOrCreate(root, Engine.LEVELDB)); + } +} From 9d29ab541683622fea84512b72227b12e3a8f81a Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 5 Sep 2026 14:30:07 +0800 Subject: [PATCH 112/161] fix(chainbase): wire common checkpoint runtime --- .../db2/archive/ArchivePointSnapshot.java | 27 ++ .../core/db2/archive/ArchiveReadContext.java | 19 +- .../core/db2/archive/ArchiveReadSnapshot.java | 2 +- .../db2/archive/ArchiveRuntimeQueryGate.java | 34 +- ...HistoricalAccountAssetBalanceResolver.java | 4 +- .../HistoricalAccountAssetPrefixResolver.java | 4 +- .../HistoricalAccountBalanceReader.java | 2 +- .../db2/archive/HistoricalQuerySession.java | 22 ++ .../archive/LatestStateGenerationAdapter.java | 15 + ...testStateGenerationCoordinatorFactory.java | 14 +- .../StateArchiveCheckpointMaterializer.java | 45 ++- .../StateArchiveCheckpointReadAdapter.java | 19 +- .../StateArchiveCheckpointReadSnapshot.java | 3 +- .../core/ChainbaseCheckpointMaterializer.java | 67 +++- .../db2/core/CommonCheckpointBaseline.java | 53 +++ .../core/CommonCheckpointBaselineFile.java | 193 +++++++++ .../core/db2/core/CommonCheckpointFormat.java | 20 + .../tron/core/db2/core/SnapshotManager.java | 87 ++++ .../PathStateCheckpointMaterializer.java | 128 +++++- .../stateroot/PathStateCommitmentCodec.java | 5 +- .../PathStatePhysicalOverlayHead.java | 114 +++++- .../stateroot/PathStateRuntimeAttachment.java | 23 +- .../org/tron/core/config/args/Storage.java | 8 + .../tron/core/config/args/StorageConfig.java | 33 +- common/src/main/resources/reference.conf | 5 +- .../core/config/args/StorageConfigTest.java | 32 +- .../java/org/tron/core/config/args/Args.java | 2 + .../main/java/org/tron/core/db/Manager.java | 372 ++++++++++++++++-- framework/src/main/resources/config.conf | 4 +- .../core/db/ManagerArchiveEngineModeTest.java | 28 ++ .../tron/core/db2/SnapshotManagerTest.java | 31 ++ .../CommonCheckpointBaselineFileTest.java | 52 +++ .../stateroot/PathNodeStoreEngineTest.java | 4 +- .../PathStateCommitmentCodecTest.java | 21 +- ...athStateManagerStartupIntegrationTest.java | 114 ++++++ .../PathStateNativeNodeStoreTest.java | 45 +++ .../PathStatePersistentFormatTest.java | 2 +- .../core/db2/stateroot/PathStateRootTest.java | 5 +- 38 files changed, 1573 insertions(+), 85 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/ArchivePointSnapshot.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointBaseline.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointBaselineFile.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java create mode 100644 framework/src/test/java/org/tron/core/db/ManagerArchiveEngineModeTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/core/CommonCheckpointBaselineFileTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchivePointSnapshot.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchivePointSnapshot.java new file mode 100644 index 00000000000..27f233f9575 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchivePointSnapshot.java @@ -0,0 +1,27 @@ +package org.tron.core.db2.archive; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import org.tron.core.db2.archive.HistoricalRangeOverlay.Entry; +import org.tron.core.db2.archive.HistoricalRangeOverlay.KeyRange; +import org.tron.core.db2.archive.HistoricalRangeOverlay.Limits; + +/** Request-owned point-read contract shared by legacy and common-checkpoint generations. */ +public interface ArchivePointSnapshot extends Closeable { + + OldValue get(String dbName, byte[] physicalRawKey) throws IOException; + + default List range(String dbName, KeyRange range, Limits limits) throws IOException { + throw new UnsupportedOperationException( + "Range reads are unavailable for this archive generation"); + } + + long getTargetBlock(); + + long getPinnedBlock(); + + byte[] getPinnedHash(); + + void requirePinnedIdentity(); +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java index a8c48324e8a..d7603d5fe88 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadContext.java @@ -17,7 +17,7 @@ /** Request-owned bindings from every versioned Store to one pinned archive snapshot. */ public final class ArchiveReadContext implements Closeable { - private final ArchiveReadSnapshot snapshot; + private final ArchivePointSnapshot snapshot; private final Closeable owner; private final Map> adapters; private final HistoricalAccountAssetBalanceResolver accountAssetResolver = @@ -26,7 +26,7 @@ public final class ArchiveReadContext implements Closeable { new HistoricalAccountAssetPrefixResolver(); private boolean closed; - private ArchiveReadContext(ArchiveReadSnapshot snapshot, + private ArchiveReadContext(ArchivePointSnapshot snapshot, Collection> adapters, Closeable owner) { this.snapshot = Objects.requireNonNull(snapshot, "snapshot"); this.owner = Objects.requireNonNull(owner, "owner"); @@ -44,6 +44,17 @@ public static ArchiveReadContext open(ArchiveReadSnapshot snapshot, } } + /** Takes ownership of one common-checkpoint point snapshot. */ + public static ArchiveReadContext open(StateArchiveCheckpointReadSnapshot snapshot, + Collection> adapters) throws IOException { + try { + return new ArchiveReadContext(snapshot, adapters, snapshot); + } catch (RuntimeException failure) { + closeAfterFailedOpen(snapshot, failure); + throw failure; + } + } + /** Takes ownership of {@code lease} so closing this context also releases gate accounting. */ public static ArchiveReadContext open(ArchiveRuntimeQueryGate.Lease lease, Collection> adapters) throws IOException { @@ -206,10 +217,10 @@ public String getDbName() { /** Read-only point view for one exact physical Store keyspace. */ public static final class HistoricalStore { - private final ArchiveReadSnapshot snapshot; + private final ArchivePointSnapshot snapshot; private final StoreAdapter adapter; - private HistoricalStore(ArchiveReadSnapshot snapshot, StoreAdapter adapter) { + private HistoricalStore(ArchivePointSnapshot snapshot, StoreAdapter adapter) { this.snapshot = snapshot; this.adapter = adapter; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java index 40d825e253c..9cecb388cd7 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveReadSnapshot.java @@ -11,7 +11,7 @@ import org.tron.core.db2.archive.HistoricalRangeOverlay.Limits; /** One immutable physical-key archive read context pinned at {@code S(P)}. */ -public final class ArchiveReadSnapshot implements Closeable { +public final class ArchiveReadSnapshot implements ArchivePointSnapshot { private final long targetBlock; private final long pinnedBlock; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGate.java b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGate.java index 76e6d6cbdab..26ff517d382 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGate.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ArchiveRuntimeQueryGate.java @@ -81,7 +81,7 @@ interface SnapshotPinSource { } /** One request-owned snapshot whose close releases both resources and gate accounting. */ - public static final class Lease implements Closeable { + public static final class Lease implements ArchivePointSnapshot { private final ArchiveRuntimeQueryGate gate; private final ArchiveReadSnapshot snapshot; @@ -99,6 +99,38 @@ public synchronized ArchiveReadSnapshot getSnapshot() { return snapshot; } + @Override + public OldValue get(String dbName, byte[] physicalRawKey) throws IOException { + return getSnapshot().get(dbName, physicalRawKey); + } + + @Override + public java.util.List range(String dbName, + HistoricalRangeOverlay.KeyRange range, HistoricalRangeOverlay.Limits limits) + throws IOException { + return getSnapshot().range(dbName, range, limits); + } + + @Override + public long getTargetBlock() { + return getSnapshot().getTargetBlock(); + } + + @Override + public long getPinnedBlock() { + return getSnapshot().getPinnedBlock(); + } + + @Override + public byte[] getPinnedHash() { + return getSnapshot().getPinnedHash(); + } + + @Override + public void requirePinnedIdentity() { + getSnapshot().requirePinnedIdentity(); + } + @Override public void close() throws IOException { synchronized (this) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolver.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolver.java index 9d55cf7d579..0b995cde370 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolver.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetBalanceResolver.java @@ -24,9 +24,9 @@ public final class HistoricalAccountAssetBalanceResolver { private final P66AccountAssetCodec codec = new P66AccountAssetCodec(); - public Result resolve(ArchiveReadSnapshot snapshot, byte[] address, String tokenId) + public Result resolve(ArchivePointSnapshot snapshot, byte[] address, String tokenId) throws IOException { - ArchiveReadSnapshot pinned = Objects.requireNonNull(snapshot, "snapshot"); + ArchivePointSnapshot pinned = Objects.requireNonNull(snapshot, "snapshot"); requireScopedDatabases(); pinned.requirePinnedIdentity(); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetPrefixResolver.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetPrefixResolver.java index 5b1bbe0b811..ebccb6a08a9 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetPrefixResolver.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountAssetPrefixResolver.java @@ -18,9 +18,9 @@ public final class HistoricalAccountAssetPrefixResolver { private final P66AccountAssetCodec codec = new P66AccountAssetCodec(); - public Result resolve(ArchiveReadSnapshot snapshot, byte[] address, Limits limits) + public Result resolve(ArchivePointSnapshot snapshot, byte[] address, Limits limits) throws IOException { - ArchiveReadSnapshot pinned = Objects.requireNonNull(snapshot, "snapshot"); + ArchivePointSnapshot pinned = Objects.requireNonNull(snapshot, "snapshot"); Limits budgets = Objects.requireNonNull(limits, "limits"); byte[] accountAddress = requireAddress(address); HistoricalAccountAssetBalanceResolver.requireScopedDatabases(); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountBalanceReader.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountBalanceReader.java index 512634bf68e..1a88a64cb7a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountBalanceReader.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalAccountBalanceReader.java @@ -15,7 +15,7 @@ public final class HistoricalAccountBalanceReader { private HistoricalAccountBalanceReader() { } - public static Result read(ArchiveReadSnapshot snapshot, byte[] address) throws IOException { + public static Result read(ArchivePointSnapshot snapshot, byte[] address) throws IOException { Objects.requireNonNull(snapshot, "snapshot"); if (address == null || address.length != ADDRESS_LENGTH) { throw new IllegalArgumentException("TRON account address must be exactly 21 bytes"); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQuerySession.java b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQuerySession.java index dbe01f6aea3..e73bfe01439 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQuerySession.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/HistoricalQuerySession.java @@ -66,6 +66,28 @@ public static HistoricalQuerySession open(ArchiveRuntimeQueryGate.Lease lease, return open(lease, targetBlockHash, Limits.defaults()); } + /** Takes ownership of a common-checkpoint point snapshot. */ + public static HistoricalQuerySession open(StateArchiveCheckpointReadSnapshot snapshot, + byte[] targetBlockHash) throws IOException { + return open(snapshot, targetBlockHash, Limits.defaults()); + } + + /** Takes ownership of a common-checkpoint point snapshot. */ + public static HistoricalQuerySession open(StateArchiveCheckpointReadSnapshot snapshot, + byte[] targetBlockHash, Limits limits) throws IOException { + ArchiveReadContext context = ArchiveReadContext.open(snapshot, EXACT_ADAPTERS); + try { + return new HistoricalQuerySession(context, targetBlockHash, limits); + } catch (RuntimeException failure) { + try { + context.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } + } + /** Takes ownership of {@code lease}, including if session construction fails. */ public static HistoricalQuerySession open(ArchiveRuntimeQueryGate.Lease lease, byte[] targetBlockHash, Limits limits) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java index 8345cb4fd30..9503306fe7e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationAdapter.java @@ -166,6 +166,13 @@ public PinnedLatestState pin(PersistentServingKeyIndexGeneration serving) throws serving.getParticipatingDatabases()); } + /** Pins the current native engines for a common-checkpoint request identity. */ + public PinnedLatestState pin(long blockNumber, byte[] blockHash) throws IOException { + String generationId = "common-checkpoint-" + blockNumber + '-' + + com.google.common.io.BaseEncoding.base16().lowerCase().encode(blockHash); + return pin(generationId, blockNumber, blockHash, participants); + } + PinnedLatestState pin(String generationId, long blockNumber, byte[] blockHash, List expectedParticipants) throws IOException { if (generationId == null || generationId.isEmpty() || blockNumber < 0 @@ -199,6 +206,14 @@ public byte[] getSourceIdentityDigest() { return Arrays.copyOf(sourceIdentityDigest, sourceIdentityDigest.length); } + List participantsForCoordinator() { + return participants; + } + + Map storesForCoordinator() { + return stores; + } + private static void validateSnapshot(String dbName, String sourceIdentity, long blockNumber, byte[] blockHash, StoreSnapshot snapshot) throws ArchivePersistenceException { if (!dbName.equals(snapshot.getDbName()) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java index 908b039ed39..e477110abac 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/LatestStateGenerationCoordinatorFactory.java @@ -31,6 +31,7 @@ public static LatestStateGenerationCoordinator create(SnapshotManager manager, public static LatestStateGenerationCoordinator create(SnapshotManager manager, LatestStateGenerationCoordinator.AuthorityReader authorityReader) throws ArchivePersistenceException { + Objects.requireNonNull(authorityReader, "authorityReader"); return create(manager, java.util.Collections.emptyMap(), authorityReader); } @@ -38,9 +39,17 @@ public static LatestStateGenerationCoordinator create(SnapshotManager manager, Map supplementalStores, LatestStateGenerationCoordinator.AuthorityReader authorityReader) throws ArchivePersistenceException { + LatestStateGenerationAdapter adapter = createAdapter(manager, supplementalStores); + return new LatestStateGenerationCoordinator(adapter.participantsForCoordinator(), + adapter.storesForCoordinator(), manager::withArchiveStateBarrier, authorityReader); + } + + /** Builds a direct request-pinning adapter for the common-checkpoint read gate. */ + public static LatestStateGenerationAdapter createAdapter(SnapshotManager manager, + Map supplementalStores) + throws ArchivePersistenceException { Objects.requireNonNull(manager, "manager"); Objects.requireNonNull(supplementalStores, "supplementalStores"); - Objects.requireNonNull(authorityReader, "authorityReader"); List registered = new ArrayList<>(manager.getDbs()); try { ArchiveStoreScope.validate(registered); @@ -91,7 +100,6 @@ public static LatestStateGenerationCoordinator create(SnapshotManager manager, } List participants = new ArrayList<>(stores.keySet()); - return new LatestStateGenerationCoordinator(participants, stores, - manager::withArchiveStateBarrier, authorityReader); + return new LatestStateGenerationAdapter(participants, stores); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java index ce6da0d7bd4..78f4f9afbd2 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java @@ -22,8 +22,10 @@ import java.util.Set; import java.util.UUID; import org.tron.core.db2.core.CommonCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointBaseline; import org.tron.core.db2.core.CommonCheckpointPayload; import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** Next-format State Archive participant for the common-checkpoint two-barrier protocol. */ public final class StateArchiveCheckpointMaterializer implements CommonCheckpointMaterializer { @@ -50,16 +52,43 @@ public final class StateArchiveCheckpointMaterializer implements CommonCheckpoin private final byte[] formatIdentity; private final BlockHistoryCodec historyCodec = new BlockHistoryCodec(); private final FaultHook faultHook; + private final CommonCheckpointBaseline baseline; + private final Engine engine; public StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity) { - this(directory, formatIdentity, (stage, blockIndex) -> { }); + this(directory, formatIdentity, null, StateArchiveCheckpointServingIndex.configuredEngine(), + (stage, blockIndex) -> { }); + } + + public StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, + CommonCheckpointBaseline baseline) { + this(directory, formatIdentity, baseline, + StateArchiveCheckpointServingIndex.configuredEngine(), (stage, blockIndex) -> { }); + } + + public StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, + CommonCheckpointBaseline baseline, Engine engine) { + this(directory, formatIdentity, baseline, engine, (stage, blockIndex) -> { }); } StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, FaultHook faultHook) { + this(directory, formatIdentity, null, StateArchiveCheckpointServingIndex.configuredEngine(), + faultHook); + } + + StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, Engine engine, + FaultHook faultHook) { + this(directory, formatIdentity, null, engine, faultHook); + } + + private StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, + CommonCheckpointBaseline baseline, Engine engine, FaultHook faultHook) { this.directory = Objects.requireNonNull(directory, "directory"); this.formatIdentity = digest(formatIdentity, "formatIdentity"); this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + this.baseline = baseline; + this.engine = Objects.requireNonNull(engine, "engine"); } @Override @@ -80,6 +109,8 @@ public synchronized Status inspect(CommonCheckpointTarget target) throws IOExcep return Status.PUBLISHED; } requireParent(current, admitted); + } else if (baseline != null) { + baseline.requireParent(admitted, "State Archive"); } Path materialized = materializedPath(admitted); if (!Files.exists(materialized, LinkOption.NOFOLLOW_LINKS)) { @@ -93,8 +124,14 @@ public synchronized Status inspect(CommonCheckpointTarget target) throws IOExcep /** Loads and fully validates the target currently published by Archive READABLE. */ public static CommonCheckpointTarget loadPublishedTarget(Path directory, byte[] expectedFormatIdentity) throws IOException { + return loadPublishedTarget(directory, expectedFormatIdentity, + StateArchiveCheckpointServingIndex.configuredEngine()); + } + + public static CommonCheckpointTarget loadPublishedTarget(Path directory, + byte[] expectedFormatIdentity, Engine engine) throws IOException { StateArchiveCheckpointMaterializer materializer = - new StateArchiveCheckpointMaterializer(directory, expectedFormatIdentity); + new StateArchiveCheckpointMaterializer(directory, expectedFormatIdentity, null, engine); Path readable = directory.resolve(READABLE_FILE); if (!Files.exists(readable, LinkOption.NOFOLLOW_LINKS)) { throw new IOException("State Archive READABLE target is missing"); @@ -132,7 +169,7 @@ public synchronized void materialize(CommonCheckpointPayload payload, faultHook.after(Stage.AFTER_BLOCK_FILE, index); } requireExactBlockSet(blocks, expectedNames); - StateArchiveCheckpointServingIndex.apply(directory, admittedPayload, admittedTarget); + StateArchiveCheckpointServingIndex.apply(directory, admittedPayload, admittedTarget, engine); faultHook.after(Stage.AFTER_SERVING_INDEX_BATCH, -1); publishImmutable(materializedPath(admittedTarget), encodeTarget(admittedTarget)); faultHook.after(Stage.AFTER_MATERIALIZED_TARGET, -1); @@ -193,7 +230,7 @@ private void requireParent(TargetMarker current, CommonCheckpointTarget target) } private void requireServingIndex(CommonCheckpointTarget target) throws IOException { - if (StateArchiveCheckpointServingIndex.inspect(directory, target) + if (StateArchiveCheckpointServingIndex.inspect(directory, target, engine) != StateArchiveCheckpointServingIndex.Status.EXACT) { throw new IOException("State Archive checkpoint serving index target differs"); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java index 03677c16419..f74ea75e105 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java @@ -7,6 +7,7 @@ import java.util.OptionalLong; import org.tron.core.db2.core.CommonCheckpointMaterializer.Status; import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** Request-owned exact-key reader for one published next-format checkpoint target. */ public final class StateArchiveCheckpointReadAdapter implements AutoCloseable { @@ -24,23 +25,35 @@ private StateArchiveCheckpointReadAdapter(CommonCheckpointTarget target, /** Opens only an exact target whose Archive READABLE and serving-index markers are published. */ public static StateArchiveCheckpointReadAdapter open(Path archiveDirectory, CommonCheckpointTarget target) throws IOException { + return open(archiveDirectory, target, StateArchiveCheckpointServingIndex.configuredEngine()); + } + + public static StateArchiveCheckpointReadAdapter open(Path archiveDirectory, + CommonCheckpointTarget target, Engine engine) throws IOException { Path directory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); StateArchiveCheckpointMaterializer materializer = - new StateArchiveCheckpointMaterializer(directory, admitted.getFormatIdentity()); + new StateArchiveCheckpointMaterializer(directory, admitted.getFormatIdentity(), null, + engine); if (materializer.inspect(admitted) != Status.PUBLISHED) { throw new IOException("State Archive checkpoint target is not published for reading"); } return new StateArchiveCheckpointReadAdapter(admitted, - StateArchiveCheckpointServingIndex.openReader(directory, admitted)); + StateArchiveCheckpointServingIndex.openReader(directory, admitted, engine)); } /** Reconstructs the published target from disk before opening the exact-point reader. */ public static StateArchiveCheckpointReadAdapter open(Path archiveDirectory, byte[] expectedFormatIdentity) throws IOException { + return open(archiveDirectory, expectedFormatIdentity, + StateArchiveCheckpointServingIndex.configuredEngine()); + } + + public static StateArchiveCheckpointReadAdapter open(Path archiveDirectory, + byte[] expectedFormatIdentity, Engine engine) throws IOException { Path directory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); return open(directory, StateArchiveCheckpointMaterializer.loadPublishedTarget(directory, - expectedFormatIdentity)); + expectedFormatIdentity, engine), engine); } /** diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java index 19fdb997cc9..835768ae959 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java @@ -1,6 +1,5 @@ package org.tron.core.db2.archive; -import java.io.Closeable; import java.io.IOException; import java.nio.file.Path; import java.util.Arrays; @@ -10,7 +9,7 @@ import org.tron.core.db2.core.CommonCheckpointRuntimeOwner; /** Request-owned, point-only view over one published next-format checkpoint head. */ -public final class StateArchiveCheckpointReadSnapshot implements Closeable { +public final class StateArchiveCheckpointReadSnapshot implements ArchivePointSnapshot { private final long targetBlock; private final long pinnedBlock; diff --git a/chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java index 975f5321d70..7c41aceae60 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java @@ -39,18 +39,30 @@ public final class ChainbaseCheckpointMaterializer implements CommonCheckpointMa private final byte[] formatIdentity; private final Map databases; private final FaultHook faultHook; + private final CommonCheckpointBaseline baseline; public ChainbaseCheckpointMaterializer(Path directory, byte[] formatIdentity, List databases) { - this(directory, formatIdentity, databases, (stage, dbName) -> { }); + this(directory, formatIdentity, databases, null, (stage, dbName) -> { }); + } + + public ChainbaseCheckpointMaterializer(Path directory, byte[] formatIdentity, + List databases, CommonCheckpointBaseline baseline) { + this(directory, formatIdentity, databases, baseline, (stage, dbName) -> { }); } ChainbaseCheckpointMaterializer(Path directory, byte[] formatIdentity, List databases, FaultHook faultHook) { + this(directory, formatIdentity, databases, null, faultHook); + } + + private ChainbaseCheckpointMaterializer(Path directory, byte[] formatIdentity, + List databases, CommonCheckpointBaseline baseline, FaultHook faultHook) { this.directory = Objects.requireNonNull(directory, "directory"); this.formatIdentity = digest(formatIdentity, "formatIdentity"); this.databases = index(databases); this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + this.baseline = baseline; } @Override @@ -58,6 +70,18 @@ public Authority authority() { return Authority.CHAINBASE; } + /** Loads the compact next-format head published beside the Chainbase databases. */ + public static PublishedHead loadPublishedHead(Path directory, byte[] expectedFormatIdentity) + throws IOException { + Marker marker = load(Objects.requireNonNull(directory, "directory").resolve(CURRENT_FILE)); + if (!Arrays.equals(marker.formatIdentity, + digest(expectedFormatIdentity, "expectedFormatIdentity"))) { + throw new IOException("Chainbase published target format identity differs"); + } + return new PublishedHead(marker.lastEpoch, marker.lastBlockNumber, marker.lastBlockHash, + marker.stateRoot, marker.payloadDigest); + } + @Override public synchronized Status inspect(CommonCheckpointTarget target) throws IOException { CommonCheckpointTarget admitted = requireTarget(target); @@ -70,6 +94,8 @@ public synchronized Status inspect(CommonCheckpointTarget target) throws IOExcep return Status.PUBLISHED; } requireParent(current, admitted); + } else if (baseline != null) { + baseline.requireParent(admitted, "Chainbase"); } Path materialized = materializedPath(admitted); if (!Files.exists(materialized, LinkOption.NOFOLLOW_LINKS)) { @@ -355,4 +381,43 @@ private Marker(byte[] encoded, byte[] formatIdentity, byte[] payloadDigest, long this.stateRoot = stateRoot; } } + + /** Minimal restart identity retained by CHAINBASE_CURRENT. */ + public static final class PublishedHead { + + private final long epoch; + private final long blockNumber; + private final byte[] blockHash; + private final byte[] stateRoot; + private final byte[] payloadDigest; + + private PublishedHead(long epoch, long blockNumber, byte[] blockHash, byte[] stateRoot, + byte[] payloadDigest) { + this.epoch = epoch; + this.blockNumber = blockNumber; + this.blockHash = Arrays.copyOf(blockHash, blockHash.length); + this.stateRoot = Arrays.copyOf(stateRoot, stateRoot.length); + this.payloadDigest = Arrays.copyOf(payloadDigest, payloadDigest.length); + } + + public long getEpoch() { + return epoch; + } + + public long getBlockNumber() { + return blockNumber; + } + + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + public byte[] getStateRoot() { + return Arrays.copyOf(stateRoot, stateRoot.length); + } + + public byte[] getPayloadDigest() { + return Arrays.copyOf(payloadDigest, payloadDigest.length); + } + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointBaseline.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointBaseline.java new file mode 100644 index 00000000000..75fe18c39ab --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointBaseline.java @@ -0,0 +1,53 @@ +package org.tron.core.db2.core; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +/** Durable parent admitted before the first next-format common checkpoint. */ +public final class CommonCheckpointBaseline { + + private final byte[] formatIdentity; + private final BlockSnapshotMeta head; + private final byte[] stateRoot; + + public CommonCheckpointBaseline(byte[] formatIdentity, BlockSnapshotMeta head, + byte[] stateRoot) { + this.formatIdentity = copy32(formatIdentity, "formatIdentity"); + this.head = Objects.requireNonNull(head, "head"); + this.stateRoot = copy32(stateRoot, "stateRoot"); + } + + public byte[] getFormatIdentity() { + return Arrays.copyOf(formatIdentity, formatIdentity.length); + } + + public BlockSnapshotMeta getHead() { + return head; + } + + public byte[] getStateRoot() { + return Arrays.copyOf(stateRoot, stateRoot.length); + } + + public void requireParent(CommonCheckpointTarget target, String authority) throws IOException { + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + BlockSnapshotMeta first = admitted.getFirstBlock(); + if (!Arrays.equals(formatIdentity, admitted.getFormatIdentity()) + || head.getEpoch() + 1 != first.getEpoch() + || head.getBlockNumber() + 1 != first.getBlockNumber() + || !Arrays.equals(head.getBlockHash(), first.getParentHash()) + || !Arrays.equals(stateRoot, admitted.getParentStateRoot())) { + throw new IOException(authority + " checkpoint does not extend the admitted baseline"); + } + } + + private static byte[] copy32(byte[] value, String name) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (copy.length != 32) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return copy; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointBaselineFile.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointBaselineFile.java new file mode 100644 index 00000000000..3d0169ef4cc --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointBaselineFile.java @@ -0,0 +1,193 @@ +package org.tron.core.db2.core; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +/** Immutable, forced admission record for the first common-checkpoint parent. */ +public final class CommonCheckpointBaselineFile { + + public static final String FILE_NAME = "COMMON_BASELINE"; + public static final String BOOTSTRAP_INTENT_FILE = "COMMON_BOOTSTRAP_INTENT"; + private static final int MAGIC = 0x43424c4e; // CBLN + private static final short VERSION = 1; + private static final int DIGEST_LENGTH = 32; + private static final int LENGTH = Integer.BYTES + 2 * Short.BYTES + DIGEST_LENGTH + + 3 * Long.BYTES + 3 * DIGEST_LENGTH + DIGEST_LENGTH; + + private final Path directory; + private final Path path; + + public CommonCheckpointBaselineFile(Path directory) { + this.directory = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); + this.path = this.directory.resolve(FILE_NAME); + } + + /** Publishes the supplied baseline once, or returns the exact previously admitted baseline. */ + public synchronized CommonCheckpointBaseline openOrCreate(CommonCheckpointBaseline supplied) + throws IOException { + Objects.requireNonNull(supplied, "supplied"); + if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + CommonCheckpointBaseline loaded = load(); + if (!Arrays.equals(encode(loaded), encode(supplied))) { + throw new IOException("Common checkpoint baseline differs from canonical startup state"); + } + return loaded; + } + Files.createDirectories(directory); + Path temporary = directory.resolve(FILE_NAME + ".tmp-" + UUID.randomUUID()); + byte[] encoded = encode(supplied); + try { + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + java.nio.ByteBuffer buffer = java.nio.ByteBuffer.wrap(encoded); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + try { + Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("Common checkpoint baseline requires atomic rename", unsupported); + } + syncDirectory(directory); + return supplied; + } finally { + Files.deleteIfExists(temporary); + } + } + + /** Durably marks that a missing PathState directory is being built for this format. */ + public synchronized void beginBootstrap(byte[] formatIdentity) throws IOException { + byte[] admitted = Arrays.copyOf(Objects.requireNonNull(formatIdentity, "formatIdentity"), + formatIdentity.length); + if (admitted.length != DIGEST_LENGTH) { + throw new IllegalArgumentException("formatIdentity must contain exactly 32 bytes"); + } + Files.createDirectories(directory); + Path intent = directory.resolve(BOOTSTRAP_INTENT_FILE); + if (Files.exists(intent, LinkOption.NOFOLLOW_LINKS)) { + if (!Arrays.equals(Files.readAllBytes(intent), admitted)) { + throw new IOException("Common checkpoint bootstrap intent format differs"); + } + return; + } + publishBytes(intent, admitted); + } + + public synchronized boolean hasBootstrapIntent(byte[] formatIdentity) throws IOException { + Path intent = directory.resolve(BOOTSTRAP_INTENT_FILE); + return Files.isRegularFile(intent, LinkOption.NOFOLLOW_LINKS) + && Arrays.equals(Files.readAllBytes(intent), formatIdentity); + } + + public synchronized void retireBootstrapIntent() throws IOException { + if (Files.deleteIfExists(directory.resolve(BOOTSTRAP_INTENT_FILE))) { + syncDirectory(directory); + } + } + + public synchronized CommonCheckpointBaseline load() throws IOException { + byte[] encoded = Files.readAllBytes(path); + if (encoded.length != LENGTH) { + throw new IOException("Common checkpoint baseline length is invalid"); + } + int bodyLength = encoded.length - DIGEST_LENGTH; + byte[] body = Arrays.copyOf(encoded, bodyLength); + if (!Arrays.equals(Arrays.copyOfRange(encoded, bodyLength, encoded.length), + Hashing.sha256().hashBytes(body).asBytes())) { + throw new IOException("Common checkpoint baseline checksum differs"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(body))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0) { + throw new IOException("Common checkpoint baseline format is unsupported"); + } + byte[] format = readDigest(input); + long epoch = input.readLong(); + long number = input.readLong(); + long timestamp = input.readLong(); + byte[] hash = readDigest(input); + byte[] parentHash = readDigest(input); + byte[] stateRoot = readDigest(input); + return new CommonCheckpointBaseline(format, + new BlockSnapshotMeta(epoch, number, hash, parentHash, timestamp), stateRoot); + } catch (EOFException truncated) { + throw new IOException("Common checkpoint baseline is truncated", truncated); + } + } + + private static byte[] encode(CommonCheckpointBaseline baseline) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(LENGTH); + DataOutputStream output = new DataOutputStream(bytes); + BlockSnapshotMeta head = baseline.getHead(); + output.writeInt(MAGIC); + output.writeShort(VERSION); + output.writeShort(0); + output.write(baseline.getFormatIdentity()); + output.writeLong(head.getEpoch()); + output.writeLong(head.getBlockNumber()); + output.writeLong(head.getTimestamp()); + output.write(head.getBlockHash()); + output.write(head.getParentHash()); + output.write(baseline.getStateRoot()); + output.flush(); + byte[] body = bytes.toByteArray(); + output.write(Hashing.sha256().hashBytes(body).asBytes()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("In-memory common baseline encoding failed", impossible); + } + } + + private void publishBytes(Path target, byte[] encoded) throws IOException { + Path temporary = directory.resolve(target.getFileName() + ".tmp-" + UUID.randomUUID()); + try { + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + java.nio.ByteBuffer buffer = java.nio.ByteBuffer.wrap(encoded); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("Common checkpoint bootstrap requires atomic rename", unsupported); + } + syncDirectory(directory); + } finally { + Files.deleteIfExists(temporary); + } + } + + private static byte[] readDigest(DataInputStream input) throws IOException { + byte[] value = new byte[DIGEST_LENGTH]; + input.readFully(value); + return value; + } + + private static void syncDirectory(Path directory) throws IOException { + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java new file mode 100644 index 00000000000..32ad2c01789 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java @@ -0,0 +1,20 @@ +package org.tron.core.db2.core; + +import com.google.common.hash.Hashing; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** Stable identity for the first production common-checkpoint candidate. */ +public final class CommonCheckpointFormat { + + public static final String ID = "java-tron-state-archive-common-checkpoint-v1"; + private static final byte[] DIGEST = Hashing.sha256() + .hashString(ID, StandardCharsets.UTF_8).asBytes(); + + private CommonCheckpointFormat() { + } + + public static byte[] identity() { + return Arrays.copyOf(DIGEST, DIGEST.length); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index df584e4be03..c3bc4a3d72d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -106,6 +106,7 @@ public class SnapshotManager implements RevokingDatabase { private OldValueCollector oldValueCollector; private ArchiveRuntimeAttachment archiveRuntimeAttachment; private PathStateRuntimeAttachment pathStateRuntimeAttachment; + private CommonCheckpointRuntimeAttachment commonCheckpointRuntimeAttachment; private Long submittedArchiveHistoryEpoch; private BlockReverseDiffSink blockReverseDiffSink; @Getter @@ -366,10 +367,32 @@ public synchronized void installArchiveCollector(OldValueCollector collector, if (archiveRuntimeAttachment != null) { throw new IllegalStateException("Borrowed archive runtime is already attached"); } + if (commonCheckpointRuntimeAttachment != null) { + throw new IllegalStateException("Common checkpoint runtime is already attached"); + } oldValueCollector = Objects.requireNonNull(collector, "collector"); blockReverseDiffSink = Objects.requireNonNull(sink, "sink"); } + /** Installs Archive artifact capture without any legacy per-block or flush-time sink. */ + public synchronized void installCommonCheckpointArchiveCollector(OldValueCollector collector) { + ArchiveStoreScope.validate(dbs); + if (oldValueCollector != null || blockReverseDiffSink != null + || archiveRuntimeAttachment != null || commonCheckpointRuntimeAttachment != null) { + throw new IllegalStateException("Archive collaborators are already installed"); + } + oldValueCollector = Objects.requireNonNull(collector, "collector"); + } + + /** Clears a partially installed common-checkpoint collector during startup rollback. */ + public synchronized void clearCommonCheckpointArchiveCollector() { + if (commonCheckpointRuntimeAttachment != null || archiveRuntimeAttachment != null + || blockReverseDiffSink != null) { + throw new IllegalStateException("Cannot clear an active Archive persistence runtime"); + } + oldValueCollector = null; + } + /** Atomically installs one borrowed archive runtime bundle after store registration. */ public synchronized void attachArchiveRuntime(ArchiveRuntimeAttachment attachment) { ArchiveStoreScope.validate(dbs); @@ -377,6 +400,9 @@ public synchronized void attachArchiveRuntime(ArchiveRuntimeAttachment attachmen if (archiveRuntimeAttachment != null) { throw new IllegalStateException("Archive runtime is already attached"); } + if (commonCheckpointRuntimeAttachment != null) { + throw new IllegalStateException("Common checkpoint runtime is already attached"); + } if (oldValueCollector != null || blockReverseDiffSink != null) { throw new IllegalStateException("Legacy archive collaborators are already installed"); } @@ -413,6 +439,46 @@ public synchronized void attachPathStateRuntime(PathStateRuntimeAttachment attac pathStateRuntimeAttachment = candidate; } + /** + * Installs the exclusive three-authority persistence owner. Archive collection and PathState + * in-memory advancement remain attached separately, but their legacy durable callbacks are + * never used while this attachment is present. + */ + public synchronized void attachCommonCheckpointRuntime( + CommonCheckpointRuntimeAttachment attachment) { + ArchiveStoreScope.validate(dbs); + CommonCheckpointRuntimeAttachment candidate = Objects.requireNonNull(attachment, + "attachment"); + if (!candidate.isEnabled()) { + throw new IllegalArgumentException("Common checkpoint runtime must be enabled"); + } + if (commonCheckpointRuntimeAttachment != null) { + throw new IllegalStateException("Common checkpoint runtime is already attached"); + } + if (archiveRuntimeAttachment != null || blockReverseDiffSink != null) { + throw new IllegalStateException( + "Common checkpoint runtime is mutually exclusive with legacy Archive persistence"); + } + if (oldValueCollector == null || pathStateRuntimeAttachment == null + || !pathStateRuntimeAttachment.isCommonCheckpointOnly()) { + throw new IllegalStateException( + "Common checkpoint requires Archive capture and a checkpoint-only PathState runtime"); + } + commonCheckpointRuntimeAttachment = candidate; + } + + /** Detaches the exact Manager-owned common-checkpoint runtime without closing it. */ + public synchronized CommonCheckpointRuntimeAttachment detachCommonCheckpointRuntime( + CommonCheckpointRuntimeAttachment expected) { + CommonCheckpointRuntimeAttachment candidate = Objects.requireNonNull(expected, "expected"); + if (commonCheckpointRuntimeAttachment != candidate) { + throw new IllegalStateException("Cannot detach a missing or foreign common runtime"); + } + commonCheckpointRuntimeAttachment = null; + oldValueCollector = null; + return candidate; + } + /** Detaches the exact borrowed path-state runtime without closing its Manager-owned state. */ public synchronized PathStateRuntimeAttachment detachPathStateRuntime( PathStateRuntimeAttachment expected) { @@ -518,6 +584,13 @@ private synchronized Closeable prepareArchiveShutdown() { archiveReadableEpoch = -1; return null; } + if (commonCheckpointRuntimeAttachment != null) { + commonCheckpointRuntimeAttachment = null; + oldValueCollector = null; + blockReverseDiffSink = null; + archiveReadableEpoch = -1; + return null; + } return blockReverseDiffSink instanceof Closeable ? (Closeable) blockReverseDiffSink : null; } @@ -610,6 +683,20 @@ private synchronized void flush(boolean force) { if (force || shouldBeRefreshed()) { try { long start = System.currentTimeMillis(); + if (commonCheckpointRuntimeAttachment != null) { + if (flushCount <= 0) { + return; + } + try { + commonCheckpointRuntimeAttachment.checkpointAndRebase(flushCount); + } catch (IOException | RuntimeException failure) { + throw new TronDBException("Common checkpoint publication failed", failure); + } + flushCount = 0; + logger.info("Common checkpoint flush cost: {} ms.", + System.currentTimeMillis() - start); + return; + } BlockSnapshotMeta pathStateFlushTarget = pathStateFlushTarget(); ArchiveWalBinding archiveBinding = publishArchiveHistoryForFlush(); if (!isV2Open()) { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java index e6a29c88f58..47f0f5d1a93 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java @@ -16,13 +16,17 @@ import java.util.Set; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.core.CommonCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointBaseline; import org.tron.core.db2.core.CommonCheckpointPayload; import org.tron.core.db2.core.CommonCheckpointTarget; /** Next-format PathState participant for the common-checkpoint two-barrier protocol. */ public final class PathStateCheckpointMaterializer implements CommonCheckpointMaterializer { - static final String CURRENT_FILE = "CURRENT"; + public static final String CURRENT_FILE = "CURRENT"; + static final String LEGACY_BASELINE_FILE = "LEGACY_BASELINE"; + public static final String COMMON_MODE_FILE = "COMMON_CHECKPOINT_MODE"; + public static final String COMMON_BASELINE_HEAD_FILE = "COMMON_BASELINE_HEAD"; static final String MATERIALIZED_DIRECTORY = "checkpoint-materialized"; private static final int MAGIC = 0x50534354; // PSCT private static final short VERSION = 1; @@ -35,19 +39,32 @@ public final class PathStateCheckpointMaterializer implements CommonCheckpointMa private final Path directory; private final byte[] formatIdentity; private final FaultHook faultHook; + private final CommonCheckpointBaseline baseline; public PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, PathStateParticipantScope scope, byte[] formatIdentity) { - this(stores, scope, formatIdentity, (stage, storeId) -> { }); + this(stores, scope, formatIdentity, null, (stage, storeId) -> { }); + } + + public PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, + PathStateParticipantScope scope, byte[] formatIdentity, CommonCheckpointBaseline baseline) { + this(stores, scope, formatIdentity, baseline, (stage, storeId) -> { }); } PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, PathStateParticipantScope scope, byte[] formatIdentity, FaultHook faultHook) { + this(stores, scope, formatIdentity, null, faultHook); + } + + private PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, + PathStateParticipantScope scope, byte[] formatIdentity, CommonCheckpointBaseline baseline, + FaultHook faultHook) { this.stores = Objects.requireNonNull(stores, "stores"); this.scope = Objects.requireNonNull(scope, "scope"); this.directory = stores.getDirectory(); this.formatIdentity = digest(formatIdentity, "formatIdentity"); this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + this.baseline = baseline; } @Override @@ -55,6 +72,72 @@ public Authority authority() { return Authority.PATH_STATE; } + public static boolean isCommonModeAdmitted(Path directory, byte[] formatIdentity) + throws IOException { + Path mode = Objects.requireNonNull(directory, "directory").resolve(COMMON_MODE_FILE); + if (!Files.exists(mode, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + if (!Files.isRegularFile(mode, LinkOption.NOFOLLOW_LINKS) + || !Arrays.equals(Files.readAllBytes(mode), + digest(formatIdentity, "formatIdentity"))) { + throw new IOException("PathState common-mode marker differs"); + } + return true; + } + + /** Converts a newly rebuilt legacy pointer into a read-only baseline admission record. */ + public static void admitFreshBaseline(PathStatePhysicalStoreSet stores, + PathStateRootMetadata metadata, CommonCheckpointBaseline baseline) throws IOException { + Path directory = Objects.requireNonNull(stores, "stores").getDirectory(); + Path current = directory.resolve(CURRENT_FILE); + Path legacy = directory.resolve(LEGACY_BASELINE_FILE); + Path mode = directory.resolve(COMMON_MODE_FILE); + if (Files.exists(mode, LinkOption.NOFOLLOW_LINKS)) { + if (!Files.isRegularFile(mode, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("PathState common-mode marker is not a regular file"); + } + return; + } + if (!Files.isRegularFile(current, LinkOption.NOFOLLOW_LINKS)) { + if (Files.isRegularFile(legacy, LinkOption.NOFOLLOW_LINKS) + && Files.isRegularFile(directory.resolve(COMMON_BASELINE_HEAD_FILE), + LinkOption.NOFOLLOW_LINKS)) { + PathStateMetadataFile.publishImmutableBytes(mode, baseline.getFormatIdentity()); + return; + } + throw new IOException("PathState fresh common baseline requires one legacy CURRENT"); + } + if (Files.exists(legacy, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("PathState fresh common baseline has conflicting pointers"); + } + if (metadata.getBlockNumber() != baseline.getHead().getBlockNumber() + || !Arrays.equals(metadata.getBlockHash(), baseline.getHead().getBlockHash()) + || !Arrays.equals(metadata.getStateRoot(), baseline.getStateRoot())) { + throw new IOException("PathState fresh baseline identity differs"); + } + PathStateMetadataFile.publishImmutableBytes(directory.resolve(COMMON_BASELINE_HEAD_FILE), + metadata.encode()); + try { + Files.move(current, legacy, java.nio.file.StandardCopyOption.ATOMIC_MOVE); + } catch (java.nio.file.AtomicMoveNotSupportedException unsupported) { + throw new IOException("PathState common baseline requires atomic rename", unsupported); + } + PathStateMetadataFile.publishImmutableBytes(mode, baseline.getFormatIdentity()); + } + + /** Loads the exact next-format head currently published by PathState CURRENT. */ + public static PublishedHead loadPublishedHead(Path directory, + byte[] expectedFormatIdentity) throws IOException { + Marker marker = load(Objects.requireNonNull(directory, "directory").resolve(CURRENT_FILE)); + if (!Arrays.equals(marker.formatIdentity, + digest(expectedFormatIdentity, "expectedFormatIdentity"))) { + throw new IOException("PathState published target format identity differs"); + } + return new PublishedHead(marker.lastEpoch, marker.lastBlockNumber, marker.lastBlockHash, + marker.stateRoot, marker.payloadDigest); + } + @Override public synchronized Status inspect(CommonCheckpointTarget target) throws IOException { CommonCheckpointTarget admitted = requireTarget(target); @@ -67,6 +150,8 @@ public synchronized Status inspect(CommonCheckpointTarget target) throws IOExcep return Status.PUBLISHED; } requireParent(current, admitted); + } else if (baseline != null) { + baseline.requireParent(admitted, "PathState"); } Path materialized = materializedPath(admitted); if (!Files.exists(materialized, LinkOption.NOFOLLOW_LINKS)) { @@ -270,4 +355,43 @@ private Marker(byte[] encoded, byte[] formatIdentity, byte[] payloadDigest, long this.stateRoot = stateRoot; } } + + /** Minimal restart identity retained by the compact next-format CURRENT record. */ + public static final class PublishedHead { + + private final long epoch; + private final long blockNumber; + private final byte[] blockHash; + private final byte[] stateRoot; + private final byte[] payloadDigest; + + private PublishedHead(long epoch, long blockNumber, byte[] blockHash, byte[] stateRoot, + byte[] payloadDigest) { + this.epoch = epoch; + this.blockNumber = blockNumber; + this.blockHash = Arrays.copyOf(blockHash, blockHash.length); + this.stateRoot = Arrays.copyOf(stateRoot, stateRoot.length); + this.payloadDigest = Arrays.copyOf(payloadDigest, payloadDigest.length); + } + + public long getEpoch() { + return epoch; + } + + public long getBlockNumber() { + return blockNumber; + } + + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + public byte[] getStateRoot() { + return Arrays.copyOf(stateRoot, stateRoot.length); + } + + public byte[] getPayloadDigest() { + return Arrays.copyOf(payloadDigest, payloadDigest.length); + } + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java index d839e0af41d..c17ae632a9c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java @@ -15,7 +15,7 @@ */ public final class PathStateCommitmentCodec { - public static final int FORMAT_VERSION = 1; + public static final int FORMAT_VERSION = 2; public static final int ROOT_LENGTH = 32; private static final byte PRESENT_TAG = 1; @@ -37,9 +37,8 @@ public static byte[] storeLeafKey(int stableStoreId, byte[] physicalRawKey) { requireStoreId(stableStoreId); byte[] key = copy(physicalRawKey, "physicalRawKey"); ByteBuffer material = ByteBuffer.allocate(Short.BYTES + STORE_LEAF_KEY_DOMAIN.length - + Short.BYTES + Integer.BYTES + Integer.BYTES + key.length); + + Integer.BYTES + Integer.BYTES + key.length); putDomain(material, STORE_LEAF_KEY_DOMAIN); - material.putShort((short) FORMAT_VERSION); material.putInt(stableStoreId); material.putInt(key.length); material.put(key); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java index 62b39688064..d32194e0eee 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java @@ -1,6 +1,7 @@ package org.tron.core.db2.stateroot; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -15,13 +16,16 @@ import java.util.concurrent.atomic.AtomicLong; import lombok.extern.slf4j.Slf4j; import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.core.CommonCheckpointBaseline; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** - * Benchmark-only PathState head that reads one durable physical base and advances in memory. + * PathState head that reads one durable physical checkpoint and advances in memory. * *

No transition method writes F/N/M, INTENT, CURRENT, or a reverse journal. The durable base is - * intentionally unchanged until the common-checkpoint flush path is installed. + * changed only by the common-checkpoint materializer. The same implementation remains available + * to the explicitly configured volatile benchmark mode. */ @Slf4j(topic = "DB") public final class PathStatePhysicalOverlayHead implements PathStateHead { @@ -62,6 +66,100 @@ public static PathStatePhysicalOverlayHead open(Path directory, Engine engine, return open(directory, engine, limits, PathStatePhysicalStoreSet.STEADY_NODE_CACHE_BYTES); } + /** Opens a next-format published target without invoking legacy journal/CURRENT recovery. */ + public static PathStatePhysicalOverlayHead openCommonCheckpoint(Path directory, Engine engine, + PathStateLayerLimits limits, long residentNodeCacheBytes, int participantThreads, + int branchThreads, byte[] formatIdentity, BlockSnapshotMeta canonicalHead, P66Phase phase) + throws IOException { + requireThreadCount(participantThreads, "participantThreads"); + requireThreadCount(branchThreads, "branchThreads"); + PathStatePhysicalStoreSet opened = PathStatePhysicalStoreSet.openExisting(directory, + new PathStateCanonicalizer().participantScope(), engine, residentNodeCacheBytes); + try { + PathStateCheckpointMaterializer.PublishedHead current = + PathStateCheckpointMaterializer.loadPublishedHead(directory, formatIdentity); + PathStateRoot root = opened.createRoot(); + root.restoreStoredRoots(current.getStateRoot()); + PathStateRoot.Snapshot restored = root.snapshot(); + PathStateRootMetadata metadata = PathStateRootMetadata.base(current.getBlockNumber(), + current.getBlockHash(), canonicalHead.getParentHash(), canonicalHead.getTimestamp(), + phase, opened.getFormatDigest(), current.getStateRoot(), current.getPayloadDigest()); + return new PathStatePhysicalOverlayHead(opened, metadata, restored, + Objects.requireNonNull(limits, "limits").getMaxLayers(), participantThreads, + branchThreads); + } catch (IOException | RuntimeException failure) { + try { + opened.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } + } + + /** Opens the immutable fresh baseline after its legacy pointer has been retired. */ + public static PathStatePhysicalOverlayHead openCommonBaseline(Path directory, Engine engine, + PathStateLayerLimits limits, long residentNodeCacheBytes, int participantThreads, + int branchThreads) throws IOException { + requireThreadCount(participantThreads, "participantThreads"); + requireThreadCount(branchThreads, "branchThreads"); + PathStatePhysicalStoreSet opened = PathStatePhysicalStoreSet.openExisting(directory, + new PathStateCanonicalizer().participantScope(), engine, residentNodeCacheBytes); + try { + PathStateRootMetadata current = PathStateRootMetadata.decode(Files.readAllBytes( + directory.resolve(PathStateCheckpointMaterializer.COMMON_BASELINE_HEAD_FILE))); + PathStateRoot root = opened.createRoot(); + root.restoreStoredRoots(current.getStateRoot()); + return new PathStatePhysicalOverlayHead(opened, current, root.snapshot(), + Objects.requireNonNull(limits, "limits").getMaxLayers(), participantThreads, + branchThreads); + } catch (IOException | RuntimeException failure) { + try { + opened.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } + } + + /** Refreshes the in-memory head after startup redo has published a newer common target. */ + public synchronized void synchronizePublishedCheckpoint(byte[] formatIdentity, + BlockSnapshotMeta canonicalHead, P66Phase phase) throws IOException { + requireHealthy(); + PathStateCheckpointMaterializer.PublishedHead current = + PathStateCheckpointMaterializer.loadPublishedHead(stores.getDirectory(), formatIdentity); + if (current.getEpoch() != canonicalHead.getEpoch() + || current.getBlockNumber() != canonicalHead.getBlockNumber() + || !Arrays.equals(current.getBlockHash(), canonicalHead.getBlockHash())) { + throw new IOException("PathState common CURRENT differs after startup redo"); + } + PathStateRoot restored = new PathStateRoot(scope, + participant -> stores.participant(participant.getDbName()).nodeStore(), + stores.superStore().nodeStore()); + restored.restoreStoredRoots(current.getStateRoot()); + snapshot = restored.snapshot(); + head = PathStateRootMetadata.base(current.getBlockNumber(), current.getBlockHash(), + canonicalHead.getParentHash(), canonicalHead.getTimestamp(), phase, formatDigest, + current.getStateRoot(), current.getPayloadDigest()); + history.clear(); + pending = null; + } + + /** Creates the PathState authority over the same stores used by this in-memory head. */ + public synchronized PathStateCheckpointMaterializer checkpointMaterializer( + byte[] formatIdentity, CommonCheckpointBaseline baseline) throws IOException { + requireHealthy(); + return new PathStateCheckpointMaterializer(stores, scope, formatIdentity, baseline); + } + + /** Retires this freshly rebuilt legacy pointer before the first common checkpoint is enabled. */ + public synchronized void admitFreshCommonBaseline(CommonCheckpointBaseline baseline) + throws IOException { + requireHealthy(); + PathStateCheckpointMaterializer.admitFreshBaseline(stores, head, baseline); + } + /** Opens a benchmark overlay with an explicit shared resident-node cache budget. */ public static PathStatePhysicalOverlayHead open(Path directory, Engine engine, PathStateLayerLimits limits, long residentNodeCacheBytes) throws IOException { @@ -84,7 +182,7 @@ public static PathStatePhysicalOverlayHead open(Path directory, Engine engine, root.restoreStoredRoots(current.getStateRoot()); PathStateRoot.Snapshot restored = root.snapshot(); if (!Arrays.equals(restored.getStateRoot(), current.getStateRoot())) { - throw new IOException("path-state benchmark overlay root mismatch"); + throw new IOException("path-state overlay root mismatch"); } return new PathStatePhysicalOverlayHead(opened, current, restored, Objects.requireNonNull(limits, "limits").getMaxLayers(), participantThreads, @@ -105,7 +203,7 @@ public synchronized PathStateRootMetadata advance(PathStateBlockTransition trans requireHealthy(); PathStateBlockTransition admitted = Objects.requireNonNull(transition, "transition"); if (pending == null || pending.transition != admitted) { - throw new IOException("path-state benchmark publication differs from prepared transition"); + throw new IOException("path-state overlay publication differs from prepared transition"); } history.add(new HeadState(head, snapshot)); while (history.size() > maxHistory) { @@ -169,7 +267,7 @@ public synchronized PathStateRootMetadata rewindTo(long blockNumber, byte[] bloc return copy(head); } } - throw new IOException("path-state benchmark overlay ancestor is outside memory history"); + throw new IOException("path-state overlay ancestor is outside memory history"); } @Override @@ -192,7 +290,7 @@ public synchronized PathStateSnapshotDelta prepareSnapshotDelta(BlockSnapshotMet PathStateBlockTransition transition) throws IOException { requireHealthy(); if (pending != null) { - throw new IOException("path-state benchmark transition is already prepared"); + throw new IOException("path-state overlay transition is already prepared"); } pending = prepare(Objects.requireNonNull(meta, "meta"), Objects.requireNonNull(transition, "transition")); @@ -300,10 +398,10 @@ private void requireChild(PathStateBlockTransition transition) throws IOExceptio private void requireHealthy() throws IOException { if (closed) { - throw new IOException("path-state benchmark overlay is closed"); + throw new IOException("path-state overlay is closed"); } if (failed) { - throw new IOException("path-state benchmark overlay failed closed"); + throw new IOException("path-state overlay failed closed"); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java index aceae6cd33d..bd0534bb3dc 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java @@ -21,6 +21,7 @@ public final class PathStateRuntimeAttachment { private final TransitionPreviewer previewer; private final SnapshotDeltaPreparer snapshotDeltaPreparer; private final boolean deferredCapture; + private final boolean commonCheckpointOnly; private final BlockingQueue deferredQueue; private final Thread deferredWorker; private Throwable failure; @@ -62,18 +63,34 @@ public static PathStateRuntimeAttachment deferred(PathStateTransitionCollector c TransitionSink sink, BaseFlushSink baseFlushSink, TransitionPreviewer previewer, SnapshotDeltaPreparer snapshotDeltaPreparer) { return new PathStateRuntimeAttachment(collector, sink, baseFlushSink, previewer, - snapshotDeltaPreparer, true); + snapshotDeltaPreparer, true, false); + } + + /** Creates the production in-memory head used exclusively by the common checkpoint owner. */ + public static PathStateRuntimeAttachment commonCheckpoint(PathStateTransitionCollector collector, + TransitionSink sink, TransitionPreviewer previewer, + SnapshotDeltaPreparer snapshotDeltaPreparer) { + return new PathStateRuntimeAttachment(collector, sink, (blockNumber, blockHash) -> { }, + previewer, snapshotDeltaPreparer, false, true); } private PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink, BaseFlushSink baseFlushSink, TransitionPreviewer previewer, SnapshotDeltaPreparer snapshotDeltaPreparer, boolean deferredCapture) { + this(collector, sink, baseFlushSink, previewer, snapshotDeltaPreparer, deferredCapture, false); + } + + private PathStateRuntimeAttachment(PathStateTransitionCollector collector, TransitionSink sink, + BaseFlushSink baseFlushSink, TransitionPreviewer previewer, + SnapshotDeltaPreparer snapshotDeltaPreparer, boolean deferredCapture, + boolean commonCheckpointOnly) { this.collector = Objects.requireNonNull(collector, "collector"); this.sink = Objects.requireNonNull(sink, "sink"); this.baseFlushSink = Objects.requireNonNull(baseFlushSink, "baseFlushSink"); this.previewer = previewer; this.snapshotDeltaPreparer = snapshotDeltaPreparer; this.deferredCapture = deferredCapture; + this.commonCheckpointOnly = commonCheckpointOnly; deferredQueue = deferredCapture ? new ArrayBlockingQueue<>(64) : null; deferredWorker = deferredCapture ? new Thread(this::runDeferred, "path-state-deferred-capture") : null; @@ -83,6 +100,10 @@ private PathStateRuntimeAttachment(PathStateTransitionCollector collector, Trans } } + public boolean isCommonCheckpointOnly() { + return commonCheckpointOnly; + } + /** Computes producer metadata without observing, publishing, or failing this runtime. */ public synchronized byte[] preview(BlockChangeView view) { if (failure != null || previewer == null || status().getState() != State.READY) { diff --git a/common/src/main/java/org/tron/core/config/args/Storage.java b/common/src/main/java/org/tron/core/config/args/Storage.java index 54d8f8bd618..803f571e51c 100644 --- a/common/src/main/java/org/tron/core/config/args/Storage.java +++ b/common/src/main/java/org/tron/core/config/args/Storage.java @@ -101,6 +101,14 @@ public class Storage { @Setter private int stateArchiveQueueCapacity; + @Getter + @Setter + private boolean commonCheckpointEnabled; + + @Getter + @Setter + private String commonCheckpointDirectory; + @Getter @Setter private boolean pathStateRootEnabled; diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index ce381fbb631..45a5669fca3 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -28,6 +28,7 @@ public class StorageConfig { private CheckpointConfig checkpoint = new CheckpointConfig(); private SnapshotConfig snapshot = new SnapshotConfig(); private StateArchiveConfig stateArchive = new StateArchiveConfig(); + private CommonCheckpointConfig commonCheckpoint = new CommonCheckpointConfig(); private PathStateRootConfig pathStateRoot = new PathStateRootConfig(); private TxCacheConfig txCache = new TxCacheConfig(); // ConfigBeanFactory requires all bean fields present per item, so we parse manually. @@ -167,6 +168,20 @@ void postProcess() { } } + @Getter + @Setter + public static class CommonCheckpointConfig { + + private boolean enabled = false; + private String directory = "common-checkpoint"; + + void postProcess() { + if (directory == null || directory.trim().isEmpty()) { + throw new IllegalArgumentException("commonCheckpoint.directory must not be empty"); + } + } + } + @Getter @Setter public static class PathStateRootConfig { @@ -174,7 +189,7 @@ public static class PathStateRootConfig { private boolean enabled = false; private String mode = "shadow"; private String directory = "path-state-root"; - private int formatVersion = 1; + private int formatVersion = 2; private int reversibleLayerLimit = 128; private long reversibleLayerBytes = 2147483648L; private long writeBufferBytes = 268435456L; @@ -193,8 +208,8 @@ void postProcess() { if (directory == null || directory.trim().isEmpty()) { throw new IllegalArgumentException("pathStateRoot.directory must not be empty"); } - if (formatVersion != 1) { - throw new IllegalArgumentException("pathStateRoot.formatVersion must be 1"); + if (formatVersion != 2) { + throw new IllegalArgumentException("pathStateRoot.formatVersion must be 2"); } if (reversibleLayerLimit <= 0 || reversibleLayerBytes <= 0 || writeBufferBytes <= 0 || nodeCacheBytes <= 0) { @@ -266,7 +281,19 @@ public static StorageConfig fromConfig(Config config) { sc.dbSettings.postProcess(); sc.snapshot.postProcess(); sc.stateArchive.postProcess(); + sc.commonCheckpoint.postProcess(); sc.pathStateRoot.postProcess(); + if (sc.commonCheckpoint.enabled + && (!sc.stateArchive.enabled || !sc.pathStateRoot.enabled)) { + throw new IllegalArgumentException( + "commonCheckpoint.enabled requires stateArchive.enabled and pathStateRoot.enabled"); + } + if (sc.commonCheckpoint.enabled + && (sc.pathStateRoot.volatileSnapshotBenchmark + || sc.pathStateRoot.asyncPrepareBenchmark)) { + throw new IllegalArgumentException( + "commonCheckpoint.enabled is mutually exclusive with PathState benchmark modes"); + } sc.txCache.postProcess(); return sc; } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 178e46465ab..6e5d17a1319 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -137,12 +137,15 @@ storage { stateArchive.directory = "state-archive" stateArchive.maxSegmentSize = 1073741824 # 1 GiB stateArchive.queueCapacity = 256 + # Next-format three-authority checkpoint. Requires fresh Archive and PathState directories. + commonCheckpoint.enabled = false + commonCheckpoint.directory = "common-checkpoint" # Experimental current-only, non-consensus path state root. Disabled by default. pathStateRoot.enabled = false pathStateRoot.mode = "shadow" pathStateRoot.directory = "path-state-root" - pathStateRoot.formatVersion = 1 + pathStateRoot.formatVersion = 2 pathStateRoot.reversibleLayerLimit = 128 pathStateRoot.reversibleLayerBytes = 2147483648 # 2 GiB pathStateRoot.writeBufferBytes = 268435456 # 256 MiB diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java index 4e934557ba9..19c3fec7716 100644 --- a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -78,13 +78,41 @@ public void testStateArchiveRejectsSmallSegments() { StorageConfig.fromConfig(withRef("storage.stateArchive.maxSegmentSize = 1024")); } + @Test + public void testCommonCheckpointDefaultsAndAdmission() { + StorageConfig defaults = StorageConfig.fromConfig(withRef()); + assertFalse(defaults.getCommonCheckpoint().isEnabled()); + assertEquals("common-checkpoint", defaults.getCommonCheckpoint().getDirectory()); + + StorageConfig configured = StorageConfig.fromConfig(withRef( + "storage.stateArchive.enabled = true\n" + + "storage.pathStateRoot.enabled = true\n" + + "storage.commonCheckpoint { enabled = true, directory = common-test }")); + assertTrue(configured.getCommonCheckpoint().isEnabled()); + assertEquals("common-test", configured.getCommonCheckpoint().getDirectory()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCommonCheckpointRequiresBothAuthorities() { + StorageConfig.fromConfig(withRef("storage.commonCheckpoint.enabled = true")); + } + + @Test(expected = IllegalArgumentException.class) + public void testCommonCheckpointRejectsBenchmarkMode() { + StorageConfig.fromConfig(withRef( + "storage.stateArchive.enabled = true\n" + + "storage.pathStateRoot.enabled = true\n" + + "storage.pathStateRoot.volatileSnapshotBenchmark = true\n" + + "storage.commonCheckpoint.enabled = true")); + } + @Test public void testPathStateRootDefaultsAndOverrides() { StorageConfig defaults = StorageConfig.fromConfig(withRef()); assertFalse(defaults.getPathStateRoot().isEnabled()); assertEquals("shadow", defaults.getPathStateRoot().getMode()); assertEquals("path-state-root", defaults.getPathStateRoot().getDirectory()); - assertEquals(1, defaults.getPathStateRoot().getFormatVersion()); + assertEquals(2, defaults.getPathStateRoot().getFormatVersion()); assertEquals(128, defaults.getPathStateRoot().getReversibleLayerLimit()); assertEquals(2147483648L, defaults.getPathStateRoot().getReversibleLayerBytes()); assertEquals(268435456L, defaults.getPathStateRoot().getWriteBufferBytes()); @@ -98,7 +126,7 @@ public void testPathStateRootDefaultsAndOverrides() { StorageConfig configured = StorageConfig.fromConfig(withRef( "storage.pathStateRoot { enabled = true, mode = shadow, directory = root-test, " - + "formatVersion = 1, reversibleLayerLimit = 8, reversibleLayerBytes = 4096, " + + "formatVersion = 2, reversibleLayerLimit = 8, reversibleLayerBytes = 4096, " + "writeBufferBytes = 1024, nodeCacheBytes = 2048, participantThreads = 2, " + "branchThreads = 3, rebuildFromGenesis = false, " + "verifyEveryBlock = true, volatileSnapshotBenchmark = true, " diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 429f309ead8..b3dd250d6c5 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -221,6 +221,8 @@ private static void applyStorageConfig(StorageConfig sc) { PARAMETER.storage.setStateArchiveDirectory(sc.getStateArchive().getDirectory()); PARAMETER.storage.setStateArchiveMaxSegmentSize(sc.getStateArchive().getMaxSegmentSize()); PARAMETER.storage.setStateArchiveQueueCapacity(sc.getStateArchive().getQueueCapacity()); + PARAMETER.storage.setCommonCheckpointEnabled(sc.getCommonCheckpoint().isEnabled()); + PARAMETER.storage.setCommonCheckpointDirectory(sc.getCommonCheckpoint().getDirectory()); PARAMETER.storage.setPathStateRootEnabled(sc.getPathStateRoot().isEnabled()); PARAMETER.storage.setPathStateRootMode(sc.getPathStateRoot().getMode()); PARAMETER.storage.setPathStateRootDirectory(sc.getPathStateRoot().getDirectory()); diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 57bca92066c..8db8d20c94f 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -15,6 +15,8 @@ import com.google.common.primitives.Longs; import com.google.protobuf.ByteString; import io.prometheus.client.Histogram; +import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -119,21 +121,36 @@ import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator.Result; import org.tron.core.db2.archive.ArchiveFormatAdmissionValidator.Status; import org.tron.core.db2.archive.ArchiveHistoryWriter; +import org.tron.core.db2.archive.ArchivePointSnapshot; import org.tron.core.db2.archive.ArchiveStoreScope; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.archive.HistoricalAccountAssetBalanceResolver; import org.tron.core.db2.archive.HistoricalAccountAssetPrefixResolver; import org.tron.core.db2.archive.HistoricalAccountBalanceReader; import org.tron.core.db2.archive.HistoricalQuerySession; +import org.tron.core.db2.archive.LatestStateGenerationAdapter; +import org.tron.core.db2.archive.LatestStateGenerationCoordinatorFactory; import org.tron.core.db2.archive.OldValue; import org.tron.core.db2.archive.SnapshotOldValueCollector; import org.tron.core.db2.archive.SnapshotPathStateTransitionCollector; +import org.tron.core.db2.archive.StateArchiveCheckpointMaterializer; +import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; import org.tron.core.db2.archive.StateArchiveRuntimeOwner; import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.ChainbaseCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointBaseline; +import org.tron.core.db2.core.CommonCheckpointBaselineFile; +import org.tron.core.db2.core.CommonCheckpointFile; +import org.tron.core.db2.core.CommonCheckpointFormat; +import org.tron.core.db2.core.CommonCheckpointRedoCoordinator; +import org.tron.core.db2.core.CommonCheckpointRuntime; +import org.tron.core.db2.core.CommonCheckpointRuntimeAttachment; +import org.tron.core.db2.core.CommonCheckpointRuntimeOwner; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.db2.stateroot.PathStateBlockTransition; import org.tron.core.db2.stateroot.PathStateCanonicalizer; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateCheckpointMaterializer; import org.tron.core.db2.stateroot.PathStateHead; import org.tron.core.db2.stateroot.PathStateLayerLimits; import org.tron.core.db2.stateroot.PathStateNativeSnapshotSource; @@ -228,6 +245,8 @@ public class Manager { private PathStateHead pathStateSnapshotHead; @Getter private PathStateRuntimeAttachment pathStateRuntime; + @Getter + private CommonCheckpointRuntimeAttachment commonCheckpointRuntime; private StateArchiveRuntimeOwner.ServingIndexFaultHook stateArchiveServingIndexFaultHook = stage -> { }; private StateArchiveRuntimeOwner.ReadableStateFaultHook stateArchiveReadableStateFaultHook = @@ -610,8 +629,13 @@ public void init() { // init liteFullNode initLiteNode(); - initStateArchive(); - initPathStateRoot(); + requireSingleEngineArchiveMode(Args.getInstance().getStorage()); + if (Args.getInstance().getStorage().isCommonCheckpointEnabled()) { + initCommonCheckpoint(); + } else { + initStateArchive(); + initPathStateRoot(); + } long headNum = chainBaseManager.getDynamicPropertiesStore().getLatestBlockHeaderNumber(); logger.info("Current headNum is: {}.", headNum); @@ -654,6 +678,15 @@ public void init() { maxFlushCount = CommonParameter.getInstance().getStorage().getMaxFlushCount(); } + static void requireSingleEngineArchiveMode(org.tron.core.config.args.Storage storage) { + org.tron.core.config.args.Storage admitted = Objects.requireNonNull(storage, "storage"); + if (admitted.isStateArchiveEnabled() && !admitted.isCommonCheckpointEnabled() + && !"ROCKSDB".equalsIgnoreCase(admitted.getDbEngine())) { + throw new IllegalStateException("LevelDB State Archive requires common checkpoint; " + + "legacy serving generations are RocksDB-only and mixed-engine startup is forbidden"); + } + } + private void initStateArchive() { org.tron.core.config.args.Storage storage = Args.getInstance().getStorage(); Path archiveDirectory = Paths.get(Args.getInstance().getOutputDirectory(), @@ -736,6 +769,265 @@ private void initStateArchive() { } } + /** Installs the fresh-format three-authority runtime before block processing is enabled. */ + private void initCommonCheckpoint() { + if (!(revokingStore instanceof SnapshotManager)) { + throw new IllegalStateException("Common checkpoint requires SnapshotManager"); + } + org.tron.core.config.args.Storage storage = Args.getInstance().getStorage(); + SnapshotManager snapshots = (SnapshotManager) revokingStore; + Path pathDirectory = Paths.get(Args.getInstance().getOutputDirectory(), + storage.getPathStateRootDirectory()).normalize(); + Path archiveDirectory = Paths.get(Args.getInstance().getOutputDirectory(), + storage.getStateArchiveDirectory()).normalize(); + Path checkpointDirectory = Paths.get(Args.getInstance().getOutputDirectory(), + storage.getCommonCheckpointDirectory()).normalize(); + byte[] formatIdentity = CommonCheckpointFormat.identity(); + PathStatePhysicalOverlayHead pathOwner = null; + CommonCheckpointRuntimeAttachment attachment = null; + try { + PathStateStoreManifest.Engine engine = PathStateStoreManifest.Engine.valueOf( + storage.getDbEngine()); + boolean pathExisted = Files.exists(pathDirectory, LinkOption.NOFOLLOW_LINKS); + boolean modeAdmitted = pathExisted + && PathStateCheckpointMaterializer.isCommonModeAdmitted(pathDirectory, formatIdentity); + CommonCheckpointBaselineFile baselineFile = + new CommonCheckpointBaselineFile(checkpointDirectory); + boolean baselineExists = Files.isRegularFile( + checkpointDirectory.resolve(CommonCheckpointBaselineFile.FILE_NAME), + LinkOption.NOFOLLOW_LINKS); + if (!baselineExists) { + requireEmptyOrMissing(archiveDirectory, "State Archive"); + if (!pathExisted) { + requireEmptyOrMissing(checkpointDirectory, "common checkpoint"); + baselineFile.beginBootstrap(formatIdentity); + } else if (!baselineFile.hasBootstrapIntent(formatIdentity)) { + throw new IllegalStateException( + "Common checkpoint refuses an existing legacy PathState directory"); + } + } + if (!pathExisted) { + rebuildPathStateRoot(snapshots, pathDirectory, engine); + } else if (!modeAdmitted && !Files.isRegularFile( + pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), + LinkOption.NOFOLLOW_LINKS) + && !Files.isRegularFile(pathDirectory.resolve( + PathStateCheckpointMaterializer.COMMON_BASELINE_HEAD_FILE), + LinkOption.NOFOLLOW_LINKS)) { + rebuildPathStateRoot(snapshots, pathDirectory, engine); + } + + PathStateLayerLimits limits = new PathStateLayerLimits( + storage.getPathStateRootReversibleLayerLimit(), + storage.getPathStateRootReversibleLayerBytes()); + BlockSnapshotMeta canonical = currentCanonicalBlockMeta(); + P66Phase phase = currentPathStatePhase(); + if (modeAdmitted && Files.isRegularFile( + pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), + LinkOption.NOFOLLOW_LINKS)) { + pathOwner = PathStatePhysicalOverlayHead.openCommonCheckpoint(pathDirectory, engine, + limits, storage.getPathStateRootNodeCacheBytes(), + storage.getPathStateRootParticipantThreads(), storage.getPathStateRootBranchThreads(), + formatIdentity, canonical, phase); + } else { + pathOwner = modeAdmitted || Files.isRegularFile(pathDirectory.resolve( + PathStateCheckpointMaterializer.COMMON_BASELINE_HEAD_FILE), + LinkOption.NOFOLLOW_LINKS) + ? PathStatePhysicalOverlayHead.openCommonBaseline(pathDirectory, engine, limits, + storage.getPathStateRootNodeCacheBytes(), + storage.getPathStateRootParticipantThreads(), + storage.getPathStateRootBranchThreads()) + : PathStatePhysicalOverlayHead.open(pathDirectory, engine, limits, + storage.getPathStateRootNodeCacheBytes(), + storage.getPathStateRootParticipantThreads(), + storage.getPathStateRootBranchThreads()); + } + + PathStateRootMetadata initialHead = pathOwner.getHead(); + CommonCheckpointBaseline supplied = new CommonCheckpointBaseline(formatIdentity, + canonical, initialHead.getStateRoot()); + CommonCheckpointBaseline baseline = baselineExists ? baselineFile.load() + : baselineFile.openOrCreate(supplied); + if (!Arrays.equals(baseline.getFormatIdentity(), formatIdentity)) { + throw new IllegalStateException("Common checkpoint baseline format differs"); + } + if (!modeAdmitted) { + if (!baseline.getHead().equals(canonical) + || !Arrays.equals(baseline.getStateRoot(), initialHead.getStateRoot())) { + throw new IllegalStateException( + "Common checkpoint baseline differs from fresh PathState head"); + } + pathOwner.admitFreshCommonBaseline(baseline); + baselineFile.retireBootstrapIntent(); + } + + java.util.Map + supplementalStores = commonCheckpointSupplementalStores(snapshots); + LatestStateGenerationAdapter latest = LatestStateGenerationCoordinatorFactory.createAdapter( + snapshots, supplementalStores); + PathStateCheckpointMaterializer pathMaterializer = pathOwner.checkpointMaterializer( + formatIdentity, baseline); + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( + new CommonCheckpointFile(checkpointDirectory), + new ChainbaseCheckpointMaterializer(checkpointDirectory, formatIdentity, + snapshots.getDbs(), baseline), + pathMaterializer, + new StateArchiveCheckpointMaterializer(archiveDirectory, formatIdentity, baseline, + engine)); + PathStatePhysicalOverlayHead admittedOwner = pathOwner; + attachment = CommonCheckpointRuntimeAttachment.open(true, + () -> new CommonCheckpointRuntime(new CommonCheckpointRuntimeOwner(coordinator), + snapshots.getDbs(), archiveDirectory, formatIdentity, latest::pin)); + + canonical = currentCanonicalBlockMeta(); + if (Files.isRegularFile(pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), + LinkOption.NOFOLLOW_LINKS) + && (initialHead.getBlockNumber() != canonical.getBlockNumber() + || !Arrays.equals(initialHead.getBlockHash(), canonical.getBlockHash()))) { + admittedOwner.synchronizePublishedCheckpoint(formatIdentity, canonical, + currentPathStatePhase()); + } + if (Files.isRegularFile(pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), + LinkOption.NOFOLLOW_LINKS)) { + requireCommonPublishedAuthorities(checkpointDirectory, archiveDirectory, pathDirectory, + formatIdentity, engine); + } + PathStateRootMetadata recovered = admittedOwner.getHead(); + if (recovered.getBlockNumber() != canonical.getBlockNumber() + || !Arrays.equals(recovered.getBlockHash(), canonical.getBlockHash())) { + throw new IllegalStateException( + "Common checkpoint PathState head differs from recovered Chainbase head"); + } + + snapshots.installCommonCheckpointArchiveCollector(commonCheckpointArchiveCollector()); + pathStateSnapshotHead = admittedOwner; + attachPathStateBlockFinalRuntime(); + snapshots.attachCommonCheckpointRuntime(attachment); + commonCheckpointRuntime = attachment; + pathOwner = null; + attachment = null; + logger.info("Common checkpoint runtime attached: checkpoint={}, archive={}, path={}, " + + "head={}, format={}", checkpointDirectory, archiveDirectory, pathDirectory, + canonical.getBlockNumber(), CommonCheckpointFormat.ID); + } catch (java.io.IOException | BadItemException | ItemNotFoundException + | RuntimeException failure) { + if (pathStateRuntime != null) { + try { + snapshots.detachPathStateRuntime(pathStateRuntime); + pathStateRuntime.close(); + } catch (java.io.IOException | RuntimeException closeFailure) { + failure.addSuppressed(closeFailure); + } finally { + pathStateRuntime = null; + } + } + pathStateSnapshotHead = null; + try { + snapshots.clearCommonCheckpointArchiveCollector(); + } catch (RuntimeException cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + if (attachment != null) { + attachment.close(); + } + if (pathOwner != null) { + try { + pathOwner.close(); + } catch (java.io.IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + throw new IllegalStateException("Failed to recover common checkpoint startup", failure); + } + } + + private BlockSnapshotMeta currentCanonicalBlockMeta() + throws BadItemException, ItemNotFoundException { + long number = getDynamicPropertiesStore().getLatestBlockHeaderNumber(); + BlockCapsule block = chainBaseManager.getBlockByNum(number); + return BlockSnapshotMeta.forBlock(number, + getDynamicPropertiesStore().getLatestBlockHeaderHash().getBytes(), + block.getParentHash().getBytes(), block.getTimeStamp()); + } + + private P66Phase currentPathStatePhase() { + long value = getDynamicPropertiesStore().getAllowAccountAssetOptimizationFromRoot(); + if (value != 0L && value != 1L) { + throw new IllegalStateException("Common checkpoint P66 phase is invalid"); + } + return value == 0L ? P66Phase.P66_OFF : P66Phase.P66_ON; + } + + private static void requireCommonPublishedAuthorities(Path checkpointDirectory, + Path archiveDirectory, Path pathDirectory, byte[] formatIdentity, + PathStateStoreManifest.Engine engine) throws java.io.IOException { + ChainbaseCheckpointMaterializer.PublishedHead chain = + ChainbaseCheckpointMaterializer.loadPublishedHead(checkpointDirectory, formatIdentity); + PathStateCheckpointMaterializer.PublishedHead path = + PathStateCheckpointMaterializer.loadPublishedHead(pathDirectory, formatIdentity); + org.tron.core.db2.core.CommonCheckpointTarget archive = + StateArchiveCheckpointMaterializer.loadPublishedTarget(archiveDirectory, formatIdentity, + engine); + BlockSnapshotMeta last = archive.getLastBlock(); + if (chain.getEpoch() != last.getEpoch() || path.getEpoch() != last.getEpoch() + || chain.getBlockNumber() != last.getBlockNumber() + || path.getBlockNumber() != last.getBlockNumber() + || !Arrays.equals(chain.getBlockHash(), last.getBlockHash()) + || !Arrays.equals(path.getBlockHash(), last.getBlockHash()) + || !Arrays.equals(chain.getPayloadDigest(), archive.getPayloadDigest()) + || !Arrays.equals(path.getPayloadDigest(), archive.getPayloadDigest()) + || !Arrays.equals(chain.getStateRoot(), archive.getStateRoot()) + || !Arrays.equals(path.getStateRoot(), archive.getStateRoot())) { + throw new java.io.IOException("Common checkpoint published authorities differ"); + } + } + + private static void requireEmptyOrMissing(Path directory, String label) + throws java.io.IOException { + if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + return; + } + if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + throw new java.io.IOException(label + " path is not a directory"); + } + try (java.util.stream.Stream entries = Files.list(directory)) { + if (entries.findAny().isPresent()) { + throw new java.io.IOException(label + " fresh directory is not empty"); + } + } + } + + private java.util.Map + commonCheckpointSupplementalStores(SnapshotManager snapshots) { + if (snapshots.getDbs().stream().anyMatch(database -> + AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(database.getDbName()))) { + return java.util.Collections.emptyMap(); + } + AccountAssetStore accountAssetStore = chainBaseManager.getAccountAssetStore(); + if (accountAssetStore == null) { + throw new IllegalStateException("Common checkpoint requires account-asset Store"); + } + return java.util.Collections.singletonMap(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + LatestStateGenerationAdapter.fromDataSource(AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, + accountAssetStore.getDbSource())); + } + + private SnapshotOldValueCollector commonCheckpointArchiveCollector() { + SnapshotManager snapshots = (SnapshotManager) revokingStore; + if (snapshots.getDbs().stream().anyMatch(database -> + AccountAssetArchiveProjector.ACCOUNT_ASSET_DB.equals(database.getDbName()))) { + return new SnapshotOldValueCollector(); + } + AccountAssetStore accountAssetStore = chainBaseManager.getAccountAssetStore(); + if (accountAssetStore == null) { + throw new IllegalStateException("Common checkpoint requires account-asset Store"); + } + return new SnapshotOldValueCollector(new AccountAssetArchiveProjector(), + accountAssetStore::prefixQuery, + SnapshotOldValueCollector::resolveTargetAssetOptimization); + } + + private void initPathStateRoot() { org.tron.core.config.args.Storage storage = Args.getInstance().getStorage(); if (!storage.isPathStateRootEnabled()) { @@ -820,8 +1112,12 @@ private void attachPathStateBlockFinalRuntime() throws java.io.IOException { } SnapshotPathStateTransitionCollector collector = new SnapshotPathStateTransitionCollector( accountAssetStore::prefixQuery, this::scanPathStateActivationAccounts); - PathStateRuntimeAttachment attachment = Args.getInstance().getStorage() - .isPathStateRootAsyncPrepareBenchmark() + org.tron.core.config.args.Storage storage = Args.getInstance().getStorage(); + PathStateRuntimeAttachment attachment = storage.isCommonCheckpointEnabled() + ? PathStateRuntimeAttachment.commonCheckpoint(collector, this::advancePathStateRoot, + transition -> pathStateSnapshotHead.preview(transition), + (meta, transition) -> pathStateSnapshotHead.prepareSnapshotDelta(meta, transition)) + : storage.isPathStateRootAsyncPrepareBenchmark() ? PathStateRuntimeAttachment.deferred(collector, this::advancePathStateRoot, this::flushPathStateBaseThrough, transition -> pathStateSnapshotHead.preview(transition), @@ -937,6 +1233,15 @@ private SnapshotIdentity readPathStateSnapshotIdentity() throws java.io.IOExcept public HistoricalAccountBalanceReader.Result getArchiveAccountBalance(long blockNumber, byte[] address) throws ItemNotFoundException, BadItemException { + CommonCheckpointRuntimeAttachment common = commonCheckpointRuntime; + if (common != null) { + try (StateArchiveCheckpointReadSnapshot snapshot = common.pinPoint(blockNumber)) { + return HistoricalAccountBalanceReader.read(snapshot, address); + } catch (java.io.IOException failure) { + throw new org.tron.core.db2.archive.ArchivePersistenceException( + "Failed to read common-checkpoint historical account snapshot", failure); + } + } StateArchiveRuntimeOwner runtime = stateArchiveRuntime; if (runtime == null) { throw new IllegalStateException("Experimental state archive is disabled"); @@ -951,7 +1256,7 @@ public HistoricalAccountBalanceReader.Result getArchiveAccountBalance(long block } public boolean isArchiveHistoricalQueryEnabled() { - return stateArchiveRuntime != null; + return stateArchiveRuntime != null || commonCheckpointRuntime != null; } /** Opens one canonical request-owned exact-27 historical query view. */ @@ -962,7 +1267,8 @@ public HistoricalQuerySession openArchiveHistoricalQuery(long blockNumber, throw new IllegalArgumentException("expectedBlockHash must be exactly 32 bytes"); } StateArchiveRuntimeOwner runtime = stateArchiveRuntime; - if (runtime == null) { + CommonCheckpointRuntimeAttachment common = commonCheckpointRuntime; + if (runtime == null && common == null) { throw new IllegalStateException("Experimental state archive is disabled"); } @@ -974,8 +1280,9 @@ public HistoricalQuerySession openArchiveHistoricalQuery(long blockNumber, HistoricalQuerySession session; try { - session = HistoricalQuerySession.open(runtime.pinHistoricalState(blockNumber), - canonicalHash); + session = common == null + ? HistoricalQuerySession.open(runtime.pinHistoricalState(blockNumber), canonicalHash) + : HistoricalQuerySession.open(common.pinPoint(blockNumber), canonicalHash); } catch (java.io.IOException failure) { throw new org.tron.core.db2.archive.ArchivePersistenceException( "Failed to open request-owned historical query session", failure); @@ -1013,14 +1320,8 @@ public StateArchiveRuntimeOwner.ServingIndexInspection inspectArchiveServingInde /** Resolves one P66-aware historical TRC10 balance from a single request generation. */ public HistoricalAccountAssetBalanceResolver.Result getArchiveAccountAssetBalance( long blockNumber, byte[] address, String tokenId) { - StateArchiveRuntimeOwner runtime = stateArchiveRuntime; - if (runtime == null) { - throw new IllegalStateException("Experimental state archive is disabled"); - } - try (org.tron.core.db2.archive.ArchiveRuntimeQueryGate.Lease lease = - runtime.pinHistoricalState(blockNumber)) { - return new HistoricalAccountAssetBalanceResolver().resolve( - lease.getSnapshot(), address, tokenId); + try (ArchivePointSnapshot snapshot = pinArchivePoint(blockNumber)) { + return new HistoricalAccountAssetBalanceResolver().resolve(snapshot, address, tokenId); } catch (java.io.IOException failure) { throw new org.tron.core.db2.archive.ArchivePersistenceException( "Failed to resolve request-owned historical AccountAsset snapshot", failure); @@ -1031,6 +1332,10 @@ public HistoricalAccountAssetBalanceResolver.Result getArchiveAccountAssetBalanc public HistoricalAccountAssetPrefixResolver.Result getArchiveAccountAssets( long blockNumber, byte[] address, HistoricalAccountAssetPrefixResolver.Limits limits) { StateArchiveRuntimeOwner runtime = stateArchiveRuntime; + if (commonCheckpointRuntime != null) { + throw new UnsupportedOperationException( + "Common-checkpoint Archive supports exact point reads only"); + } if (runtime == null) { throw new IllegalStateException("Experimental state archive is disabled"); } @@ -1050,13 +1355,8 @@ public OldValue getArchiveStateValue(long blockNumber, String dbName, byte[] phy throw new IllegalArgumentException("Not a versioned archive state database: " + dbName); } Objects.requireNonNull(physicalRawKey, "physicalRawKey"); - StateArchiveRuntimeOwner runtime = stateArchiveRuntime; - if (runtime == null) { - throw new IllegalStateException("Experimental state archive is disabled"); - } - try (org.tron.core.db2.archive.ArchiveRuntimeQueryGate.Lease lease = - runtime.pinHistoricalState(blockNumber)) { - return lease.getSnapshot().get(dbName, physicalRawKey); + try (ArchivePointSnapshot snapshot = pinArchivePoint(blockNumber)) { + return snapshot.get(dbName, physicalRawKey); } catch (java.io.IOException failure) { throw new org.tron.core.db2.archive.ArchivePersistenceException( "Failed to read request-owned historical State Store snapshot", failure); @@ -1068,6 +1368,18 @@ public boolean hasArchiveStateValue(long blockNumber, String dbName, byte[] phys return getArchiveStateValue(blockNumber, dbName, physicalRawKey).isPresent(); } + private ArchivePointSnapshot pinArchivePoint(long blockNumber) throws java.io.IOException { + CommonCheckpointRuntimeAttachment common = commonCheckpointRuntime; + if (common != null) { + return common.pinPoint(blockNumber); + } + StateArchiveRuntimeOwner legacy = stateArchiveRuntime; + if (legacy == null) { + throw new IllegalStateException("Experimental state archive is disabled"); + } + return legacy.pinHistoricalState(blockNumber); + } + /** * init genesis block. */ @@ -3248,6 +3560,7 @@ public void close() { stopFilterProcessThread(); stopValidateSignThread(); rewardViCalService.stop(); + closeCommonCheckpoint(); closePathStateRoot(); closeStateArchive(); chainBaseManager.shutdown(); @@ -3269,6 +3582,19 @@ private void closeStateArchive() { } } + private void closeCommonCheckpoint() { + CommonCheckpointRuntimeAttachment runtime = commonCheckpointRuntime; + if (runtime == null) { + return; + } + if (!(revokingStore instanceof SnapshotManager)) { + throw new IllegalStateException("Common checkpoint runtime lost SnapshotManager ownership"); + } + ((SnapshotManager) revokingStore).detachCommonCheckpointRuntime(runtime); + runtime.close(); + commonCheckpointRuntime = null; + } + private void closePathStateRoot() { PathStateRuntimeAttachment runtime = pathStateRuntime; if (runtime != null) { diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 0ac85c5b000..f78fe965254 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -42,12 +42,14 @@ storage { stateArchive.directory = "state-archive" stateArchive.maxSegmentSize = 1073741824 stateArchive.queueCapacity = 256 + commonCheckpoint.enabled = false + commonCheckpoint.directory = "common-checkpoint" # Experimental current-only, non-consensus path state root. Keep disabled by default. pathStateRoot.enabled = false pathStateRoot.mode = "shadow" pathStateRoot.directory = "path-state-root" - pathStateRoot.formatVersion = 1 + pathStateRoot.formatVersion = 2 pathStateRoot.reversibleLayerLimit = 128 pathStateRoot.reversibleLayerBytes = 2147483648 pathStateRoot.writeBufferBytes = 268435456 diff --git a/framework/src/test/java/org/tron/core/db/ManagerArchiveEngineModeTest.java b/framework/src/test/java/org/tron/core/db/ManagerArchiveEngineModeTest.java new file mode 100644 index 00000000000..0d50d433d6f --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/ManagerArchiveEngineModeTest.java @@ -0,0 +1,28 @@ +package org.tron.core.db; + +import static org.junit.Assert.assertThrows; + +import org.junit.Test; +import org.tron.core.config.args.Storage; + +public class ManagerArchiveEngineModeTest { + + @Test + public void rejectsLegacyLevelDbArchiveButAcceptsSingleEngineModes() { + Storage storage = storage("LEVELDB", true, false); + assertThrows(IllegalStateException.class, + () -> Manager.requireSingleEngineArchiveMode(storage)); + + Manager.requireSingleEngineArchiveMode(storage("LEVELDB", true, true)); + Manager.requireSingleEngineArchiveMode(storage("ROCKSDB", true, false)); + Manager.requireSingleEngineArchiveMode(storage("LEVELDB", false, false)); + } + + private static Storage storage(String engine, boolean archive, boolean commonCheckpoint) { + Storage storage = new Storage(); + storage.setDbEngine(engine); + storage.setStateArchiveEnabled(archive); + storage.setCommonCheckpointEnabled(commonCheckpoint); + return storage; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java b/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java index d2939aaad59..c9df6547a69 100644 --- a/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java +++ b/framework/src/test/java/org/tron/core/db2/SnapshotManagerTest.java @@ -1,12 +1,15 @@ package org.tron.core.db2; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.collect.Maps; import com.google.common.primitives.Longs; import com.google.protobuf.ByteString; import java.io.IOException; +import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -25,8 +28,12 @@ import org.tron.core.capsule.BlockCapsule; import org.tron.core.db2.RevokingDbWithCacheNewValueTest.TestRevokingTronStore; import org.tron.core.db2.SnapshotRootTest.ProtoCapsuleTest; +import org.tron.core.db2.archive.OldValueCollector; import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.CommonCheckpointRuntimeAttachment; import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.stateroot.PathStateRuntimeAttachment; +import org.tron.core.db2.stateroot.PathStateTransitionCollector; import org.tron.core.exception.BadItemException; import org.tron.core.exception.ItemNotFoundException; import org.tron.core.exception.TronError; @@ -148,6 +155,30 @@ public void testFlushError() { Assert.assertEquals(TronError.ErrCode.DB_FLUSH, thrown.getErrCode()); } + @Test + public void commonCheckpointFlushIsExclusiveAndResetsPrefix() throws Exception { + SnapshotManager manager = new SnapshotManager(""); + CommonCheckpointRuntimeAttachment common = mock(CommonCheckpointRuntimeAttachment.class); + when(common.isEnabled()).thenReturn(true); + manager.installCommonCheckpointArchiveCollector(mock(OldValueCollector.class)); + PathStateRuntimeAttachment path = PathStateRuntimeAttachment.commonCheckpoint( + mock(PathStateTransitionCollector.class), transition -> { }, null, null); + manager.attachPathStateRuntime(path); + manager.attachCommonCheckpointRuntime(common); + manager.setUnChecked(false); + manager.setMaxFlushCount(1); + Field count = SnapshotManager.class.getDeclaredField("flushCount"); + count.setAccessible(true); + count.setInt(manager, 1); + + manager.flush(); + + verify(common).checkpointAndRebase(1); + Assert.assertFalse(manager.shouldBeRefreshed()); + Assert.assertSame(common, manager.detachCommonCheckpointRuntime(common)); + Assert.assertSame(path, manager.detachPathStateRuntime(path)); + } + @Test public void archiveStateBarrierBlocksSessionAdvanceAndFlush() throws Exception { SnapshotManager manager = new SnapshotManager(""); diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointBaselineFileTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointBaselineFileTest.java new file mode 100644 index 00000000000..e7728127c29 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointBaselineFileTest.java @@ -0,0 +1,52 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +public class CommonCheckpointBaselineFileTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void publishesOnceAndRejectsDriftOrCorruption() throws Exception { + Path root = temporaryFolder.newFolder("baseline").toPath(); + CommonCheckpointBaselineFile file = new CommonCheckpointBaselineFile(root); + CommonCheckpointBaseline expected = baseline(7); + + CommonCheckpointBaseline first = file.openOrCreate(expected); + CommonCheckpointBaseline second = file.openOrCreate(expected); + assertEquals(first.getHead(), second.getHead()); + assertArrayEquals(first.getStateRoot(), second.getStateRoot()); + assertThrows(IOException.class, () -> file.openOrCreate(baseline(8))); + + Path authority = root.resolve(CommonCheckpointBaselineFile.FILE_NAME); + byte[] corrupt = Files.readAllBytes(authority); + corrupt[corrupt.length - 1] ^= 1; + Files.write(authority, corrupt); + assertThrows(IOException.class, file::load); + } + + private static CommonCheckpointBaseline baseline(int seed) { + return new CommonCheckpointBaseline(CommonCheckpointFormat.identity(), + BlockSnapshotMeta.forBlock(seed, hash(seed), hash(seed - 1), seed * 3_000L), + hash(seed + 20)); + } + + private static byte[] hash(int seed) { + byte[] value = new byte[32]; + for (int index = 0; index < value.length; index++) { + value[index] = (byte) (seed + index); + } + return value; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathNodeStoreEngineTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathNodeStoreEngineTest.java index 5a1dfce9ecc..190fd72ab9c 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathNodeStoreEngineTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathNodeStoreEngineTest.java @@ -59,8 +59,8 @@ public void levelDbAndRocksDbProduceIdenticalRootAndPathNodes() throws Exception rocks.root.apply(reversed); byte[] expected = Hex.decode( - "f8d0364fdb0432016c12f9a660de2bd34513257014e35d90ac289d9024e6d216"); - assertArrayEquals(expected, level.root.rootHash()); + "16a59be5527b6c746e4bc2b0a67046989116f7f855a10ae0fb65263e9fb7bfda"); + assertEquals(Hex.toHexString(expected), Hex.toHexString(level.root.rootHash())); assertArrayEquals(expected, rocks.root.rootHash()); level.root.verifyNodeStores(); rocks.root.verifyNodeStores(); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java index e206c482b3a..d88817e5d96 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java @@ -1,6 +1,7 @@ package org.tron.core.db2.stateroot; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; @@ -31,21 +32,21 @@ public void fixedStoreKeyGoldensMatchIndependentKeccakOracle() throws Exception storageKey[i] = (byte) i; } - assertGolden("0b7f18d3381a9e44da93058f4214d7f0d818d1824be0f30839ed5200eb7f946a", + assertGolden("18d18850670fc1314f55e5346718606abf66777fdd67228eb193bbffffd9a2d7", 4, accountKey); - assertGolden("90ad9575451bd26f005db5063deaeac71d8b221edd0a0bb0a345f21b40c16ff5", + assertGolden("ec0aa93f7d668a1604e02eef0876edc5ff8e3904b117c172f066085fb290a26f", 22, storageKey); - assertGolden("ec6a48ade48f24cd456a89de3a5f86d282b0ae9892f265be58e5e8a704856350", + assertGolden("21bccc1258bd8d933e0c1de0cb40c3c33e3c5b12beb832d29313f2f99dd9ce0d", 21, new byte[]{1}); } @Test public void approvedAbiAndAssetIssueStoresHaveIndependentLeafDomains() throws Exception { - assertGolden("14af9866899065b509f6ea5d45902d443a4357efad5e6478c47ef108d603b8d7", + assertGolden("29f5801fed0819272800fc0bb431887f257e7e52d18086edbb61b21dd38a2aaa", 1, new byte[]{1}); - assertGolden("5c0a5639b07fa98f7e067c3e0c1d067ca1de752810b7576bc20fd2c75ba89eb5", + assertGolden("94ae32adcf9abf3bab5286ae66f5f1939083e78918f4621bbf7cd3ace8305101", 6, new byte[]{1}); - assertGolden("99951328ba6d7d4a7fa199cf727a7c4494bd885ce2ac8c04475dd1fd699af7de", + assertGolden("01c15364ad6927f41150cd5900739eb1a117c2f5e4d71c63c3a456eea63e401b", 7, new byte[]{1}); } @@ -76,9 +77,8 @@ public void superLeafGoldensBindStableIdentityFormatAndRoot() throws Exception { storeRoot[i] = (byte) i; } - assertArrayEquals( - Hex.decode("cf8715b85b2ac18d2b63e57b9e8902887f1986df7dc6a46da01fbc1f8f99f8bf"), - PathStateCommitmentCodec.superLeafKey(4)); + assertEquals("8c8018ac64709921cac7388f659d7396acef5e373bf3b329f9d93088536434a1", + Hex.toHexString(PathStateCommitmentCodec.superLeafKey(4))); assertArrayEquals(referenceSuperKey(4), PathStateCommitmentCodec.superLeafKey(4)); assertArrayEquals(Hex.decode("f38400000004876163636f756e748400000001a0000102030405060708090a0b" + "0c0d0e0f101112131415161718191a1b1c1d1e1f"), @@ -104,7 +104,7 @@ public void rejectsAmbiguousOrUnboundInputs() { private static void assertGolden(String expectedHex, int storeId, byte[] key) throws IOException { byte[] actual = PathStateCommitmentCodec.storeLeafKey(storeId, key); - assertArrayEquals(Hex.decode(expectedHex), actual); + assertEquals(expectedHex, Hex.toHexString(actual)); assertArrayEquals(referenceStoreKey(storeId, key), actual); } @@ -113,7 +113,6 @@ private static byte[] referenceStoreKey(int storeId, byte[] key) throws IOExcept try (DataOutputStream output = new DataOutputStream(bytes)) { output.writeShort(STORE_DOMAIN.length); output.write(STORE_DOMAIN); - output.writeShort(PathStateCommitmentCodec.FORMAT_VERSION); output.writeInt(storeId); output.writeInt(key.length); output.write(key); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index 33a4de9b1cb..90844bde203 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; @@ -33,6 +34,7 @@ import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; import org.tron.core.db2.common.DB; import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.CommonCheckpointBaselineFile; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.db2.core.SnapshotRoot; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; @@ -295,6 +297,84 @@ public void missingCurrentRebuildsExactNativeSnapshotAndAttaches() throws Except invoke(manager, "closePathStateRoot"); } + @Test + public void commonCheckpointFreshFlushAndRestartUseOneDurableBoundary() throws Exception { + Path output = temporaryFolder.newFolder("common-checkpoint-startup").toPath(); + long baseNumber = 100L; + long timestamp = 300L; + BlockId baseId = new BlockId(Sha256Hash.wrap(bytes(31)), baseNumber); + Sha256Hash baseParent = Sha256Hash.wrap(bytes(30)); + DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(baseNumber); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(baseId); + when(dynamic.getLatestBlockHeaderTimestamp()).thenReturn(timestamp); + when(dynamic.getAllowAccountAssetOptimizationFromRoot()).thenReturn(1L); + BlockCapsule baseBlock = mock(BlockCapsule.class); + when(baseBlock.getNum()).thenReturn(baseNumber); + when(baseBlock.getBlockId()).thenReturn(baseId); + when(baseBlock.getParentHash()).thenReturn(baseParent); + when(baseBlock.getTimeStamp()).thenReturn(timestamp); + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); + when(chainBase.getBlockByNum(baseNumber)).thenReturn(baseBlock); + when(chainBase.getAccountAssetStore()).thenReturn(mock(AccountAssetStore.class)); + + AtomicInteger closed = new AtomicInteger(); + Manager manager = new Manager(); + setChainBaseManager(manager, chainBase); + SnapshotManager[] holder = new SnapshotManager[1]; + withCommonConfig(output, () -> { + SnapshotManager snapshots = new SnapshotManager(""); + for (PathStateParticipantDescriptor.StoreIdentity participant + : PathStateParticipantDescriptor.current().getStores()) { + snapshots.getDbs().add(emptyNativeStore(participant.getDbName(), baseNumber, + baseId.getBytes(), closed)); + } + snapshots.enable(); + snapshots.setUnChecked(false); + holder[0] = snapshots; + setField(manager, "revokingStore", snapshots); + invoke(manager, "initCommonCheckpoint"); + }); + SnapshotManager snapshots = holder[0]; + assertNotNull(manager.getCommonCheckpointRuntime()); + assertNotNull(manager.getPathStateSnapshotHead()); + assertTrue(Files.isRegularFile(output.resolve("common-checkpoint") + .resolve(CommonCheckpointBaselineFile.FILE_NAME))); + assertTrue(Files.isRegularFile(output.resolve("path-state-root") + .resolve(PathStateCheckpointMaterializer.COMMON_MODE_FILE))); + assertFalse(Files.exists(output.resolve("path-state-root/CURRENT"))); + + BlockId childId = new BlockId(Sha256Hash.wrap(bytes(32)), 101L); + byte[] childHash = childId.getBytes(); + try (ISession session = snapshots.buildSession()) { + session.commit(BlockSnapshotMeta.forBlock(101, childHash, baseId.getBytes(), 303L)); + } + setSnapshotField(snapshots, "flushCount", 1); + snapshots.flush(); + assertTrue(Files.isRegularFile(output.resolve("path-state-root/CURRENT"))); + assertTrue(Files.isRegularFile(output.resolve("state-archive/READABLE"))); + assertFalse(Files.exists(output.resolve("common-checkpoint/COMMON_CHECKPOINT"))); + + BlockCapsule childBlock = mock(BlockCapsule.class); + when(childBlock.getNum()).thenReturn(101L); + when(childBlock.getBlockId()).thenReturn(childId); + when(childBlock.getParentHash()).thenReturn(baseId); + when(childBlock.getTimeStamp()).thenReturn(303L); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(101L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(childId); + when(dynamic.getLatestBlockHeaderTimestamp()).thenReturn(303L); + when(chainBase.getBlockByNum(101L)).thenReturn(childBlock); + invoke(manager, "closeCommonCheckpoint"); + invoke(manager, "closePathStateRoot"); + + withCommonConfig(output, () -> invoke(manager, "initCommonCheckpoint")); + assertEquals(101L, manager.getPathStateSnapshotHead().getHead().getBlockNumber()); + assertArrayEquals(childHash, manager.getPathStateSnapshotHead().getHead().getBlockHash()); + invoke(manager, "closeCommonCheckpoint"); + invoke(manager, "closePathStateRoot"); + } + @SuppressWarnings("unchecked") private static Chainbase propertiesStoreWithP66Enabled() { DB database = mock(DB.class); @@ -361,6 +441,33 @@ private static void withConfig(Path output, boolean enabled, ThrowingRunnable ac } } + private static void withCommonConfig(Path output, ThrowingRunnable action) throws Exception { + CommonParameter args = CommonParameter.getInstance(); + Storage oldStorage = args.getStorage(); + String oldOutput = args.outputDirectory; + try { + Storage storage = new Storage(); + args.outputDirectory = output.toString(); + args.storage = storage; + storage.setDbEngine("ROCKSDB"); + storage.setStateArchiveEnabled(true); + storage.setStateArchiveDirectory("state-archive"); + storage.setCommonCheckpointEnabled(true); + storage.setCommonCheckpointDirectory("common-checkpoint"); + storage.setPathStateRootEnabled(true); + storage.setPathStateRootDirectory("path-state-root"); + storage.setPathStateRootReversibleLayerLimit(8); + storage.setPathStateRootReversibleLayerBytes(1L << 20); + storage.setPathStateRootNodeCacheBytes(1L << 20); + storage.setPathStateRootParticipantThreads(2); + storage.setPathStateRootBranchThreads(2); + action.run(); + } finally { + args.outputDirectory = oldOutput; + args.storage = oldStorage; + } + } + private static void setChainBaseManager(Manager manager, ChainBaseManager chainBase) throws Exception { setField(manager, "chainBaseManager", chainBase); @@ -372,6 +479,13 @@ private static void setField(Manager manager, String name, Object value) throws field.set(manager, value); } + private static void setSnapshotField(SnapshotManager manager, String name, int value) + throws Exception { + java.lang.reflect.Field field = SnapshotManager.class.getDeclaredField(name); + field.setAccessible(true); + field.setInt(manager, value); + } + private static void invoke(Manager manager, String methodName) throws Exception { Method method = Manager.class.getDeclaredMethod(methodName); method.setAccessible(true); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index 3642e27d00d..fc5788642ab 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -27,7 +27,11 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.tron.common.arch.Arch; +import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.core.CommonCheckpointBaseline; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointTarget; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.EntryConsumer; import org.tron.core.db2.stateroot.PathStateRebuildCoordinator.SnapshotIdentity; @@ -771,6 +775,47 @@ Engine.ROCKSDB, new PathStateLayerLimits(4, 1L << 20))) { } } + @Test + public void commonOverlayReplacesInMemoryTrieAfterStartupRedo() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-common-redo-overlay").toPath(); + preparePublishedPhysicalTarget(root, scope); + byte[] formatIdentity = bytes(73); + byte[] blockHash = bytes(74); + + try (PathStatePhysicalOverlayHead head = PathStatePhysicalOverlayHead.open(root, + Engine.ROCKSDB, new PathStateLayerLimits(4, 1L << 20))) { + PathStateRootMetadata baselineHead = head.getHead(); + CommonCheckpointBaseline baseline = new CommonCheckpointBaseline(formatIdentity, + BlockSnapshotMeta.forBlock(baselineHead.getBlockNumber(), baselineHead.getBlockHash(), + baselineHead.getParentHash(), baselineHead.getTimestamp()), + baselineHead.getStateRoot()); + head.admitFreshCommonBaseline(baseline); + + PathStateBlockTransition transition = new PathStateBlockTransition(1, blockHash, + baselineHead.getBlockHash(), 3, P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{2}))); + BlockSnapshotMeta block = BlockSnapshotMeta.forBlock(1, blockHash, + baselineHead.getBlockHash(), 3); + PathStateSnapshotDelta delta = head.prepareSnapshotDelta(block, transition); + PathStateFlushTarget target = PathStateFlushTarget.coalesce( + Collections.singletonList(delta)); + CommonCheckpointPayload payload = CommonCheckpointPayload.create(formatIdentity, target, + Collections.singletonList(new BlockReverseDiff(block, Collections.emptyList(), + delta.getMutationViewDigest())), Collections.emptyList()); + CommonCheckpointTarget checkpointTarget = CommonCheckpointTarget.from(payload); + PathStateCheckpointMaterializer materializer = head.checkpointMaterializer(formatIdentity, + baseline); + materializer.materialize(payload, checkpointTarget); + materializer.publish(checkpointTarget); + + head.synchronizePublishedCheckpoint(formatIdentity, block, P66Phase.P66_ON); + assertEquals(1, head.getHead().getBlockNumber()); + assertArrayEquals(blockHash, head.getHead().getBlockHash()); + assertArrayEquals(delta.getStateRoot(), head.getHead().getStateRoot()); + } + } + @Test public void asyncPrepareQueuesTransitionAndCompletesOffCallerThread() throws Exception { PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java index c3d4195ab98..32ad614fdda 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java @@ -43,7 +43,7 @@ public void manifestCreatesIndependentCurrentOnlyLayoutAndReopensWithoutRewrite( assertTrue(Files.isDirectory(created.getLayersDirectory())); assertArrayEquals(original, Files.readAllBytes(manifest)); assertEquals( - "d0fc17ad2ea70578b2400c8c3563b05407ff6d7f53f26ea7ad47b513565d404e", + "37fc0b69dae958872a3088ee060353e370c4ad7b9ff1804e5e151b47da1efa20", ByteArray.toHexString(Hashing.sha256().hashBytes(original).asBytes())); assertFalse(Files.exists(root.resolve("history"))); } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java index 8d3cc7cf7c3..fa11feda9f5 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java @@ -43,9 +43,8 @@ public void aggregatesEveryParticipantIntoIndependentOracleSuperRoot() { } byte[] expected = referenceRoot(participants, mutations); - assertArrayEquals( - Hex.decode("f8d0364fdb0432016c12f9a660de2bd34513257014e35d90ac289d9024e6d216"), - expected); + assertEquals("16a59be5527b6c746e4bc2b0a67046989116f7f855a10ae0fb65263e9fb7bfda", + Hex.toHexString(expected)); assertArrayEquals(expected, stateRoot.rootHash()); } From d7c402699a885add052236971cfe69654a2be629 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 5 Sep 2026 14:53:28 +0800 Subject: [PATCH 113/161] fix(chainbase): flush common checkpoint on close --- .../main/java/org/tron/core/db/Manager.java | 1 + ...athStateManagerStartupIntegrationTest.java | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 8db8d20c94f..b4a5ce3267e 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -3590,6 +3590,7 @@ private void closeCommonCheckpoint() { if (!(revokingStore instanceof SnapshotManager)) { throw new IllegalStateException("Common checkpoint runtime lost SnapshotManager ownership"); } + revokingStore.flushPending(); ((SnapshotManager) revokingStore).detachCommonCheckpointRuntime(runtime); runtime.close(); commonCheckpointRuntime = null; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index 90844bde203..ba74c88f35a 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -365,12 +365,28 @@ public void commonCheckpointFreshFlushAndRestartUseOneDurableBoundary() throws E when(dynamic.getLatestBlockHeaderHash()).thenReturn(childId); when(dynamic.getLatestBlockHeaderTimestamp()).thenReturn(303L); when(chainBase.getBlockByNum(101L)).thenReturn(childBlock); + + BlockId pendingId = new BlockId(Sha256Hash.wrap(bytes(33)), 102L); + try (ISession session = snapshots.buildSession()) { + session.commit(BlockSnapshotMeta.forBlock(102, pendingId.getBytes(), childHash, 306L)); + } + setSnapshotField(snapshots, "flushCount", 1); + BlockCapsule pendingBlock = mock(BlockCapsule.class); + when(pendingBlock.getNum()).thenReturn(102L); + when(pendingBlock.getBlockId()).thenReturn(pendingId); + when(pendingBlock.getParentHash()).thenReturn(childId); + when(pendingBlock.getTimeStamp()).thenReturn(306L); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(102L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(pendingId); + when(dynamic.getLatestBlockHeaderTimestamp()).thenReturn(306L); + when(chainBase.getBlockByNum(102L)).thenReturn(pendingBlock); invoke(manager, "closeCommonCheckpoint"); invoke(manager, "closePathStateRoot"); withCommonConfig(output, () -> invoke(manager, "initCommonCheckpoint")); - assertEquals(101L, manager.getPathStateSnapshotHead().getHead().getBlockNumber()); - assertArrayEquals(childHash, manager.getPathStateSnapshotHead().getHead().getBlockHash()); + assertEquals(102L, manager.getPathStateSnapshotHead().getHead().getBlockNumber()); + assertArrayEquals(pendingId.getBytes(), + manager.getPathStateSnapshotHead().getHead().getBlockHash()); invoke(manager, "closeCommonCheckpoint"); invoke(manager, "closePathStateRoot"); } From ae815675a8825e27ef0a5d66d6c3fab383b20ad4 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 5 Sep 2026 15:11:33 +0800 Subject: [PATCH 114/161] fix(chainbase): isolate common checkpoint recovery --- .../tron/core/db2/core/SnapshotManager.java | 27 ++++++++++++++++ ...otManagerCommonCheckpointRecoveryTest.java | 31 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/db2/core/SnapshotManagerCommonCheckpointRecoveryTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index c3bc4a3d72d..0ec2c99b565 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -10,6 +10,9 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; @@ -950,6 +953,11 @@ private void pruneCheckpoint() { @Override public void check() { recoveredArchiveWalBinding = null; + if (hasDurableCommonCheckpointAuthority()) { + logger.info("Common checkpoint authority established, skip legacy checkpoint recovery"); + unChecked = false; + return; + } if (!isV2Open()) { List cpList = getCheckpointList(); if (cpList != null && cpList.size() != 0) { @@ -964,6 +972,25 @@ public void check() { } } + private boolean hasDurableCommonCheckpointAuthority() { + org.tron.core.config.args.Storage storage = + CommonParameter.getInstance().getStorage(); + Path directory = Paths.get(CommonParameter.getInstance().getOutputDirectory(), + storage.getCommonCheckpointDirectory()).normalize(); + return hasDurableCommonCheckpointAuthority(storage.isCommonCheckpointEnabled(), directory); + } + + static boolean hasDurableCommonCheckpointAuthority(boolean enabled, Path directory) { + if (!enabled) { + return false; + } + Path admitted = Objects.requireNonNull(directory, "directory"); + return Files.isRegularFile(admitted.resolve(ChainbaseCheckpointMaterializer.CURRENT_FILE), + LinkOption.NOFOLLOW_LINKS) + || Files.isRegularFile(admitted.resolve(CommonCheckpointFile.FILE_NAME), + LinkOption.NOFOLLOW_LINKS); + } + private void checkV1() { for (Chainbase db: dbs) { if (!Snapshot.isRoot(db.getHead())) { diff --git a/framework/src/test/java/org/tron/core/db2/core/SnapshotManagerCommonCheckpointRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/core/SnapshotManagerCommonCheckpointRecoveryTest.java new file mode 100644 index 00000000000..8681e15e93c --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/SnapshotManagerCommonCheckpointRecoveryTest.java @@ -0,0 +1,31 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class SnapshotManagerCommonCheckpointRecoveryTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void legacyRecoveryStopsAfterCommonAuthorityIsDurable() throws Exception { + Path directory = temporaryFolder.newFolder("common-checkpoint").toPath(); + assertFalse(SnapshotManager.hasDurableCommonCheckpointAuthority(true, directory)); + + Path redo = directory.resolve(CommonCheckpointFile.FILE_NAME); + Files.write(redo, new byte[] {1}); + assertTrue(SnapshotManager.hasDurableCommonCheckpointAuthority(true, directory)); + assertFalse(SnapshotManager.hasDurableCommonCheckpointAuthority(false, directory)); + + Files.delete(redo); + Files.write(directory.resolve(ChainbaseCheckpointMaterializer.CURRENT_FILE), new byte[] {1}); + assertTrue(SnapshotManager.hasDurableCommonCheckpointAuthority(true, directory)); + } +} From 632c995968204a85f571d70fa5904a3669769460 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 5 Sep 2026 15:15:54 +0800 Subject: [PATCH 115/161] fix(chainbase): honor archive engine configuration Use the configured storage engine for legacy and common-checkpoint Archive indexes while preserving fail-closed durable engine identity checks.\n\nAdd LevelDB generation extension, CRC32C engine manifests compatible with the legacy SHA format, and runtime-bound validation outside the point-query pin hot path. --- .../PersistentServingKeyIndexCatalog.java | 36 ++- .../PersistentServingKeyIndexGeneration.java | 287 ++++++++---------- .../StateArchiveCheckpointMaterializer.java | 15 + .../StateArchiveCheckpointReadAdapter.java | 9 + .../StateArchiveCheckpointReadSnapshot.java | 27 ++ .../StateArchiveCheckpointServingIndex.java | 15 +- .../archive/StateArchiveIndexDatabase.java | 193 +++++++++++- .../StateArchiveIndexEngineManifest.java | 69 +++-- .../db2/archive/StateArchiveRuntimeOwner.java | 3 +- .../db2/core/CommonCheckpointRuntime.java | 32 +- .../main/java/org/tron/core/db/Manager.java | 12 +- .../core/db/ManagerArchiveEngineModeTest.java | 28 -- ...rsistentServingKeyIndexGenerationTest.java | 15 +- .../StateArchiveIndexDatabaseTest.java | 21 ++ ...eArchiveManagerStartupIntegrationTest.java | 37 ++- .../ChainbaseCheckpointMaterializerTest.java | 9 +- 16 files changed, 549 insertions(+), 259 deletions(-) delete mode 100644 framework/src/test/java/org/tron/core/db/ManagerArchiveEngineModeTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java index 110f5d92753..b7ce938d5d3 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexCatalog.java @@ -23,6 +23,7 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Stream; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** Durable generation catalog with request refcounts and safe retired-generation reaping. */ public final class PersistentServingKeyIndexCatalog implements Closeable { @@ -35,15 +36,18 @@ public final class PersistentServingKeyIndexCatalog implements Closeable { private final Path root; private final Path generations; + private final Engine engine; private final Map references = new HashMap<>(); private final Set retired = new HashSet<>(); private final FaultHook faultHook; private String currentId; private boolean closed; - private PersistentServingKeyIndexCatalog(Path root, String currentId, FaultHook faultHook) { + private PersistentServingKeyIndexCatalog(Path root, String currentId, Engine engine, + FaultHook faultHook) { this.root = root; this.generations = root.resolve(GENERATIONS); + this.engine = Objects.requireNonNull(engine, "engine"); this.currentId = currentId; this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); } @@ -57,12 +61,21 @@ static PersistentServingKeyIndexCatalog open(Path root, FaultHook faultHook) Objects.requireNonNull(root, "root"); String currentId = readCurrent(root); Path current = root.resolve(GENERATIONS).resolve(currentId); + Engine engine = StateArchiveIndexEngineManifest.load(current); + return open(root, engine, faultHook); + } + + static PersistentServingKeyIndexCatalog open(Path root, Engine engine, FaultHook faultHook) + throws IOException { + Objects.requireNonNull(root, "root"); + String currentId = readCurrent(root); + Path current = root.resolve(GENERATIONS).resolve(currentId); try (PersistentServingKeyIndexGeneration ignored = - PersistentServingKeyIndexGeneration.open(current)) { - // Opening validates both the immutable descriptor and RocksDB generation. + PersistentServingKeyIndexGeneration.open(current, engine)) { + // Opening validates both the immutable descriptor and configured native generation. } PersistentServingKeyIndexCatalog catalog = - new PersistentServingKeyIndexCatalog(root, currentId, faultHook); + new PersistentServingKeyIndexCatalog(root, currentId, engine, faultHook); catalog.discoverRetired(); return catalog; } @@ -85,8 +98,9 @@ static PersistentServingKeyIndexCatalog create(Path root, Path initialShadow, throw new IllegalArgumentException("Serving index catalog already exists"); } Files.createDirectories(root.resolve(GENERATIONS)); + Engine engine = StateArchiveIndexEngineManifest.load(initialShadow); PersistentServingKeyIndexCatalog catalog = - new PersistentServingKeyIndexCatalog(root, null, faultHook); + new PersistentServingKeyIndexCatalog(root, null, engine, faultHook); if (!catalog.publishInternal(null, initialShadow, null)) { throw new IllegalStateException("Failed to publish initial serving generation"); } @@ -100,15 +114,16 @@ private static PersistentServingKeyIndexCatalog createInternal(Path root, Path i throw new IllegalArgumentException("Serving index catalog already exists"); } Files.createDirectories(root.resolve(GENERATIONS)); + Engine engine = StateArchiveIndexEngineManifest.load(initialShadow); PersistentServingKeyIndexCatalog catalog = - new PersistentServingKeyIndexCatalog(root, null, stage -> { }); + new PersistentServingKeyIndexCatalog(root, null, engine, stage -> { }); if (!catalog.publishInternal(null, initialShadow, readerVisible)) { throw new IllegalStateException("Failed to publish initial serving generation"); } return catalog; } - /** Pins one immutable RocksDB handle and holds its generation refcount until close. */ + /** Pins one immutable native generation handle and holds its refcount until close. */ public synchronized PersistentServingKeyIndexGeneration pin( ArchiveProgressEnvelope readerVisible) throws IOException { return pinInternal(readerVisible); @@ -128,7 +143,8 @@ private PersistentServingKeyIndexGeneration pinInternal( references.put(pinnedId, references.getOrDefault(pinnedId, 0) + 1); PersistentServingKeyIndexGeneration pinned; try { - pinned = PersistentServingKeyIndexGeneration.open(generations.resolve(pinnedId), + pinned = PersistentServingKeyIndexGeneration.openTrusted(generations.resolve(pinnedId), + engine, () -> release(pinnedId)); } catch (IOException | RuntimeException failure) { release(pinnedId); @@ -166,7 +182,7 @@ private boolean publishInternal(String expectedId, Path shadow, long replacementFrom; long replacementThrough; try (PersistentServingKeyIndexGeneration replacement = - PersistentServingKeyIndexGeneration.open(shadow)) { + PersistentServingKeyIndexGeneration.open(shadow, engine)) { if (readerVisible != null) { validateReaderVisibility(replacement, readerVisible); } @@ -177,7 +193,7 @@ private boolean publishInternal(String expectedId, Path shadow, validateGenerationId(replacementId); if (currentId != null) { try (PersistentServingKeyIndexGeneration current = - PersistentServingKeyIndexGeneration.open(generations.resolve(currentId))) { + PersistentServingKeyIndexGeneration.open(generations.resolve(currentId), engine)) { if (replacementFrom != current.getIndexedFrom() || replacementThrough < current.getIndexedThrough()) { throw new IllegalArgumentException("Serving generation publication regresses coverage"); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java index 5c6e873e5ec..f921698ff28 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java @@ -27,16 +27,9 @@ import java.util.Objects; import java.util.OptionalLong; import java.util.stream.Stream; -import org.rocksdb.Checkpoint; -import org.rocksdb.CompressionType; -import org.rocksdb.Options; -import org.rocksdb.RocksDB; -import org.rocksdb.RocksDBException; -import org.rocksdb.RocksIterator; -import org.rocksdb.WriteBatch; -import org.rocksdb.WriteOptions; - -/** Persistent immutable exact-key serving generation backed by RocksDB. */ +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Persistent immutable exact-key serving generation backed by the configured database engine. */ public final class PersistentServingKeyIndexGeneration implements ServingKeyIndex { private static final int MAGIC = 0x534b4947; // SKIG @@ -62,35 +55,33 @@ public final class PersistentServingKeyIndexGeneration implements ServingKeyInde private static final String PENDING_COMPACTION_BYTES = "rocksdb.estimate-pending-compaction-bytes"; - static { - RocksDB.loadLibrary(); - } - private final Path directory; private final Descriptor descriptor; - private final Options options; - private final RocksDB database; + private final Engine engine; + private final StateArchiveIndexDatabase.Reader database; private final Runnable release; private boolean closed; private PersistentServingKeyIndexGeneration(Path directory, Descriptor descriptor, - Runnable release) throws IOException { + Engine engine, boolean validateEngine, Runnable release) throws IOException { this.directory = directory; this.descriptor = descriptor; + this.engine = Objects.requireNonNull(engine, "engine"); this.release = Objects.requireNonNull(release, "release"); - this.options = new Options().setCreateIfMissing(false); - RocksDB opened = null; + if (validateEngine) { + StateArchiveIndexEngineManifest.openOrCreateLegacy(directory, engine); + } + StateArchiveIndexDatabase.Reader opened = null; try { - opened = RocksDB.openReadOnly(options, directory.resolve(DATABASE).toString()); + opened = StateArchiveIndexDatabase.openReader(directory.resolve(DATABASE), engine); if (descriptor.formatVersion == EXACT_VERSION) { validateExactStoreCoverage(opened, descriptor); } this.database = opened; - } catch (RocksDBException | RuntimeException failure) { + } catch (IOException | RuntimeException failure) { if (opened != null) { opened.close(); } - options.close(); throw new IOException("Failed to open serving index generation", failure); } } @@ -121,6 +112,8 @@ public static PersistentServingKeyIndexGeneration build(Path directory, String g throw new IllegalArgumentException("Serving generation directory already exists"); } Files.createDirectories(directory); + Engine engine = configuredEngine(); + StateArchiveIndexEngineManifest.openOrCreate(directory, engine); MessageDigest sourceDigest = sha256(); updateLong(sourceDigest, baseEpoch); @@ -131,37 +124,31 @@ public static PersistentServingKeyIndexGeneration build(Path directory, String g long previousBlock = baseEpoch; byte[] previousHash = Arrays.copyOf(baseHash, baseHash.length); long keyChanges = 0; - Options buildOptions = new Options().setCreateIfMissing(true); - WriteOptions writes = new WriteOptions().setSync(false); - try (RocksDB target = RocksDB.open(buildOptions, directory.resolve(DATABASE).toString())) { + try (StateArchiveIndexDatabase.Writer target = + StateArchiveIndexDatabase.openWriter(directory.resolve(DATABASE), engine)) { for (HistoryCommitMarker marker : committed) { BlockSnapshotMeta meta = marker.getMeta(); validateNext(marker, previousEpoch, previousBlock, previousHash, participants); HistoryIndexRecord record = reader.read(marker.getIndexLocation()); validateMarker(marker, record, participants); - try (WriteBatch batch = new WriteBatch()) { - for (HistoryIndexRecord.KeyGroup group : record.getGroups()) { - for (byte[] key : group.getKeys()) { - batch.put(dataKey(group.getDbName(), key, meta.getEpoch()), PRESENT); - batch.put(rangeDataKey(group.getDbName(), key, meta.getEpoch()), PRESENT); - keyChanges++; - } + List mutations = new ArrayList<>(); + for (HistoryIndexRecord.KeyGroup group : record.getGroups()) { + for (byte[] key : group.getKeys()) { + mutations.add(StateArchiveIndexDatabase.put( + dataKey(group.getDbName(), key, meta.getEpoch()), PRESENT)); + mutations.add(StateArchiveIndexDatabase.put( + rangeDataKey(group.getDbName(), key, meta.getEpoch()), PRESENT)); + keyChanges++; } - target.write(writes, batch); } + target.write(mutations, false); updateSourceDigest(sourceDigest, marker); previousEpoch = meta.getEpoch(); previousBlock = meta.getBlockNumber(); previousHash = meta.getBlockHash(); } - try (WriteOptions sync = new WriteOptions().setSync(true)) { - target.put(sync, new byte[]{0}, new byte[]{1}); - } - } catch (RocksDBException failure) { - throw new IOException("Failed to build serving index generation", failure); - } finally { - writes.close(); - buildOptions.close(); + target.write(Collections.singletonList( + StateArchiveIndexDatabase.put(new byte[]{0}, new byte[]{1})), true); } Descriptor descriptor = new Descriptor(VERSION, scopeIdentity, generationId, baseEpoch, @@ -169,7 +156,7 @@ public static PersistentServingKeyIndexGeneration build(Path directory, String g previousHash, sourceDigest.digest(), latestSourceIdentityDigest, participants, keyChanges); persistDescriptor(directory, descriptor); HistorySegmentStore.syncDirectory(directory); - return open(directory); + return open(directory, engine); } public static PersistentServingKeyIndexGeneration open(Path directory) throws IOException { @@ -178,7 +165,24 @@ public static PersistentServingKeyIndexGeneration open(Path directory) throws IO static PersistentServingKeyIndexGeneration open(Path directory, Runnable release) throws IOException { - return new PersistentServingKeyIndexGeneration(directory, loadDescriptor(directory), release); + return open(directory, configuredEngine(), release); + } + + static PersistentServingKeyIndexGeneration open(Path directory, Engine engine) + throws IOException { + return open(directory, engine, () -> { }); + } + + static PersistentServingKeyIndexGeneration open(Path directory, Engine engine, + Runnable release) throws IOException { + return new PersistentServingKeyIndexGeneration(directory, loadDescriptor(directory), engine, + true, release); + } + + static PersistentServingKeyIndexGeneration openTrusted(Path directory, Engine engine, + Runnable release) throws IOException { + return new PersistentServingKeyIndexGeneration(directory, loadDescriptor(directory), engine, + false, release); } /** Creates one approved v5 exact-only generation from a validated logical increment plan. */ @@ -199,15 +203,15 @@ static PersistentServingKeyIndexGeneration buildExact(Path directory, String gen throw new IllegalArgumentException("Serving generation directory already exists"); } Files.createDirectories(directory); + Engine engine = configuredEngine(); + StateArchiveIndexEngineManifest.openOrCreate(directory, engine); byte[] sourceDigest = rollSourceDigest(plan.getSourceSeedDigest(), plan.getSourceStepDigests()); long keyChanges; - try (Options buildOptions = exactOptions(true); - RocksDB target = RocksDB.open(buildOptions, directory.resolve(DATABASE).toString())) { + try (StateArchiveIndexDatabase.Writer target = + StateArchiveIndexDatabase.openWriter(directory.resolve(DATABASE), engine)) { keyChanges = applyExactPlan(target, generationId, plan, plan.getIndexedFrom(), sourceDigest, faultHook); - } catch (RocksDBException failure) { - throw new IOException("Failed to build exact serving generation", failure); } Descriptor descriptor = new Descriptor(EXACT_VERSION, ArchiveParticipantDescriptor.FORMAT_ID, generationId, plan.getIndexedFrom(), @@ -215,7 +219,7 @@ static PersistentServingKeyIndexGeneration buildExact(Path directory, String gen plan.getParticipatingDatabases(), keyChanges); persistDescriptor(directory, descriptor); HistorySegmentStore.syncDirectory(directory); - return open(directory); + return open(directory, engine); } /** Checkpoints this immutable v5 generation and applies only the validated {@code (I,H]} plan. */ @@ -243,23 +247,16 @@ synchronized PersistentServingKeyIndexGeneration extendExact(Path directory, throw new IllegalArgumentException("Serving generation directory already exists"); } Files.createDirectories(directory); - try (Options checkpointOptions = exactOptions(false); - RocksDB checkpointSource = RocksDB.open(checkpointOptions, - this.directory.resolve(DATABASE).toString()); - Checkpoint checkpoint = Checkpoint.create(checkpointSource)) { - checkpoint.createCheckpoint(directory.resolve(DATABASE).toString()); - } catch (RocksDBException failure) { - throw new IOException("Failed to checkpoint exact serving generation", failure); - } + StateArchiveIndexEngineManifest.openOrCreate(directory, engine); + StateArchiveIndexDatabase.checkpoint(this.directory.resolve(DATABASE), + directory.resolve(DATABASE), engine); byte[] sourceDigest = rollSourceDigest(descriptor.sourceDigest, plan.getSourceStepDigests()); long added; - try (Options writeOptions = exactOptions(false); - RocksDB target = RocksDB.open(writeOptions, directory.resolve(DATABASE).toString())) { + try (StateArchiveIndexDatabase.Writer target = + StateArchiveIndexDatabase.openWriter(directory.resolve(DATABASE), engine)) { added = applyExactPlan(target, generationId, plan, descriptor.indexedFrom, sourceDigest, faultHook); - } catch (RocksDBException failure) { - throw new IOException("Failed to extend exact serving generation", failure); } Descriptor replacement = new Descriptor(EXACT_VERSION, descriptor.scopeIdentity, generationId, descriptor.indexedFrom, plan.getIndexedThrough(), plan.getHeadHash(), @@ -267,7 +264,7 @@ synchronized PersistentServingKeyIndexGeneration extendExact(Path directory, descriptor.keyChanges + added); persistDescriptor(directory, replacement); HistorySegmentStore.syncDirectory(directory); - return open(directory); + return open(directory, engine); } @Override @@ -286,12 +283,13 @@ public synchronized OptionalLong firstChangeAfter(String dbName, byte[] rawKey, byte[] prefix = dataPrefix(dbName, rawKey); byte[] seek = ByteBuffer.allocate(prefix.length + Long.BYTES).put(prefix) .putLong(targetBlock + 1).array(); - try (RocksIterator iterator = database.newIterator()) { + try (StateArchiveIndexDatabase.Cursor iterator = database.cursor()) { iterator.seek(seek); - if (!iterator.isValid()) { + StateArchiveIndexDatabase.KeyValue entry = iterator.next(); + if (entry == null) { return OptionalLong.empty(); } - byte[] found = iterator.key(); + byte[] found = entry.getKey(); if (found.length != prefix.length + Long.BYTES || !startsWith(found, prefix)) { return OptionalLong.empty(); } @@ -325,10 +323,11 @@ public synchronized List changesInRange(St byte[] databasePrefix = rangeDatabasePrefix(dbName); List result = new ArrayList<>(); - try (RocksIterator iterator = database.newIterator()) { + try (StateArchiveIndexDatabase.Cursor iterator = database.cursor()) { iterator.seek(concat(databasePrefix, encodeRangeRawKey(lowerInclusive))); - while (iterator.isValid()) { - RangeDataKey found = decodeRangeDataKey(iterator.key(), databasePrefix); + StateArchiveIndexDatabase.KeyValue entry; + while ((entry = iterator.next()) != null) { + RangeDataKey found = decodeRangeDataKey(entry.getKey(), databasePrefix); if (found == null) { break; } @@ -338,10 +337,11 @@ public synchronized List changesInRange(St } if (found.epoch <= targetBlock) { iterator.seek(rangeDataKey(dbName, found.rawKey, targetBlock + 1)); - if (!iterator.isValid()) { + entry = iterator.next(); + if (entry == null) { break; } - RangeDataKey candidate = decodeRangeDataKey(iterator.key(), databasePrefix); + RangeDataKey candidate = decodeRangeDataKey(entry.getKey(), databasePrefix); if (candidate == null || !Arrays.equals(candidate.rawKey, found.rawKey)) { continue; } @@ -355,9 +355,6 @@ public synchronized List changesInRange(St } iterator.seek(rangeAfterRawKey(databasePrefix, found.rawKey)); } - iterator.status(); - } catch (RocksDBException failure) { - throw new IOException("Failed to scan serving index generation", failure); } return Collections.unmodifiableList(result); } @@ -426,12 +423,7 @@ public PersistentStoreCoverage getPersistentStoreCoverage(String dbName) throws if (!isExactOnlyFormat()) { throw new ArchivePersistenceException("Serving generation has no durable Store coverage"); } - byte[] encoded; - try { - encoded = database.get(storeCoverageKey(dbName)); - } catch (RocksDBException failure) { - throw new IOException("Failed to read serving Store coverage", failure); - } + byte[] encoded = database.get(storeCoverageKey(dbName)); PersistentStoreCoverage coverage = decodeCoverage(encoded); if (!coverage.dbName.equals(dbName) || coverage.indexedFrom != descriptor.indexedFrom @@ -480,12 +472,15 @@ Path getDirectory() { return directory; } + Engine getEngine() { + return engine; + } + @Override - public synchronized void close() { + public synchronized void close() throws IOException { if (!closed) { closed = true; database.close(); - options.close(); release.run(); } } @@ -517,11 +512,12 @@ private StoreStatistics inspectStore(String dbName) throws IOException { long pages = 0; long pagedEntries = 0; long logicalBytes = 0; - try (RocksIterator iterator = database.newIterator()) { + try (StateArchiveIndexDatabase.Cursor iterator = database.cursor()) { iterator.seek(metaPrefix); - while (iterator.isValid() && startsWith(iterator.key(), metaPrefix)) { - byte[] key = iterator.key(); - byte[] value = iterator.value(); + StateArchiveIndexDatabase.KeyValue entry; + while ((entry = iterator.next()) != null && startsWith(entry.getKey(), metaPrefix)) { + byte[] key = entry.getKey(); + byte[] value = entry.getValue(); KeyMeta meta = decodeKeyMeta(value); keyMetadata++; changeEntries += meta.count; @@ -532,38 +528,27 @@ private StoreStatistics inspectStore(String dbName) throws IOException { pagedKeys++; expectedPagedEntries += meta.count; } - iterator.next(); } - iterator.status(); - } catch (RocksDBException failure) { - throw new IOException("Failed to inspect exact serving key metadata", failure); } - try (RocksIterator iterator = database.newIterator()) { + try (StateArchiveIndexDatabase.Cursor iterator = database.cursor()) { iterator.seek(pagePrefix); - while (iterator.isValid() && startsWith(iterator.key(), pagePrefix)) { - byte[] key = iterator.key(); - byte[] value = iterator.value(); + StateArchiveIndexDatabase.KeyValue entry; + while ((entry = iterator.next()) != null && startsWith(entry.getKey(), pagePrefix)) { + byte[] key = entry.getKey(); + byte[] value = entry.getValue(); pages++; pagedEntries += decodeEpochPage(value).length; logicalBytes += key.length + value.length; - iterator.next(); } - iterator.status(); - } catch (RocksDBException failure) { - throw new IOException("Failed to inspect exact serving epoch pages", failure); } if (pagedEntries != expectedPagedEntries) { throw new ArchivePersistenceException( "Serving statistics found inconsistent paged entry totals: " + dbName); } byte[] coverageKey = storeCoverageKey(dbName); - try { - byte[] coverageValue = database.get(coverageKey); - decodeCoverage(coverageValue); - logicalBytes += coverageKey.length + coverageValue.length; - } catch (RocksDBException failure) { - throw new IOException("Failed to inspect serving Store coverage", failure); - } + byte[] coverageValue = database.get(coverageKey); + decodeCoverage(coverageValue); + logicalBytes += coverageKey.length + coverageValue.length; return new StoreStatistics(dbName, keyMetadata, inlineKeys, pagedKeys, pages, changeEntries, logicalBytes); } @@ -598,8 +583,8 @@ private static FileSizeMeasurement measureGenerationFiles(Path root) throws IOEx private OptionalLong readLongProperty(String name) { try { - return OptionalLong.of(database.getLongProperty(name)); - } catch (RocksDBException | IllegalArgumentException failure) { + return database.readLongProperty(name); + } catch (IOException | IllegalArgumentException failure) { return OptionalLong.empty(); } } @@ -618,15 +603,11 @@ private static LongPropertyMeasurement readProperty(RocksPropertyReader reader, private OptionalLong firstExactChangeAfter(String dbName, byte[] rawKey, long targetBlock, long upperBound) throws IOException { KeyMeta meta; - try { - byte[] encoded = database.get(keyMetaKey(dbName, rawKey)); - if (encoded == null) { - return OptionalLong.empty(); - } - meta = decodeKeyMeta(encoded); - } catch (RocksDBException failure) { - throw new IOException("Failed to read exact serving key metadata", failure); + byte[] encoded = database.get(keyMetaKey(dbName, rawKey)); + if (encoded == null) { + return OptionalLong.empty(); } + meta = decodeKeyMeta(encoded); if (meta.lastEpoch <= targetBlock || meta.firstEpoch > upperBound) { return OptionalLong.empty(); } @@ -652,20 +633,17 @@ private OptionalLong firstExactChangeAfter(String dbName, byte[] rawKey, long ta } private long[] readPage(String dbName, byte[] rawKey, int pageIndex) throws IOException { - try { - byte[] encoded = database.get(keyPageKey(dbName, rawKey, pageIndex)); - if (encoded == null) { - throw new ArchivePersistenceException("Exact serving epoch page is missing"); - } - return decodeEpochPage(encoded); - } catch (RocksDBException failure) { - throw new IOException("Failed to read exact serving epoch page", failure); + byte[] encoded = database.get(keyPageKey(dbName, rawKey, pageIndex)); + if (encoded == null) { + throw new ArchivePersistenceException("Exact serving epoch page is missing"); } + return decodeEpochPage(encoded); } - private static long applyExactPlan(RocksDB target, String generationId, + private static long applyExactPlan(StateArchiveIndexDatabase.Writer target, + String generationId, ServingIndexIncrementalPlan plan, long coverageFrom, byte[] sourceDigest, - ExactWriteFaultHook faultHook) throws IOException, RocksDBException { + ExactWriteFaultHook faultHook) throws IOException { Map> changes = new LinkedHashMap<>(); for (Map.Entry> database : plan.getChangesByDatabase().entrySet()) { @@ -674,44 +652,47 @@ private static long applyExactPlan(RocksDB target, String generationId, changes.computeIfAbsent(key, ignored -> new ArrayList<>()).add(change.getEpoch()); } } - try (WriteBatch batch = new WriteBatch(); WriteOptions writes = new WriteOptions() - .setSync(true)) { - for (Map.Entry> entry : changes.entrySet()) { - appendExactChanges(target, batch, entry.getKey(), entry.getValue()); - } - for (String database : plan.getParticipatingDatabases()) { - PersistentStoreCoverage coverage = new PersistentStoreCoverage(database, - coverageFrom, plan.getIndexedThrough(), plan.getHeadHash(), sourceDigest, - generationId, comparatorId(database)); - batch.put(storeCoverageKey(database), encodeCoverage(coverage)); - } - faultHook.beforeWrite(); - target.write(writes, batch); + List mutations = new ArrayList<>(); + for (Map.Entry> entry : changes.entrySet()) { + appendExactChanges(target, mutations, entry.getKey(), entry.getValue()); + } + for (String database : plan.getParticipatingDatabases()) { + PersistentStoreCoverage coverage = new PersistentStoreCoverage(database, + coverageFrom, plan.getIndexedThrough(), plan.getHeadHash(), sourceDigest, + generationId, comparatorId(database)); + mutations.add(StateArchiveIndexDatabase.put(storeCoverageKey(database), + encodeCoverage(coverage))); } + faultHook.beforeWrite(); + target.write(mutations, true); return changes.values().stream().mapToLong(List::size).sum(); } - private static void appendExactChanges(RocksDB target, WriteBatch batch, ExactKey key, - List appended) throws RocksDBException { + private static void appendExactChanges(StateArchiveIndexDatabase.Writer target, + List batch, ExactKey key, + List appended) throws IOException { byte[] metaKey = keyMetaKey(key.dbName, key.rawKey); byte[] existing = target.get(metaKey); KeyMeta meta = existing == null ? null : decodeKeyMeta(existing); if (meta == null) { requireStrictEpochs(appended, Long.MIN_VALUE); if (appended.size() <= INLINE_EPOCH_LIMIT) { - batch.put(metaKey, encodeKeyMeta(KeyMeta.inline(toArray(appended)))); + batch.add(StateArchiveIndexDatabase.put(metaKey, + encodeKeyMeta(KeyMeta.inline(toArray(appended))))); return; } writeAllPages(batch, key, appended, 0); - batch.put(metaKey, encodeKeyMeta(KeyMeta.paged(appended.size(), appended.get(0), - appended.get(appended.size() - 1)))); + batch.add(StateArchiveIndexDatabase.put(metaKey, + encodeKeyMeta(KeyMeta.paged(appended.size(), appended.get(0), + appended.get(appended.size() - 1))))); return; } requireStrictEpochs(appended, meta.lastEpoch); if (meta.mode == INLINE && meta.count + appended.size() <= INLINE_EPOCH_LIMIT) { List combined = asList(meta.inlineEpochs); combined.addAll(appended); - batch.put(metaKey, encodeKeyMeta(KeyMeta.inline(toArray(combined)))); + batch.add(StateArchiveIndexDatabase.put(metaKey, + encodeKeyMeta(KeyMeta.inline(toArray(combined))))); return; } if (meta.mode == INLINE) { @@ -726,17 +707,18 @@ private static void appendExactChanges(RocksDB target, WriteBatch batch, ExactKe combined.addAll(appended); writeAllPages(batch, key, combined, lastPageIndex); } - batch.put(metaKey, encodeKeyMeta(KeyMeta.paged(meta.count + appended.size(), - meta.firstEpoch, appended.get(appended.size() - 1)))); + batch.add(StateArchiveIndexDatabase.put(metaKey, + encodeKeyMeta(KeyMeta.paged(meta.count + appended.size(), + meta.firstEpoch, appended.get(appended.size() - 1))))); } - private static void writeAllPages(WriteBatch batch, ExactKey key, List epochs, - int firstPageIndex) throws RocksDBException { + private static void writeAllPages(List batch, + ExactKey key, List epochs, int firstPageIndex) { for (int start = 0, page = firstPageIndex; start < epochs.size(); start += EPOCHS_PER_PAGE, page++) { int end = Math.min(start + EPOCHS_PER_PAGE, epochs.size()); - batch.put(keyPageKey(key.dbName, key.rawKey, page), - encodeEpochPage(toArray(epochs.subList(start, end)))); + batch.add(StateArchiveIndexDatabase.put(keyPageKey(key.dbName, key.rawKey, page), + encodeEpochPage(toArray(epochs.subList(start, end))))); } } @@ -893,8 +875,8 @@ private static PersistentStoreCoverage decodeCoverage(byte[] encoded) { } } - private static void validateExactStoreCoverage(RocksDB database, Descriptor descriptor) - throws RocksDBException { + private static void validateExactStoreCoverage(StateArchiveIndexDatabase.Reader database, + Descriptor descriptor) throws IOException { for (String participant : descriptor.participants) { PersistentStoreCoverage coverage = decodeCoverage(database.get( storeCoverageKey(participant))); @@ -996,9 +978,8 @@ private static String comparatorId(String dbName) { ? "MARKET_PRICE_V1" : "UNSIGNED_RAW_V1"; } - private static Options exactOptions(boolean create) { - return new Options().setCreateIfMissing(create) - .setCompressionType(CompressionType.NO_COMPRESSION); + private static Engine configuredEngine() { + return StateArchiveCheckpointServingIndex.configuredEngine(); } private static void validateExactIdentity(String generationId, diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java index 78f4f9afbd2..69a5c7dc37d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.UUID; import org.tron.core.db2.core.CommonCheckpointMaterializer; @@ -144,6 +145,20 @@ public static CommonCheckpointTarget loadPublishedTarget(Path directory, return target; } + /** Returns the published target when present, validating its complete serving boundary once. */ + public static Optional loadPublishedTargetIfPresent(Path directory, + byte[] expectedFormatIdentity, Engine engine) throws IOException { + if (!Files.exists(directory.resolve(READABLE_FILE), LinkOption.NOFOLLOW_LINKS)) { + return Optional.empty(); + } + return Optional.of(loadPublishedTarget(directory, expectedFormatIdentity, engine)); + } + + /** Resolves the configured Archive index engine at runtime construction boundaries. */ + public static Engine configuredEngine() { + return StateArchiveCheckpointServingIndex.configuredEngine(); + } + @Override public synchronized void materialize(CommonCheckpointPayload payload, CommonCheckpointTarget target) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java index f74ea75e105..bd9ec5a7dd7 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadAdapter.java @@ -42,6 +42,15 @@ public static StateArchiveCheckpointReadAdapter open(Path archiveDirectory, StateArchiveCheckpointServingIndex.openReader(directory, admitted, engine)); } + /** Opens a target already validated and pinned by the common-checkpoint runtime. */ + public static StateArchiveCheckpointReadAdapter openTrusted(Path archiveDirectory, + CommonCheckpointTarget target, Engine engine) throws IOException { + Path directory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + return new StateArchiveCheckpointReadAdapter(admitted, + StateArchiveCheckpointServingIndex.openTrustedReader(directory, admitted, engine)); + } + /** Reconstructs the published target from disk before opening the exact-point reader. */ public static StateArchiveCheckpointReadAdapter open(Path archiveDirectory, byte[] expectedFormatIdentity) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java index 835768ae959..90b6e9569d7 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshot.java @@ -6,7 +6,9 @@ import java.util.Objects; import java.util.Optional; import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; +import org.tron.core.db2.core.CommonCheckpointTarget; import org.tron.core.db2.core.CommonCheckpointRuntimeOwner; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** Request-owned, point-only view over one published next-format checkpoint head. */ public final class StateArchiveCheckpointReadSnapshot implements ArchivePointSnapshot { @@ -55,6 +57,31 @@ public static StateArchiveCheckpointReadSnapshot pin(long targetBlock, } } + /** Pins a target already validated and bound by its owning common-checkpoint runtime. */ + public static StateArchiveCheckpointReadSnapshot pin(long targetBlock, + CommonCheckpointRuntimeOwner owner, Path archiveDirectory, + CommonCheckpointTarget publishedTarget, Engine engine, + PinnedLatestStateFactory latestFactory) throws IOException { + CommonCheckpointRuntimeOwner admittedOwner = Objects.requireNonNull(owner, "owner"); + CommonCheckpointRuntimeOwner.ReadLease lease = admittedOwner.acquireReadLease(); + StateArchiveCheckpointReadAdapter archive = null; + PinnedLatestState latest = null; + try { + archive = StateArchiveCheckpointReadAdapter.openTrusted(archiveDirectory, + publishedTarget, engine); + if (targetBlock < archive.getIndexedFrom() || targetBlock > archive.getIndexedThrough()) { + throw new IllegalArgumentException("checkpoint target block is outside indexed coverage"); + } + latest = Objects.requireNonNull(latestFactory, "latestFactory").pin( + archive.getIndexedThrough(), archive.getHeadHash()); + return new StateArchiveCheckpointReadSnapshot(targetBlock, lease, archive, + Objects.requireNonNull(latest, "pinned latest state")); + } catch (IOException | RuntimeException failure) { + closeAfterFailedPin(lease, archive, latest, failure); + throw failure; + } + } + /** Returns the first reverse-diff old value, or the same-request pinned latest value. */ public synchronized OldValue get(String dbName, byte[] physicalRawKey) throws IOException { ensureOpen(); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java index 001a7d04b6e..5a630d71dce 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java @@ -128,7 +128,12 @@ static Reader openReader(Path archiveDirectory, CommonCheckpointTarget target) static Reader openReader(Path archiveDirectory, CommonCheckpointTarget target, Engine engine) throws IOException { - return new Reader(archiveDirectory, target, engine); + return new Reader(archiveDirectory, target, engine, true); + } + + static Reader openTrustedReader(Path archiveDirectory, CommonCheckpointTarget target, + Engine engine) throws IOException { + return new Reader(archiveDirectory, target, engine, false); } static Engine configuredEngine() { @@ -296,11 +301,13 @@ static final class Reader implements AutoCloseable { private final Marker marker; private boolean closed; - private Reader(Path archiveDirectory, CommonCheckpointTarget target, Engine engine) - throws IOException { + private Reader(Path archiveDirectory, CommonCheckpointTarget target, Engine engine, + boolean validateEngine) throws IOException { this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); Objects.requireNonNull(target, "target"); - StateArchiveIndexEngineManifest.require(archiveDirectory.resolve(DIRECTORY), engine); + if (validateEngine) { + StateArchiveIndexEngineManifest.require(archiveDirectory.resolve(DIRECTORY), engine); + } StateArchiveIndexDatabase.Reader opened; try { opened = StateArchiveIndexDatabase.openReader(databasePath(archiveDirectory), engine); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java index 6f2f7c6cb00..f8b1876d424 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java @@ -4,12 +4,16 @@ import java.io.Closeable; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.OptionalLong; import org.iq80.leveldb.DB; import org.iq80.leveldb.DBIterator; import org.iq80.leveldb.ReadOptions; @@ -17,7 +21,7 @@ import org.tron.common.utils.DbOptionalsUtils; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; -/** Engine-neutral native store for the common-checkpoint Archive serving index. */ +/** Engine-neutral native store for Archive serving indexes. */ final class StateArchiveIndexDatabase { private static final Map LEVEL_DATABASES = new HashMap<>(); @@ -37,6 +41,56 @@ static Writer openWriter(Path directory, Engine engine) throws IOException { : new RocksWriter(path); } + static void checkpoint(Path source, Path target, Engine engine) throws IOException { + Path from = normalize(source); + Path to = normalize(target); + if (engine == Engine.ROCKSDB) { + RocksCheckpoint.create(from, to); + return; + } + checkpointLevel(from, to); + } + + private static void checkpointLevel(Path source, Path target) throws IOException { + SharedLevelDatabase shared = acquireLevel(source, false); + boolean suspended = false; + try { + shared.database.suspendCompactions(); + suspended = true; + Files.createDirectory(target); + try (java.util.stream.Stream entries = Files.list(source)) { + for (Path entry : (Iterable) entries::iterator) { + if (!Files.isRegularFile(entry, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + String name = entry.getFileName().toString(); + if ("LOCK".equals(name) || "LOG".equals(name) || "LOG.old".equals(name)) { + continue; + } + Path destination = target.resolve(name); + if (name.endsWith(".sst") || name.endsWith(".ldb")) { + Files.createLink(destination, entry); + } else { + Files.copy(entry, destination, StandardCopyOption.COPY_ATTRIBUTES); + try (java.nio.channels.FileChannel channel = java.nio.channels.FileChannel.open( + destination, java.nio.file.StandardOpenOption.WRITE)) { + channel.force(true); + } + } + } + } + HistorySegmentStore.syncDirectory(target); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while checkpointing LevelDB Archive index", failure); + } finally { + if (suspended) { + shared.database.resumeCompactions(); + } + releaseLevel(shared); + } + } + static Mutation put(byte[] key, byte[] value) { return new Mutation(key, value); } @@ -78,6 +132,10 @@ interface Reader extends Closeable { byte[] get(byte[] key) throws IOException; KeyValue seek(byte[] key) throws IOException; + + Cursor cursor() throws IOException; + + OptionalLong readLongProperty(String name) throws IOException; } interface Writer extends Closeable { @@ -85,6 +143,15 @@ interface Writer extends Closeable { byte[] get(byte[] key) throws IOException; void write(List mutations) throws IOException; + + void write(List mutations, boolean sync) throws IOException; + } + + interface Cursor extends Closeable { + + void seek(byte[] key) throws IOException; + + KeyValue next() throws IOException; } static final class Mutation { @@ -167,6 +234,16 @@ public KeyValue seek(byte[] key) throws IOException { } } + @Override + public Cursor cursor() { + return new LevelCursor(shared.database.iterator(reads)); + } + + @Override + public OptionalLong readLongProperty(String name) { + return OptionalLong.empty(); + } + @Override public void close() throws IOException { if (!closed) { @@ -179,8 +256,6 @@ public void close() throws IOException { private static final class LevelWriter implements Writer { private final SharedLevelDatabase shared; - private final org.iq80.leveldb.WriteOptions writes = - new org.iq80.leveldb.WriteOptions().sync(true); private boolean closed; private LevelWriter(SharedLevelDatabase shared) { @@ -194,11 +269,16 @@ public byte[] get(byte[] key) { @Override public void write(List mutations) throws IOException { + write(mutations, true); + } + + @Override + public void write(List mutations, boolean sync) throws IOException { try (org.iq80.leveldb.WriteBatch batch = shared.database.createWriteBatch()) { for (Mutation mutation : mutations) { batch.put(mutation.key, mutation.value); } - shared.database.write(batch, writes); + shared.database.write(batch, new org.iq80.leveldb.WriteOptions().sync(sync)); } } @@ -211,6 +291,33 @@ public void close() throws IOException { } } + private static final class LevelCursor implements Cursor { + private final DBIterator iterator; + + private LevelCursor(DBIterator iterator) { + this.iterator = iterator; + } + + @Override + public void seek(byte[] key) { + iterator.seek(key); + } + + @Override + public KeyValue next() { + if (!iterator.hasNext()) { + return null; + } + Map.Entry entry = iterator.next(); + return new KeyValue(entry.getKey(), entry.getValue()); + } + + @Override + public void close() throws IOException { + iterator.close(); + } + } + private static final class RocksReader implements Reader { static { org.rocksdb.RocksDB.loadLibrary(); @@ -254,6 +361,20 @@ public KeyValue seek(byte[] key) throws IOException { } } + @Override + public Cursor cursor() { + return new RocksCursor(database, new org.rocksdb.ReadOptions()); + } + + @Override + public OptionalLong readLongProperty(String name) { + try { + return OptionalLong.of(database.getLongProperty(name)); + } catch (org.rocksdb.RocksDBException | IllegalArgumentException failure) { + return OptionalLong.empty(); + } + } + @Override public void close() { if (!closed) { @@ -272,7 +393,6 @@ private static final class RocksWriter implements Writer { private final org.rocksdb.Options options = new org.rocksdb.Options().setCreateIfMissing(true) .setCompressionType(org.rocksdb.CompressionType.NO_COMPRESSION); - private final org.rocksdb.WriteOptions writes = new org.rocksdb.WriteOptions().setSync(true); private final org.rocksdb.RocksDB database; private boolean closed; @@ -280,7 +400,6 @@ private RocksWriter(Path directory) throws IOException { try { database = org.rocksdb.RocksDB.open(options, directory.toString()); } catch (org.rocksdb.RocksDBException | RuntimeException failure) { - writes.close(); options.close(); throw new IOException("Failed to open RocksDB Archive serving index", failure); } @@ -297,11 +416,18 @@ public byte[] get(byte[] key) throws IOException { @Override public void write(List mutations) throws IOException { + write(mutations, true); + } + + @Override + public void write(List mutations, boolean sync) throws IOException { try (org.rocksdb.WriteBatch batch = new org.rocksdb.WriteBatch()) { for (Mutation mutation : mutations) { batch.put(mutation.key, mutation.value); } - database.write(writes, batch); + try (org.rocksdb.WriteOptions selected = new org.rocksdb.WriteOptions().setSync(sync)) { + database.write(selected, batch); + } } catch (org.rocksdb.RocksDBException failure) { throw new IOException("Failed to write RocksDB Archive serving index", failure); } @@ -312,9 +438,60 @@ public void close() { if (!closed) { closed = true; database.close(); - writes.close(); options.close(); } } } + + private static final class RocksCursor implements Cursor { + private final org.rocksdb.ReadOptions reads; + private final org.rocksdb.RocksIterator iterator; + + private RocksCursor(org.rocksdb.RocksDB database, org.rocksdb.ReadOptions reads) { + this.reads = reads; + this.iterator = database.newIterator(reads); + } + + @Override + public void seek(byte[] key) { + iterator.seek(key); + } + + @Override + public KeyValue next() throws IOException { + if (!iterator.isValid()) { + try { + iterator.status(); + } catch (org.rocksdb.RocksDBException failure) { + throw new IOException("Failed to scan RocksDB Archive serving index", failure); + } + return null; + } + KeyValue value = new KeyValue(iterator.key(), iterator.value()); + iterator.next(); + return value; + } + + @Override + public void close() { + iterator.close(); + reads.close(); + } + } + + private static final class RocksCheckpoint { + static { + org.rocksdb.RocksDB.loadLibrary(); + } + + private static void create(Path source, Path target) throws IOException { + try (org.rocksdb.Options options = new org.rocksdb.Options().setCreateIfMissing(false); + org.rocksdb.RocksDB database = org.rocksdb.RocksDB.open(options, source.toString()); + org.rocksdb.Checkpoint checkpoint = org.rocksdb.Checkpoint.create(database)) { + checkpoint.createCheckpoint(target.toString()); + } catch (org.rocksdb.RocksDBException failure) { + throw new IOException("Failed to checkpoint RocksDB Archive serving index", failure); + } + } + } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexEngineManifest.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexEngineManifest.java index b40412a1853..e03ec911304 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexEngineManifest.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexEngineManifest.java @@ -1,6 +1,5 @@ package org.tron.core.db2.archive; -import com.google.common.hash.Hashing; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -22,14 +21,26 @@ final class StateArchiveIndexEngineManifest { static final String FILE = "ENGINE"; private static final String TEMP = "ENGINE.tmp"; private static final int MAGIC = 0x53414945; // SAIE - private static final short VERSION = 1; - private static final int DIGEST_LENGTH = 32; - private static final int ENCODED_LENGTH = Integer.BYTES + 2 * Short.BYTES + DIGEST_LENGTH; + private static final short LEGACY_SHA_VERSION = 1; + private static final short VERSION = 2; + private static final int BODY_LENGTH = Integer.BYTES + 2 * Short.BYTES; + private static final int CHECKSUM_LENGTH = Integer.BYTES; + private static final int LEGACY_DIGEST_LENGTH = 32; + private static final int ENCODED_LENGTH = BODY_LENGTH + CHECKSUM_LENGTH; private StateArchiveIndexEngineManifest() { } static void openOrCreate(Path directory, Engine engine) throws IOException { + openOrCreate(directory, engine, false); + } + + static void openOrCreateLegacy(Path directory, Engine engine) throws IOException { + openOrCreate(directory, engine, true); + } + + private static void openOrCreate(Path directory, Engine engine, boolean admitLegacyRocks) + throws IOException { Path root = Objects.requireNonNull(directory, "directory"); Engine selected = Objects.requireNonNull(engine, "engine"); Files.createDirectories(root); @@ -38,12 +49,14 @@ static void openOrCreate(Path directory, Engine engine) throws IOException { } Path manifest = root.resolve(FILE); if (Files.exists(manifest, LinkOption.NOFOLLOW_LINKS)) { - requireFile(manifest, selected); + require(root, selected); return; } Path database = root.resolve("keys"); if (Files.exists(database, LinkOption.NOFOLLOW_LINKS)) { - throw new IOException("State Archive serving index has no engine identity"); + if (!admitLegacyRocks || selected != Engine.ROCKSDB) { + throw new IOException("State Archive serving index has no engine identity"); + } } byte[] encoded = encode(selected); Path temporary = root.resolve(TEMP); @@ -63,32 +76,48 @@ static void openOrCreate(Path directory, Engine engine) throws IOException { } static void require(Path directory, Engine engine) throws IOException { - requireFile(Objects.requireNonNull(directory, "directory").resolve(FILE), - Objects.requireNonNull(engine, "engine")); + Engine expected = Objects.requireNonNull(engine, "engine"); + if (load(directory) != expected) { + throw new IOException("State Archive serving index engine differs: expected " + expected); + } } - private static void requireFile(Path manifest, Engine engine) throws IOException { + static Engine load(Path directory) throws IOException { + Path manifest = Objects.requireNonNull(directory, "directory").resolve(FILE); if (!Files.isRegularFile(manifest, LinkOption.NOFOLLOW_LINKS)) { throw new IOException("State Archive serving engine identity is missing"); } byte[] encoded = Files.readAllBytes(manifest); - if (encoded.length != ENCODED_LENGTH) { + if (encoded.length != ENCODED_LENGTH + && encoded.length != BODY_LENGTH + LEGACY_DIGEST_LENGTH) { throw new IOException("State Archive serving engine identity length is invalid"); } - int bodyLength = encoded.length - DIGEST_LENGTH; - byte[] body = Arrays.copyOf(encoded, bodyLength); - if (!Arrays.equals(Arrays.copyOfRange(encoded, bodyLength, encoded.length), - Hashing.sha256().hashBytes(body).asBytes())) { - throw new IOException("State Archive serving engine identity checksum differs"); - } + byte[] body = Arrays.copyOf(encoded, BODY_LENGTH); try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(body))) { - if (input.readInt() != MAGIC || input.readShort() != VERSION) { + int magic = input.readInt(); + short version = input.readShort(); + if (magic != MAGIC || version != VERSION && version != LEGACY_SHA_VERSION) { throw new IOException("State Archive serving engine identity is unsupported"); } + if (version == VERSION) { + if (encoded.length != ENCODED_LENGTH + || java.nio.ByteBuffer.wrap(encoded, BODY_LENGTH, CHECKSUM_LENGTH).getInt() + != com.google.common.hash.Hashing.crc32c().hashBytes(body).asInt()) { + throw new IOException("State Archive serving engine identity checksum differs"); + } + } else if (encoded.length != BODY_LENGTH + LEGACY_DIGEST_LENGTH + || !Arrays.equals(Arrays.copyOfRange(encoded, BODY_LENGTH, encoded.length), + com.google.common.hash.Hashing.sha256().hashBytes(body).asBytes())) { + throw new IOException("State Archive serving engine identity checksum differs"); + } int tag = input.readUnsignedShort(); - if (tag != tag(engine)) { - throw new IOException("State Archive serving index engine differs: expected " + engine); + if (tag == tag(Engine.LEVELDB)) { + return Engine.LEVELDB; + } + if (tag == tag(Engine.ROCKSDB)) { + return Engine.ROCKSDB; } + throw new IOException("State Archive serving engine identity tag is unsupported"); } } @@ -101,7 +130,7 @@ private static byte[] encode(Engine engine) { output.writeShort(tag(engine)); output.flush(); byte[] body = bytes.toByteArray(); - output.write(Hashing.sha256().hashBytes(body).asBytes()); + output.writeInt(com.google.common.hash.Hashing.crc32c().hashBytes(body).asInt()); output.flush(); return bytes.toByteArray(); } catch (IOException impossible) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java index 2ed92e30b0b..3931f0f9ffe 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java @@ -582,7 +582,8 @@ private PersistentServingKeyIndexCatalog openOrCreateServingCatalog( Path root = archiveDirectory.resolve("serving-index"); if (Files.exists(root, LinkOption.NOFOLLOW_LINKS)) { PersistentServingKeyIndexCatalog catalog = - PersistentServingKeyIndexCatalog.open(root, this::afterCatalogStage); + PersistentServingKeyIndexCatalog.open(root, + StateArchiveCheckpointServingIndex.configuredEngine(), this::afterCatalogStage); try { upgradeServingExactIndex(writer, catalog); return catalog; diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java index b6888763618..fb387f97ee5 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java @@ -6,7 +6,9 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import org.tron.core.db2.archive.StateArchiveCheckpointMaterializer; import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** Isolated composition boundary for the next-format common-checkpoint runtime. */ public final class CommonCheckpointRuntime implements AutoCloseable { @@ -15,13 +17,22 @@ public final class CommonCheckpointRuntime implements AutoCloseable { private final List databases; private final Path archiveDirectory; private final byte[] formatIdentity; + private final Engine engine; private final StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory; private final CommonCheckpointPayloadFactory payloadFactory = new CommonCheckpointPayloadFactory(); private final CommonCheckpointSnapshotRebaser rebaser = new CommonCheckpointSnapshotRebaser(); + private CommonCheckpointTarget publishedTarget; public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, Path archiveDirectory, byte[] formatIdentity, StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory) { + this(owner, databases, archiveDirectory, formatIdentity, + StateArchiveCheckpointMaterializer.configuredEngine(), latestFactory); + } + + public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, + Path archiveDirectory, byte[] formatIdentity, Engine engine, + StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory) { this.owner = Objects.requireNonNull(owner, "owner"); this.databases = new ArrayList<>(Objects.requireNonNull(databases, "databases")); if (this.databases.isEmpty() || this.databases.contains(null)) { @@ -29,31 +40,42 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List rebaser.rebase(databases, target, flushCount)); + publishedTarget = target; return target; } /** Pins one point-only historical request under the same publication gate. */ - public StateArchiveCheckpointReadSnapshot pinPoint(long targetBlock) throws IOException { + public synchronized StateArchiveCheckpointReadSnapshot pinPoint(long targetBlock) + throws IOException { + CommonCheckpointTarget target = publishedTarget; + if (target == null) { + throw new IOException("State Archive has no published common-checkpoint target"); + } return StateArchiveCheckpointReadSnapshot.pin(targetBlock, owner, archiveDirectory, - formatIdentity, latestFactory); + target, engine, latestFactory); } public CommonCheckpointRuntimeOwner.State getState() { diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index b4a5ce3267e..e72f46542d4 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -629,7 +629,6 @@ public void init() { // init liteFullNode initLiteNode(); - requireSingleEngineArchiveMode(Args.getInstance().getStorage()); if (Args.getInstance().getStorage().isCommonCheckpointEnabled()) { initCommonCheckpoint(); } else { @@ -678,15 +677,6 @@ public void init() { maxFlushCount = CommonParameter.getInstance().getStorage().getMaxFlushCount(); } - static void requireSingleEngineArchiveMode(org.tron.core.config.args.Storage storage) { - org.tron.core.config.args.Storage admitted = Objects.requireNonNull(storage, "storage"); - if (admitted.isStateArchiveEnabled() && !admitted.isCommonCheckpointEnabled() - && !"ROCKSDB".equalsIgnoreCase(admitted.getDbEngine())) { - throw new IllegalStateException("LevelDB State Archive requires common checkpoint; " - + "legacy serving generations are RocksDB-only and mixed-engine startup is forbidden"); - } - } - private void initStateArchive() { org.tron.core.config.args.Storage storage = Args.getInstance().getStorage(); Path archiveDirectory = Paths.get(Args.getInstance().getOutputDirectory(), @@ -877,7 +867,7 @@ private void initCommonCheckpoint() { PathStatePhysicalOverlayHead admittedOwner = pathOwner; attachment = CommonCheckpointRuntimeAttachment.open(true, () -> new CommonCheckpointRuntime(new CommonCheckpointRuntimeOwner(coordinator), - snapshots.getDbs(), archiveDirectory, formatIdentity, latest::pin)); + snapshots.getDbs(), archiveDirectory, formatIdentity, engine, latest::pin)); canonical = currentCanonicalBlockMeta(); if (Files.isRegularFile(pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), diff --git a/framework/src/test/java/org/tron/core/db/ManagerArchiveEngineModeTest.java b/framework/src/test/java/org/tron/core/db/ManagerArchiveEngineModeTest.java deleted file mode 100644 index 0d50d433d6f..00000000000 --- a/framework/src/test/java/org/tron/core/db/ManagerArchiveEngineModeTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.tron.core.db; - -import static org.junit.Assert.assertThrows; - -import org.junit.Test; -import org.tron.core.config.args.Storage; - -public class ManagerArchiveEngineModeTest { - - @Test - public void rejectsLegacyLevelDbArchiveButAcceptsSingleEngineModes() { - Storage storage = storage("LEVELDB", true, false); - assertThrows(IllegalStateException.class, - () -> Manager.requireSingleEngineArchiveMode(storage)); - - Manager.requireSingleEngineArchiveMode(storage("LEVELDB", true, true)); - Manager.requireSingleEngineArchiveMode(storage("ROCKSDB", true, false)); - Manager.requireSingleEngineArchiveMode(storage("LEVELDB", false, false)); - } - - private static Storage storage(String engine, boolean archive, boolean commonCheckpoint) { - Storage storage = new Storage(); - storage.setDbEngine(engine); - storage.setStateArchiveEnabled(archive); - storage.setCommonCheckpointEnabled(commonCheckpoint); - return storage; - } -} diff --git a/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java b/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java index 291d9bd4da8..1b7db89ed41 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/PersistentServingKeyIndexGenerationTest.java @@ -26,6 +26,7 @@ import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedHistory; import org.tron.core.db2.archive.ArchiveReadSnapshot.PinnedLatestState; import org.tron.core.db2.archive.HistoryIndexRecord.KeyGroup; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; public class PersistentServingKeyIndexGenerationTest { @@ -135,10 +136,16 @@ public void incrementallyPublishesExact27PagesAndCoverageFromCheckpoint() throws assertEquals(6, statistics.getStores().get("account").getChangeEntryCount()); assertEquals(0, statistics.getStores().get("abi").getChangeEntryCount()); assertTrue(statistics.getStores().get("abi").getLogicalBytes() > 0); - assertTrue(statistics.getEngine().getEstimatedLiveDataBytes().isAvailable()); - assertTrue(statistics.getEngine().getTotalSstBytes().isAvailable()); - assertTrue(statistics.getEngine().getPendingCompactionBytes().isAvailable()); - assertTrue(statistics.getEngine().getEstimatedLiveDataBytes().getValue() >= 0); + if (first.getEngine() == Engine.ROCKSDB) { + assertTrue(statistics.getEngine().getEstimatedLiveDataBytes().isAvailable()); + assertTrue(statistics.getEngine().getTotalSstBytes().isAvailable()); + assertTrue(statistics.getEngine().getPendingCompactionBytes().isAvailable()); + assertTrue(statistics.getEngine().getEstimatedLiveDataBytes().getValue() >= 0); + } else { + assertFalse(statistics.getEngine().getEstimatedLiveDataBytes().isAvailable()); + assertFalse(statistics.getEngine().getTotalSstBytes().isAvailable()); + assertFalse(statistics.getEngine().getPendingCompactionBytes().isAvailable()); + } PersistentServingKeyIndexGeneration.GenerationStatistics unavailable = first.inspectStatistics(ignored -> OptionalLong.empty()); assertFalse(unavailable.getEngine().getEstimatedLiveDataBytes().isAvailable()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java index 2138a274306..868e0b49c26 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java @@ -1,10 +1,13 @@ package org.tron.core.db2.archive; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; +import com.google.common.hash.Hashing; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -24,6 +27,7 @@ public void supportsConfiguredEngineAndRejectsEngineDrift() throws Exception { Path root = temporaryFolder.newFolder(engine.name().toLowerCase()).toPath(); Path database = root.resolve("keys"); StateArchiveIndexEngineManifest.openOrCreate(root, engine); + assertEquals(12, Files.size(root.resolve(StateArchiveIndexEngineManifest.FILE))); try (StateArchiveIndexDatabase.Writer writer = StateArchiveIndexDatabase.openWriter(database, engine)) { writer.write(Arrays.asList( @@ -50,4 +54,21 @@ public void rejectsExistingDatabaseWithoutEngineIdentity() throws Exception { assertThrows(IOException.class, () -> StateArchiveIndexEngineManifest.openOrCreate(root, Engine.LEVELDB)); } + + @Test + public void acceptsPriorShaIdentityAndRejectsCrcCorruption() throws Exception { + Path legacy = temporaryFolder.newFolder("legacy-sha").toPath(); + byte[] body = ByteBuffer.allocate(8).putInt(0x53414945).putShort((short) 1) + .putShort((short) 2).array(); + Files.write(legacy.resolve(StateArchiveIndexEngineManifest.FILE), + ByteBuffer.allocate(40).put(body).put(Hashing.sha256().hashBytes(body).asBytes()).array()); + assertEquals(Engine.ROCKSDB, StateArchiveIndexEngineManifest.load(legacy)); + + Path current = temporaryFolder.newFolder("current-crc").toPath(); + StateArchiveIndexEngineManifest.openOrCreate(current, Engine.LEVELDB); + byte[] corrupt = Files.readAllBytes(current.resolve(StateArchiveIndexEngineManifest.FILE)); + corrupt[7] = 2; + Files.write(current.resolve(StateArchiveIndexEngineManifest.FILE), corrupt); + assertThrows(IOException.class, () -> StateArchiveIndexEngineManifest.load(current)); + } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index b3f22f5b929..cfc80d6ec40 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -64,6 +64,7 @@ import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.db2.core.SnapshotRoot; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; import org.tron.core.store.AccountAssetStore; import org.tron.core.store.CheckTmpStore; import org.tron.core.store.DynamicPropertiesStore; @@ -241,6 +242,11 @@ public void managerBootstrapsFreshBaseAndContinuesNormalFlush() throws Exception assertEquals(head, manager.getStateArchiveRuntime().getRecoveredHead()); assertEquals(0, manager.getStateArchiveRuntime().getStartupRecoveryActionCount()); assertServingFixedPoint(archive, head, 6); + try (PersistentServingKeyIndexCatalog catalog = + PersistentServingKeyIndexCatalog.open(archive.resolve("serving-index")); + PersistentServingKeyIndexGeneration serving = catalog.pin()) { + assertEquals(Engine.valueOf(engine), serving.getEngine()); + } assertEquals(head.getEpoch(), snapshots.getArchiveReadableEpoch()); assertTrue(Files.isRegularFile(archive.resolve("MANIFEST"))); assertFalse(Files.exists(archive.resolve("participants"))); @@ -959,11 +965,12 @@ public void managerRunsTwoNormalFlushTargetsThroughExact27FixedPoint() throws Ex assertEquals(27, inspection.getGeneration().getStores().size()); assertEquals(epoch, inspection.getGeneration().getIndexedThrough()); assertTrue(inspection.getGeneration().getApparentBytes() > 0); - assertTrue(inspection.getGeneration().getEngine().getEstimatedLiveDataBytes() - .isAvailable()); - assertTrue(inspection.getGeneration().getEngine().getTotalSstBytes().isAvailable()); - assertTrue(inspection.getGeneration().getEngine().getPendingCompactionBytes() - .isAvailable()); + assertEquals("ROCKSDB".equals(engine), inspection.getGeneration().getEngine() + .getEstimatedLiveDataBytes().isAvailable()); + assertEquals("ROCKSDB".equals(engine), inspection.getGeneration().getEngine() + .getTotalSstBytes().isAvailable()); + assertEquals("ROCKSDB".equals(engine), inspection.getGeneration().getEngine() + .getPendingCompactionBytes().isAvailable()); setField(snapshots, "size", 0); } @@ -1175,16 +1182,18 @@ public void substitutedAllStoreServingIndexFailsBeforeRuntimeAttachment() throws Path foreignArchive = temporaryFolder.newFolder("foreign-serving-history").toPath(); Path foreignShadow = output.resolve("foreign-serving-shadow"); - try (ArchiveHistoryWriter foreign = new ArchiveHistoryWriter( - foreignArchive, 4096, ArchiveStoreScope.getStateDatabases())) { - foreign.accept(new BlockReverseDiff(head.getMeta(), Collections.singletonList( - new BlockReverseDiff.DbGroup("proposal", Collections.singletonList( - new BlockReverseDiff.Entry(new byte[]{9, 9}, OldValue.absent())))))); - try (PersistentServingKeyIndexGeneration ignored = - foreign.buildServingGeneration(foreignShadow, "foreign")) { - // Catalog publication reopens the generation after this build handle is closed. + withArchiveConfig(output, "ROCKSDB", true, () -> { + try (ArchiveHistoryWriter foreign = new ArchiveHistoryWriter( + foreignArchive, 4096, ArchiveStoreScope.getStateDatabases())) { + foreign.accept(new BlockReverseDiff(head.getMeta(), Collections.singletonList( + new BlockReverseDiff.DbGroup("proposal", Collections.singletonList( + new BlockReverseDiff.Entry(new byte[]{9, 9}, OldValue.absent())))))); + try (PersistentServingKeyIndexGeneration ignored = + foreign.buildServingGeneration(foreignShadow, "foreign")) { + // Catalog publication reopens the generation after this build handle is closed. + } } - } + }); try (PersistentServingKeyIndexCatalog catalog = PersistentServingKeyIndexCatalog.open(archive.resolve("serving-index"))) { assertTrue(catalog.publish(catalog.getCurrentGenerationId(), foreignShadow)); diff --git a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java index a3d43757c57..eae8369918a 100644 --- a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java @@ -324,7 +324,8 @@ public void runtimeComposesStartupCheckpointRebaseAndPointQuery() throws Excepti new CommonCheckpointFile(root.resolve("wal")), chainbase, pathState, archive); CommonCheckpointRuntime runtime = new CommonCheckpointRuntime( new CommonCheckpointRuntimeOwner(coordinator), databases, root.resolve("archive"), - format, (blockNumber, blockHash) -> new TestLatest(code, blockNumber, blockHash)); + format, Engine.LEVELDB, + (blockNumber, blockHash) -> new TestLatest(code, blockNumber, blockHash)); assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.NO_CHECKPOINT, runtime.recoverBeforeServing()); @@ -338,6 +339,12 @@ public void runtimeComposesStartupCheckpointRebaseAndPointQuery() throws Excepti try (StateArchiveCheckpointReadSnapshot snapshot = runtime.pinPoint(1)) { assertArrayEquals(new byte[]{2}, snapshot.get("code", new byte[]{1}).getValue()); } + java.nio.file.Files.delete(root.resolve("archive/checkpoint-serving-index/ENGINE")); + try (StateArchiveCheckpointReadSnapshot snapshot = runtime.pinPoint(1)) { + assertArrayEquals(new byte[]{2}, snapshot.get("code", new byte[]{1}).getValue()); + } + assertThrows(IOException.class, () -> StateArchiveCheckpointMaterializer.loadPublishedTarget( + root.resolve("archive"), format, Engine.LEVELDB)); runtime.close(); assertEquals(CommonCheckpointRuntimeOwner.State.CLOSED, runtime.getState()); } From d0a7802dc2fa24360a5f3ea620ed200239d392d8 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 5 Sep 2026 16:31:13 +0800 Subject: [PATCH 116/161] feat(db): configure archive database profiles Apply configurable small, large, and giant native database settings to the 28 PathState stores while retaining the globally selected storage engine.\n\nConfigure the Archive serving-key index for both engines and share RocksDB handles across concurrent readers, writers, and checkpoints. --- .../archive/StateArchiveIndexDatabase.java | 242 +++++++++++++----- .../stateroot/PathStateNativeNodeStore.java | 93 ++++++- .../stateroot/PathStatePhysicalStoreSet.java | 56 +++- .../org/tron/core/config/args/Storage.java | 8 + .../tron/core/config/args/StorageConfig.java | 74 ++++++ common/src/main/resources/reference.conf | 70 +++++ .../core/config/args/StorageConfigTest.java | 32 +++ .../java/org/tron/core/config/args/Args.java | 3 + .../tron/core/config/args/StorageTest.java | 12 + .../StateArchiveIndexDatabaseTest.java | 30 +++ .../PathStateNativeNodeStoreTest.java | 40 +++ 11 files changed, 576 insertions(+), 84 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java index f8b1876d424..8daa4a05bfc 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java @@ -18,41 +18,48 @@ import org.iq80.leveldb.DBIterator; import org.iq80.leveldb.ReadOptions; import org.iq80.leveldb.Snapshot; -import org.tron.common.utils.DbOptionalsUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tron.common.parameter.CommonParameter; +import org.tron.core.config.args.StorageConfig.NativeDbConfig; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** Engine-neutral native store for Archive serving indexes. */ final class StateArchiveIndexDatabase { + private static final Logger logger = LoggerFactory.getLogger("DB"); private static final Map LEVEL_DATABASES = new HashMap<>(); + private static final Map ROCKS_DATABASES = new HashMap<>(); private StateArchiveIndexDatabase() { } static Reader openReader(Path directory, Engine engine) throws IOException { Path path = normalize(directory); - return engine == Engine.LEVELDB ? new LevelReader(acquireLevel(path, false)) - : new RocksReader(path); + NativeDbConfig config = configuredOptions(); + return engine == Engine.LEVELDB ? new LevelReader(acquireLevel(path, false, config)) + : new RocksReader(acquireRocks(path, false, config)); } static Writer openWriter(Path directory, Engine engine) throws IOException { Path path = normalize(directory); - return engine == Engine.LEVELDB ? new LevelWriter(acquireLevel(path, true)) - : new RocksWriter(path); + NativeDbConfig config = configuredOptions(); + return engine == Engine.LEVELDB ? new LevelWriter(acquireLevel(path, true, config)) + : new RocksWriter(acquireRocks(path, true, config)); } static void checkpoint(Path source, Path target, Engine engine) throws IOException { Path from = normalize(source); Path to = normalize(target); if (engine == Engine.ROCKSDB) { - RocksCheckpoint.create(from, to); + checkpointRocks(from, to); return; } checkpointLevel(from, to); } private static void checkpointLevel(Path source, Path target) throws IOException { - SharedLevelDatabase shared = acquireLevel(source, false); + SharedLevelDatabase shared = acquireLevel(source, false, configuredOptions()); boolean suspended = false; try { shared.database.suspendCompactions(); @@ -99,12 +106,20 @@ private static Path normalize(Path directory) { return Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); } - private static synchronized SharedLevelDatabase acquireLevel(Path directory, boolean create) + private static synchronized SharedLevelDatabase acquireLevel(Path directory, boolean create, + NativeDbConfig config) throws IOException { SharedLevelDatabase shared = LEVEL_DATABASES.get(directory); if (shared == null) { - org.iq80.leveldb.Options options = DbOptionalsUtils.createDefaultDbOptions() - .createIfMissing(create); + org.iq80.leveldb.Options options = new org.iq80.leveldb.Options() + .createIfMissing(create) + .paranoidChecks(true) + .verifyChecksums(true) + .compressionType(org.iq80.leveldb.CompressionType.SNAPPY) + .blockSize(config.getBlockSize()) + .writeBufferSize(config.getWriteBufferSize()) + .cacheSize(config.getCacheSize()) + .maxOpenFiles(config.getMaxOpenFiles()); try { shared = new SharedLevelDatabase(directory, factory.open(directory.toFile(), options)); } catch (IOException | RuntimeException failure) { @@ -114,11 +129,64 @@ private static synchronized SharedLevelDatabase acquireLevel(Path directory, boo throw failure; } LEVEL_DATABASES.put(directory, shared); + logger.info("Archive serving index opened: directory={}, engine=LEVELDB, blockBytes={}, " + + "writeBufferBytes={}, cacheBytes={}, maxOpenFiles={}", directory, + config.getBlockSize(), config.getWriteBufferSize(), config.getCacheSize(), + config.getMaxOpenFiles()); } shared.references++; return shared; } + private static synchronized SharedRocksDatabase acquireRocks(Path directory, boolean create, + NativeDbConfig config) throws IOException { + SharedRocksDatabase shared = ROCKS_DATABASES.get(directory); + if (shared == null) { + RocksResources resources = new RocksResources(config, create); + try { + shared = new SharedRocksDatabase(directory, + org.rocksdb.RocksDB.open(resources.options, directory.toString()), resources); + } catch (org.rocksdb.RocksDBException | RuntimeException failure) { + resources.close(); + throw new IOException("Failed to open RocksDB Archive serving index", failure); + } + ROCKS_DATABASES.put(directory, shared); + logger.info("Archive serving index opened: directory={}, engine=ROCKSDB, blockBytes={}, " + + "writeBufferBytes={}, cacheBytes={}, maxOpenFiles={}", directory, + config.getBlockSize(), config.getWriteBufferSize(), config.getCacheSize(), + config.getMaxOpenFiles()); + } + shared.references++; + return shared; + } + + private static synchronized void releaseRocks(SharedRocksDatabase shared) { + if (--shared.references != 0) { + return; + } + ROCKS_DATABASES.remove(shared.directory); + shared.database.close(); + shared.resources.close(); + } + + private static void checkpointRocks(Path source, Path target) throws IOException { + SharedRocksDatabase shared = acquireRocks(source, false, configuredOptions()); + try (org.rocksdb.Checkpoint checkpoint = org.rocksdb.Checkpoint.create(shared.database)) { + checkpoint.createCheckpoint(target.toString()); + } catch (org.rocksdb.RocksDBException failure) { + throw new IOException("Failed to checkpoint RocksDB Archive serving index", failure); + } finally { + releaseRocks(shared); + } + } + + private static NativeDbConfig configuredOptions() { + org.tron.core.config.args.Storage storage = CommonParameter.getInstance().getStorage(); + NativeDbConfig config = storage == null ? null + : storage.getStateArchiveServingIndexDbSettings(); + return config == null ? NativeDbConfig.large() : config; + } + private static synchronized void releaseLevel(SharedLevelDatabase shared) throws IOException { if (--shared.references != 0) { return; @@ -193,6 +261,66 @@ private SharedLevelDatabase(Path directory, DB database) { } } + private static final class SharedRocksDatabase { + private final Path directory; + private final org.rocksdb.RocksDB database; + private final RocksResources resources; + private int references; + + private SharedRocksDatabase(Path directory, org.rocksdb.RocksDB database, + RocksResources resources) { + this.directory = directory; + this.database = database; + this.resources = resources; + } + } + + private static final class RocksResources implements Closeable { + private final org.rocksdb.LRUCache cache; + private final org.rocksdb.BloomFilter filter; + private final org.rocksdb.Options options; + + private RocksResources(NativeDbConfig config, boolean create) { + org.rocksdb.RocksDB.loadLibrary(); + cache = new org.rocksdb.LRUCache(config.getCacheSize()); + filter = new org.rocksdb.BloomFilter(config.getBloomBitsPerKey(), false); + org.rocksdb.BlockBasedTableConfig table = new org.rocksdb.BlockBasedTableConfig() + .setBlockSize(config.getBlockSize()) + .setChecksumType(org.rocksdb.ChecksumType.kCRC32c) + .setBlockCache(cache) + .setCacheIndexAndFilterBlocks(true) + .setPinL0FilterAndIndexBlocksInCache(false) + .setWholeKeyFiltering(true) + .setFilter(filter); + options = new org.rocksdb.Options() + .setCreateIfMissing(create) + .setParanoidChecks(true) + .setCompressionType(org.rocksdb.CompressionType.SNAPPY_COMPRESSION) + .setWriteBufferSize(config.getWriteBufferSize()) + .setMaxWriteBufferNumber(config.getMaxWriteBufferNumber()) + .setMinWriteBufferNumberToMerge(1) + .setMaxOpenFiles(config.getMaxOpenFiles()) + .setNumLevels(config.getLevelNumber()) + .setLevelCompactionDynamicLevelBytes(true) + .setLevel0FileNumCompactionTrigger(config.getLevel0FileNumCompactionTrigger()) + .setLevel0SlowdownWritesTrigger(config.getLevel0SlowdownWritesTrigger()) + .setLevel0StopWritesTrigger(config.getLevel0StopWritesTrigger()) + .setMaxBackgroundCompactions(config.getBackgroundCompactions()) + .setMaxBackgroundFlushes(config.getBackgroundFlushes()) + .setTargetFileSizeBase(config.getTargetFileSizeBase()) + .setMaxBytesForLevelBase(config.getMaxBytesForLevelBase()) + .setMaxBytesForLevelMultiplier(config.getMaxBytesForLevelMultiplier()) + .setTableFormatConfig(table); + } + + @Override + public void close() { + options.close(); + filter.close(); + cache.close(); + } + } + private static final class LevelReader implements Reader { private final SharedLevelDatabase shared; private final Snapshot snapshot; @@ -205,7 +333,7 @@ private LevelReader(SharedLevelDatabase shared) throws IOException { ReadOptions openedReads = null; try { openedSnapshot = shared.database.getSnapshot(); - openedReads = new ReadOptions().fillCache(false).snapshot(openedSnapshot); + openedReads = new ReadOptions().fillCache(true).snapshot(openedSnapshot); } catch (RuntimeException failure) { if (openedSnapshot != null) { openedSnapshot.close(); @@ -319,28 +447,28 @@ public void close() throws IOException { } private static final class RocksReader implements Reader { - static { - org.rocksdb.RocksDB.loadLibrary(); - } - - private final org.rocksdb.Options options = - new org.rocksdb.Options().setCreateIfMissing(false); - private final org.rocksdb.RocksDB database; + private final SharedRocksDatabase shared; + private final org.rocksdb.Snapshot snapshot; + private final org.rocksdb.ReadOptions reads; private boolean closed; - private RocksReader(Path directory) throws IOException { + private RocksReader(SharedRocksDatabase shared) { + this.shared = shared; + snapshot = shared.database.getSnapshot(); try { - database = org.rocksdb.RocksDB.openReadOnly(options, directory.toString()); - } catch (org.rocksdb.RocksDBException | RuntimeException failure) { - options.close(); - throw new IOException("Failed to open RocksDB Archive serving index", failure); + reads = new org.rocksdb.ReadOptions().setVerifyChecksums(true).setFillCache(true) + .setSnapshot(snapshot); + } catch (RuntimeException failure) { + shared.database.releaseSnapshot(snapshot); + releaseRocks(shared); + throw failure; } } @Override public byte[] get(byte[] key) throws IOException { try { - return database.get(key); + return shared.database.get(reads, key); } catch (org.rocksdb.RocksDBException failure) { throw new IOException("Failed to read RocksDB Archive serving index", failure); } @@ -348,8 +476,8 @@ public byte[] get(byte[] key) throws IOException { @Override public KeyValue seek(byte[] key) throws IOException { - try (org.rocksdb.ReadOptions reads = new org.rocksdb.ReadOptions(); - org.rocksdb.RocksIterator iterator = database.newIterator(reads)) { + try (org.rocksdb.ReadOptions seekReads = snapshotReads(snapshot); + org.rocksdb.RocksIterator iterator = shared.database.newIterator(seekReads)) { iterator.seek(key); if (!iterator.isValid()) { iterator.status(); @@ -363,13 +491,13 @@ public KeyValue seek(byte[] key) throws IOException { @Override public Cursor cursor() { - return new RocksCursor(database, new org.rocksdb.ReadOptions()); + return new RocksCursor(shared.database, snapshotReads(snapshot), true); } @Override public OptionalLong readLongProperty(String name) { try { - return OptionalLong.of(database.getLongProperty(name)); + return OptionalLong.of(shared.database.getLongProperty(name)); } catch (org.rocksdb.RocksDBException | IllegalArgumentException failure) { return OptionalLong.empty(); } @@ -379,36 +507,30 @@ public OptionalLong readLongProperty(String name) { public void close() { if (!closed) { closed = true; - database.close(); - options.close(); + reads.close(); + shared.database.releaseSnapshot(snapshot); + releaseRocks(shared); } } - } - private static final class RocksWriter implements Writer { - static { - org.rocksdb.RocksDB.loadLibrary(); + private static org.rocksdb.ReadOptions snapshotReads(org.rocksdb.Snapshot snapshot) { + return new org.rocksdb.ReadOptions().setVerifyChecksums(true).setFillCache(true) + .setSnapshot(snapshot); } + } - private final org.rocksdb.Options options = - new org.rocksdb.Options().setCreateIfMissing(true) - .setCompressionType(org.rocksdb.CompressionType.NO_COMPRESSION); - private final org.rocksdb.RocksDB database; + private static final class RocksWriter implements Writer { + private final SharedRocksDatabase shared; private boolean closed; - private RocksWriter(Path directory) throws IOException { - try { - database = org.rocksdb.RocksDB.open(options, directory.toString()); - } catch (org.rocksdb.RocksDBException | RuntimeException failure) { - options.close(); - throw new IOException("Failed to open RocksDB Archive serving index", failure); - } + private RocksWriter(SharedRocksDatabase shared) { + this.shared = shared; } @Override public byte[] get(byte[] key) throws IOException { try { - return database.get(key); + return shared.database.get(key); } catch (org.rocksdb.RocksDBException failure) { throw new IOException("Failed to read RocksDB Archive serving index", failure); } @@ -426,7 +548,7 @@ public void write(List mutations, boolean sync) throws IOException { batch.put(mutation.key, mutation.value); } try (org.rocksdb.WriteOptions selected = new org.rocksdb.WriteOptions().setSync(sync)) { - database.write(selected, batch); + shared.database.write(selected, batch); } } catch (org.rocksdb.RocksDBException failure) { throw new IOException("Failed to write RocksDB Archive serving index", failure); @@ -437,8 +559,7 @@ public void write(List mutations, boolean sync) throws IOException { public void close() { if (!closed) { closed = true; - database.close(); - options.close(); + releaseRocks(shared); } } } @@ -446,10 +567,13 @@ public void close() { private static final class RocksCursor implements Cursor { private final org.rocksdb.ReadOptions reads; private final org.rocksdb.RocksIterator iterator; + private final boolean ownsReadOptions; - private RocksCursor(org.rocksdb.RocksDB database, org.rocksdb.ReadOptions reads) { + private RocksCursor(org.rocksdb.RocksDB database, org.rocksdb.ReadOptions reads, + boolean ownsReadOptions) { this.reads = reads; this.iterator = database.newIterator(reads); + this.ownsReadOptions = ownsReadOptions; } @Override @@ -475,22 +599,8 @@ public KeyValue next() throws IOException { @Override public void close() { iterator.close(); - reads.close(); - } - } - - private static final class RocksCheckpoint { - static { - org.rocksdb.RocksDB.loadLibrary(); - } - - private static void create(Path source, Path target) throws IOException { - try (org.rocksdb.Options options = new org.rocksdb.Options().setCreateIfMissing(false); - org.rocksdb.RocksDB database = org.rocksdb.RocksDB.open(options, source.toString()); - org.rocksdb.Checkpoint checkpoint = org.rocksdb.Checkpoint.create(database)) { - checkpoint.createCheckpoint(target.toString()); - } catch (org.rocksdb.RocksDBException failure) { - throw new IOException("Failed to checkpoint RocksDB Archive serving index", failure); + if (ownsReadOptions) { + reads.close(); } } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java index 022a2fb0109..238249b4dc8 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateNativeNodeStore.java @@ -15,19 +15,29 @@ import java.util.Objects; import org.iq80.leveldb.DB; import org.iq80.leveldb.WriteOptions; +import org.rocksdb.BlockBasedTableConfig; +import org.rocksdb.BloomFilter; +import org.rocksdb.ChecksumType; +import org.rocksdb.CompressionType; +import org.rocksdb.LRUCache; import org.rocksdb.RocksDBException; -import org.tron.common.utils.DbOptionalsUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tron.core.config.args.StorageConfig.NativeDbConfig; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** Package-owned LevelDB/RocksDB key/value engine shared by namespaced path-node views. */ final class PathStateNativeNodeStore implements Closeable { + private static final Logger logger = LoggerFactory.getLogger("DB"); + static { org.rocksdb.RocksDB.loadLibrary(); } private final Path directory; private final Engine engine; + private final String storageProfile; private final Delegate delegate; private long writeBatchCalls; private long writeBatchMutations; @@ -35,16 +45,26 @@ final class PathStateNativeNodeStore implements Closeable { private long unsyncedWriteBatchCalls; private volatile boolean closed; - private PathStateNativeNodeStore(Path directory, Engine engine, Delegate delegate) { + private PathStateNativeNodeStore(Path directory, Engine engine, String storageProfile, + Delegate delegate) { this.directory = directory; this.engine = engine; + this.storageProfile = storageProfile; this.delegate = delegate; } /** Opens one independent node database; callers choose the WAL sync boundary per batch. */ static PathStateNativeNodeStore open(Path directory, Engine engine) throws IOException { + return open(directory, engine, "small", NativeDbConfig.small()); + } + + /** Opens one independent database with an explicit validated resource profile. */ + static PathStateNativeNodeStore open(Path directory, Engine engine, String storageProfile, + NativeDbConfig config) throws IOException { Path path = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); Engine selected = Objects.requireNonNull(engine, "engine"); + String profile = Objects.requireNonNull(storageProfile, "storageProfile"); + NativeDbConfig settings = Objects.requireNonNull(config, "config"); if (Files.isSymbolicLink(path)) { throw new IOException("path-state node database must not be a symbolic link: " + path); } @@ -52,9 +72,13 @@ static PathStateNativeNodeStore open(Path directory, Engine engine) throws IOExc if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { throw new IOException("path-state node database is not a directory: " + path); } - Delegate opened = selected == Engine.LEVELDB ? new LevelDelegate(path) - : new RocksDelegate(path); - return new PathStateNativeNodeStore(path, selected, opened); + Delegate opened = selected == Engine.LEVELDB ? new LevelDelegate(path, settings) + : new RocksDelegate(path, settings); + logger.info("Path-state database opened: directory={}, engine={}, profile={}, blockBytes={}, " + + "writeBufferBytes={}, cacheBytes={}, maxOpenFiles={}", path, selected, profile, + settings.getBlockSize(), settings.getWriteBufferSize(), settings.getCacheSize(), + settings.getMaxOpenFiles()); + return new PathStateNativeNodeStore(path, selected, profile, opened); } byte[] get(byte[] key) { @@ -146,6 +170,10 @@ Engine getEngine() { return engine; } + String getStorageProfile() { + return storageProfile; + } + @Override public synchronized void close() throws IOException { if (!closed) { @@ -181,12 +209,21 @@ private interface Delegate extends Closeable { private static final class LevelDelegate implements Delegate { - private final org.iq80.leveldb.Options options = DbOptionalsUtils.createDefaultDbOptions(); + private final org.iq80.leveldb.Options options; private final WriteOptions syncWrites = new WriteOptions().sync(true); private final WriteOptions unsyncedWrites = new WriteOptions().sync(false); private final DB database; - private LevelDelegate(Path directory) throws IOException { + private LevelDelegate(Path directory, NativeDbConfig config) throws IOException { + options = new org.iq80.leveldb.Options() + .createIfMissing(true) + .paranoidChecks(true) + .verifyChecksums(true) + .compressionType(org.iq80.leveldb.CompressionType.SNAPPY) + .blockSize(config.getBlockSize()) + .writeBufferSize(config.getWriteBufferSize()) + .cacheSize(config.getCacheSize()) + .maxOpenFiles(config.getMaxOpenFiles()); database = factory.open(directory.toFile(), options); } @@ -244,21 +281,53 @@ public void close() throws IOException { private static final class RocksDelegate implements Delegate { - private final org.rocksdb.Options options = - new org.rocksdb.Options().setCreateIfMissing(true).setParanoidChecks(true); + private final LRUCache blockCache; + private final BloomFilter bloomFilter; + private final org.rocksdb.Options options; private final org.rocksdb.WriteOptions syncWrites = new org.rocksdb.WriteOptions().setSync(true); private final org.rocksdb.WriteOptions unsyncedWrites = new org.rocksdb.WriteOptions().setSync(false); private final org.rocksdb.RocksDB database; - private RocksDelegate(Path directory) throws IOException { + private RocksDelegate(Path directory, NativeDbConfig config) throws IOException { + blockCache = new LRUCache(config.getCacheSize()); + bloomFilter = new BloomFilter(config.getBloomBitsPerKey(), false); + BlockBasedTableConfig table = new BlockBasedTableConfig() + .setBlockSize(config.getBlockSize()) + .setChecksumType(ChecksumType.kCRC32c) + .setBlockCache(blockCache) + .setCacheIndexAndFilterBlocks(true) + .setPinL0FilterAndIndexBlocksInCache(false) + .setWholeKeyFiltering(true) + .setFilter(bloomFilter); + options = new org.rocksdb.Options() + .setCreateIfMissing(true) + .setParanoidChecks(true) + .setCompressionType(CompressionType.SNAPPY_COMPRESSION) + .setWriteBufferSize(config.getWriteBufferSize()) + .setMaxWriteBufferNumber(config.getMaxWriteBufferNumber()) + .setMinWriteBufferNumberToMerge(1) + .setMaxOpenFiles(config.getMaxOpenFiles()) + .setNumLevels(config.getLevelNumber()) + .setLevelCompactionDynamicLevelBytes(true) + .setLevel0FileNumCompactionTrigger(config.getLevel0FileNumCompactionTrigger()) + .setLevel0SlowdownWritesTrigger(config.getLevel0SlowdownWritesTrigger()) + .setLevel0StopWritesTrigger(config.getLevel0StopWritesTrigger()) + .setMaxBackgroundCompactions(config.getBackgroundCompactions()) + .setMaxBackgroundFlushes(config.getBackgroundFlushes()) + .setTargetFileSizeBase(config.getTargetFileSizeBase()) + .setMaxBytesForLevelBase(config.getMaxBytesForLevelBase()) + .setMaxBytesForLevelMultiplier(config.getMaxBytesForLevelMultiplier()) + .setTableFormatConfig(table); try { database = org.rocksdb.RocksDB.open(options, directory.toString()); } catch (RocksDBException failure) { unsyncedWrites.close(); syncWrites.close(); options.close(); + bloomFilter.close(); + blockCache.close(); throw new IOException("failed to open path-state RocksDB node database", failure); } } @@ -318,10 +387,12 @@ public void scanAll(EntryConsumer consumer) throws IOException { @Override public void close() { + database.close(); unsyncedWrites.close(); syncWrites.close(); - database.close(); options.close(); + bloomFilter.close(); + blockCache.close(); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java index 9bc6cff307b..8e6ce9242e6 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java @@ -26,6 +26,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tron.common.crypto.Hash; +import org.tron.common.parameter.CommonParameter; +import org.tron.core.config.args.StorageConfig.NativeDbConfig; +import org.tron.core.config.args.StorageConfig.PathStateDbSettingsConfig; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.core.CommonCheckpointPayload; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; @@ -52,6 +55,10 @@ public final class PathStatePhysicalStoreSet implements Closeable { private static final Set LARGE_BOOTSTRAP_STORES = java.util.Collections.unmodifiableSet( new HashSet<>(Arrays.asList( "account", "account-asset", "delegation", "storage-row"))); + private static final Set GIANT_NATIVE_STORES = java.util.Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("account", "account-asset", "storage-row"))); + private static final Set LARGE_NATIVE_STORES = java.util.Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("code", "contract", "delegation"))); private static final String STORES_DIRECTORY = "stores"; private static final String SUPER_DIRECTORY = "super"; @@ -95,7 +102,8 @@ public final class PathStatePhysicalStoreSet implements Closeable { private boolean closed; private PathStatePhysicalStoreSet(PathStatePhysicalStoreManifest manifest, - PathStateParticipantScope scope, long residentNodeCacheBytes) + PathStateParticipantScope scope, long residentNodeCacheBytes, + PathStateDbSettingsConfig dbSettings) throws IOException { this.manifest = manifest; this.directory = manifest.getDirectory(); @@ -110,10 +118,12 @@ private PathStatePhysicalStoreSet(PathStatePhysicalStoreManifest manifest, Path participantDirectory = directory.resolve(STORES_DIRECTORY).resolve(String.format( "%02d-%s", participant.getStoreId(), participant.getDbName())).resolve(NODES_DIRECTORY); participants.put(participant.getDbName(), new PhysicalStore(participantDirectory, - manifest.getEngine(), participant.getStoreId(), residentNodeCache)); + manifest.getEngine(), participant.getStoreId(), residentNodeCache, + storageProfileNameFor(participant.getDbName()), + storageProfileFor(participant.getDbName(), dbSettings))); } superStore = new PhysicalStore(directory.resolve(SUPER_DIRECTORY).resolve(NODES_DIRECTORY), - manifest.getEngine(), 0, residentNodeCache); + manifest.getEngine(), 0, residentNodeCache, "small", dbSettings.getSmall()); } catch (IOException | RuntimeException failure) { closeAfterFailure(failure); throw failure; @@ -131,7 +141,7 @@ public static PathStatePhysicalStoreSet open(Path directory, PathStateParticipan PathStatePhysicalStoreManifest manifest = PathStatePhysicalStoreManifest.createOrOpen(root, Objects.requireNonNull(engine, "engine")); return new PathStatePhysicalStoreSet(manifest, Objects.requireNonNull(scope, "scope"), - STEADY_NODE_CACHE_BYTES); + STEADY_NODE_CACHE_BYTES, configuredDbSettings()); } /** Opens only a fully materialized physical layout; missing child databases fail closed. */ @@ -155,7 +165,38 @@ public static PathStatePhysicalStoreSet openExisting(Path directory, .resolve(NODES_DIRECTORY)); } requireStoreDirectory(root.resolve(SUPER_DIRECTORY).resolve(NODES_DIRECTORY)); - return new PathStatePhysicalStoreSet(manifest, admittedScope, residentNodeCacheBytes); + return new PathStatePhysicalStoreSet(manifest, admittedScope, residentNodeCacheBytes, + configuredDbSettings()); + } + + static String storageProfileNameFor(String dbName) { + String supplied = Objects.requireNonNull(dbName, "dbName"); + if (GIANT_NATIVE_STORES.contains(supplied)) { + return "giant"; + } + if (LARGE_NATIVE_STORES.contains(supplied)) { + return "large"; + } + return "small"; + } + + private static NativeDbConfig storageProfileFor(String dbName, + PathStateDbSettingsConfig settings) { + String profile = storageProfileNameFor(dbName); + if ("giant".equals(profile)) { + return settings.getGiant(); + } + if ("large".equals(profile)) { + return settings.getLarge(); + } + return settings.getSmall(); + } + + private static PathStateDbSettingsConfig configuredDbSettings() { + org.tron.core.config.args.Storage storage = CommonParameter.getInstance().getStorage(); + PathStateDbSettingsConfig settings = storage == null ? null + : storage.getPathStateRootDbSettings(); + return settings == null ? new PathStateDbSettingsConfig() : settings; } public synchronized PhysicalStore participant(String dbName) { @@ -1641,8 +1682,9 @@ public static final class PhysicalStore implements Closeable { private final ResidentNodeStore nodeStore; private PhysicalStore(Path directory, Engine engine, int storeId, - ResidentNodeCache residentNodeCache) throws IOException { - nativeStore = PathStateNativeNodeStore.open(directory, engine); + ResidentNodeCache residentNodeCache, String storageProfile, NativeDbConfig dbSettings) + throws IOException { + nativeStore = PathStateNativeNodeStore.open(directory, engine, storageProfile, dbSettings); nodeStore = new ResidentNodeStore(new PhysicalNodeStore(nativeStore), residentNodeCache, storeId); } diff --git a/common/src/main/java/org/tron/core/config/args/Storage.java b/common/src/main/java/org/tron/core/config/args/Storage.java index 803f571e51c..04a595c2686 100644 --- a/common/src/main/java/org/tron/core/config/args/Storage.java +++ b/common/src/main/java/org/tron/core/config/args/Storage.java @@ -101,6 +101,10 @@ public class Storage { @Setter private int stateArchiveQueueCapacity; + @Getter + @Setter + private StorageConfig.NativeDbConfig stateArchiveServingIndexDbSettings; + @Getter @Setter private boolean commonCheckpointEnabled; @@ -167,6 +171,10 @@ public class Storage { @Setter private boolean pathStateRootAsyncPrepareBenchmark; + @Getter + @Setter + private StorageConfig.PathStateDbSettingsConfig pathStateRootDbSettings; + private Options defaultDbOptions; @Getter diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index 45a5669fca3..7e4eaeec4c8 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -152,6 +152,7 @@ public static class StateArchiveConfig { private String directory = "state-archive"; private long maxSegmentSize = 1073741824L; private int queueCapacity = 256; + private NativeDbConfig servingIndex = NativeDbConfig.large(); void postProcess() { if (directory == null || directory.trim().isEmpty()) { @@ -165,6 +166,7 @@ void postProcess() { throw new IllegalArgumentException( "stateArchive.queueCapacity must be in [1, 65536]"); } + servingIndex.validate("storage.stateArchive.servingIndex"); } } @@ -200,6 +202,7 @@ public static class PathStateRootConfig { private boolean verifyEveryBlock = true; private boolean volatileSnapshotBenchmark = false; private boolean asyncPrepareBenchmark = false; + private PathStateDbSettingsConfig dbSettings = new PathStateDbSettingsConfig(); void postProcess() { if (!"shadow".equals(mode)) { @@ -230,6 +233,77 @@ void postProcess() { throw new IllegalArgumentException( "pathStateRoot.asyncPrepareBenchmark requires volatileSnapshotBenchmark"); } + dbSettings.validate(); + } + } + + /** Engine-neutral native options for one Archive/PathState resource tier. */ + @Getter + @Setter + public static class NativeDbConfig { + + private int blockSize = 4 * 1024; + private int writeBufferSize = 16 * 1024 * 1024; + private long cacheSize = 32L * 1024 * 1024; + private int maxOpenFiles = 100; + private long targetFileSizeBase = 16L * 1024 * 1024; + private long maxBytesForLevelBase = 64L * 1024 * 1024; + private int bloomBitsPerKey = 10; + private int maxWriteBufferNumber = 2; + private int levelNumber = 7; + private double maxBytesForLevelMultiplier = 10.0d; + private int level0FileNumCompactionTrigger = 4; + private int level0SlowdownWritesTrigger = 20; + private int level0StopWritesTrigger = 36; + private int backgroundFlushes = 1; + private int backgroundCompactions = 1; + + public static NativeDbConfig small() { + return new NativeDbConfig(); + } + + public static NativeDbConfig large() { + NativeDbConfig config = new NativeDbConfig(); + config.writeBufferSize = 64 * 1024 * 1024; + config.targetFileSizeBase = 64L * 1024 * 1024; + config.maxBytesForLevelBase = 256L * 1024 * 1024; + return config; + } + + public static NativeDbConfig giant() { + NativeDbConfig config = large(); + config.cacheSize = 64L * 1024 * 1024; + config.targetFileSizeBase = 128L * 1024 * 1024; + config.maxBytesForLevelBase = 512L * 1024 * 1024; + return config; + } + + void validate(String path) { + if (blockSize <= 0 || writeBufferSize <= 0 || cacheSize <= 0 || maxOpenFiles <= 0 + || targetFileSizeBase <= 0 || maxBytesForLevelBase <= 0 + || bloomBitsPerKey <= 0 || maxWriteBufferNumber <= 0 || levelNumber <= 0 + || maxBytesForLevelMultiplier <= 0 || level0FileNumCompactionTrigger <= 0 + || level0SlowdownWritesTrigger < level0FileNumCompactionTrigger + || level0StopWritesTrigger < level0SlowdownWritesTrigger + || backgroundFlushes <= 0 || backgroundCompactions <= 0) { + throw new IllegalArgumentException(path + " native database options are invalid"); + } + } + } + + /** Fixed small/large/giant PathState profile catalog. */ + @Getter + @Setter + public static class PathStateDbSettingsConfig { + + private NativeDbConfig small = NativeDbConfig.small(); + private NativeDbConfig large = NativeDbConfig.large(); + private NativeDbConfig giant = NativeDbConfig.giant(); + + void validate() { + small.validate("storage.pathStateRoot.dbSettings.small"); + large.validate("storage.pathStateRoot.dbSettings.large"); + giant.validate("storage.pathStateRoot.dbSettings.giant"); } } diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 6e5d17a1319..317376a00bc 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -137,6 +137,23 @@ storage { stateArchive.directory = "state-archive" stateArchive.maxSegmentSize = 1073741824 # 1 GiB stateArchive.queueCapacity = 256 + stateArchive.servingIndex { + blockSize = 4096 + writeBufferSize = 67108864 + cacheSize = 33554432 + maxOpenFiles = 100 + targetFileSizeBase = 67108864 + maxBytesForLevelBase = 268435456 + bloomBitsPerKey = 10 + maxWriteBufferNumber = 2 + levelNumber = 7 + maxBytesForLevelMultiplier = 10 + level0FileNumCompactionTrigger = 4 + level0SlowdownWritesTrigger = 20 + level0StopWritesTrigger = 36 + backgroundFlushes = 1 + backgroundCompactions = 1 + } # Next-format three-authority checkpoint. Requires fresh Archive and PathState directories. commonCheckpoint.enabled = false commonCheckpoint.directory = "common-checkpoint" @@ -157,6 +174,59 @@ storage { # Benchmark-only. Advances PathState in memory and writes no per-block journal/F/N/CURRENT. pathStateRoot.volatileSnapshotBenchmark = false pathStateRoot.asyncPrepareBenchmark = false + pathStateRoot.dbSettings { + small { + blockSize = 4096 + writeBufferSize = 16777216 + cacheSize = 33554432 + maxOpenFiles = 100 + targetFileSizeBase = 16777216 + maxBytesForLevelBase = 67108864 + bloomBitsPerKey = 10 + maxWriteBufferNumber = 2 + levelNumber = 7 + maxBytesForLevelMultiplier = 10 + level0FileNumCompactionTrigger = 4 + level0SlowdownWritesTrigger = 20 + level0StopWritesTrigger = 36 + backgroundFlushes = 1 + backgroundCompactions = 1 + } + large { + blockSize = 4096 + writeBufferSize = 67108864 + cacheSize = 33554432 + maxOpenFiles = 100 + targetFileSizeBase = 67108864 + maxBytesForLevelBase = 268435456 + bloomBitsPerKey = 10 + maxWriteBufferNumber = 2 + levelNumber = 7 + maxBytesForLevelMultiplier = 10 + level0FileNumCompactionTrigger = 4 + level0SlowdownWritesTrigger = 20 + level0StopWritesTrigger = 36 + backgroundFlushes = 1 + backgroundCompactions = 1 + } + giant { + blockSize = 4096 + writeBufferSize = 67108864 + cacheSize = 67108864 + maxOpenFiles = 100 + targetFileSizeBase = 134217728 + maxBytesForLevelBase = 536870912 + bloomBitsPerKey = 10 + maxWriteBufferNumber = 2 + levelNumber = 7 + maxBytesForLevelMultiplier = 10 + level0FileNumCompactionTrigger = 4 + level0SlowdownWritesTrigger = 20 + level0StopWritesTrigger = 36 + backgroundFlushes = 1 + backgroundCompactions = 1 + } + } # Data root setting, for check data, currently only reward-vi is used. # merkleRoot = { diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java index 19c3fec7716..9c71056626e 100644 --- a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -73,6 +73,38 @@ public void testStateArchiveDefaultsAndOverrides() { assertEquals(8, configured.getStateArchive().getQueueCapacity()); } + @Test + public void testArchiveNativeDatabaseProfileDefaultsAndOverrides() { + StorageConfig defaults = StorageConfig.fromConfig(withRef()); + assertEquals(67108864, + defaults.getStateArchive().getServingIndex().getWriteBufferSize()); + assertEquals(33554432L, + defaults.getStateArchive().getServingIndex().getCacheSize()); + assertEquals(16777216, + defaults.getPathStateRoot().getDbSettings().getSmall().getWriteBufferSize()); + assertEquals(67108864, + defaults.getPathStateRoot().getDbSettings().getGiant().getWriteBufferSize()); + assertEquals(67108864L, + defaults.getPathStateRoot().getDbSettings().getGiant().getCacheSize()); + + StorageConfig configured = StorageConfig.fromConfig(withRef( + "storage.pathStateRoot.dbSettings.small.cacheSize = 1048576\n" + + "storage.pathStateRoot.dbSettings.giant.maxOpenFiles = 321\n" + + "storage.stateArchive.servingIndex.writeBufferSize = 8388608")); + assertEquals(1048576L, + configured.getPathStateRoot().getDbSettings().getSmall().getCacheSize()); + assertEquals(321, + configured.getPathStateRoot().getDbSettings().getGiant().getMaxOpenFiles()); + assertEquals(8388608, + configured.getStateArchive().getServingIndex().getWriteBufferSize()); + } + + @Test(expected = IllegalArgumentException.class) + public void testArchiveNativeDatabaseProfileRejectsInvalidValues() { + StorageConfig.fromConfig(withRef( + "storage.pathStateRoot.dbSettings.large.maxOpenFiles = 0")); + } + @Test(expected = IllegalArgumentException.class) public void testStateArchiveRejectsSmallSegments() { StorageConfig.fromConfig(withRef("storage.stateArchive.maxSegmentSize = 1024")); diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index b3dd250d6c5..c8487bc362e 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -221,6 +221,8 @@ private static void applyStorageConfig(StorageConfig sc) { PARAMETER.storage.setStateArchiveDirectory(sc.getStateArchive().getDirectory()); PARAMETER.storage.setStateArchiveMaxSegmentSize(sc.getStateArchive().getMaxSegmentSize()); PARAMETER.storage.setStateArchiveQueueCapacity(sc.getStateArchive().getQueueCapacity()); + PARAMETER.storage.setStateArchiveServingIndexDbSettings( + sc.getStateArchive().getServingIndex()); PARAMETER.storage.setCommonCheckpointEnabled(sc.getCommonCheckpoint().isEnabled()); PARAMETER.storage.setCommonCheckpointDirectory(sc.getCommonCheckpoint().getDirectory()); PARAMETER.storage.setPathStateRootEnabled(sc.getPathStateRoot().isEnabled()); @@ -247,6 +249,7 @@ private static void applyStorageConfig(StorageConfig sc) { sc.getPathStateRoot().isVolatileSnapshotBenchmark()); PARAMETER.storage.setPathStateRootAsyncPrepareBenchmark( sc.getPathStateRoot().isAsyncPrepareBenchmark()); + PARAMETER.storage.setPathStateRootDbSettings(sc.getPathStateRoot().getDbSettings()); // estimatedTransactions / maxFlushCount clamping & validation run inside // TxCacheConfig.postProcess / SnapshotConfig.postProcess during bean load. diff --git a/framework/src/test/java/org/tron/core/config/args/StorageTest.java b/framework/src/test/java/org/tron/core/config/args/StorageTest.java index c6b954838ca..8adfc00f749 100644 --- a/framework/src/test/java/org/tron/core/config/args/StorageTest.java +++ b/framework/src/test/java/org/tron/core/config/args/StorageTest.java @@ -71,6 +71,18 @@ public void getDirectory() { Assert.assertEquals("database", storage.getDbDirectory()); } + @Test + public void archiveDatabaseProfilesAreBridgedFromConfiguration() { + Assert.assertNotNull(storage.getPathStateRootDbSettings()); + Assert.assertEquals(16 * 1024 * 1024, + storage.getPathStateRootDbSettings().getSmall().getWriteBufferSize()); + Assert.assertEquals(64 * 1024 * 1024L, + storage.getPathStateRootDbSettings().getGiant().getCacheSize()); + Assert.assertNotNull(storage.getStateArchiveServingIndexDbSettings()); + Assert.assertEquals(64 * 1024 * 1024, + storage.getStateArchiveServingIndexDbSettings().getWriteBufferSize()); + } + @Test public void getPath() { Assert.assertEquals("storage_directory_test", StorageUtils.getPathByDbName("account")); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java index 868e0b49c26..a368be76235 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java @@ -4,13 +4,16 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import com.google.common.hash.Hashing; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.stream.Stream; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -47,6 +50,25 @@ public void supportsConfiguredEngineAndRejectsEngineDrift() throws Exception { } } + @Test + public void rocksReaderAndWriterShareConfiguredDatabaseHandle() throws Exception { + Path database = temporaryFolder.newFolder("rocks-shared-handle").toPath().resolve("keys"); + try (StateArchiveIndexDatabase.Writer writer = + StateArchiveIndexDatabase.openWriter(database, Engine.ROCKSDB)) { + writer.write(Arrays.asList(StateArchiveIndexDatabase.put(new byte[]{1}, new byte[]{2}))); + try (StateArchiveIndexDatabase.Reader reader = + StateArchiveIndexDatabase.openReader(database, Engine.ROCKSDB)) { + assertArrayEquals(new byte[]{2}, reader.get(new byte[]{1})); + } + } + + String nativeOptions = new String(Files.readAllBytes(latestOptionsFile(database)), + StandardCharsets.US_ASCII); + assertTrue(nativeOptions.contains("write_buffer_size=67108864")); + assertTrue(nativeOptions.contains("block_size=4096")); + assertTrue(nativeOptions.contains("filter_policy=rocksdb.BuiltinBloomFilter")); + } + @Test public void rejectsExistingDatabaseWithoutEngineIdentity() throws Exception { Path root = temporaryFolder.newFolder("missing-manifest").toPath(); @@ -71,4 +93,12 @@ public void acceptsPriorShaIdentityAndRejectsCrcCorruption() throws Exception { Files.write(current.resolve(StateArchiveIndexEngineManifest.FILE), corrupt); assertThrows(IOException.class, () -> StateArchiveIndexEngineManifest.load(current)); } + + private static Path latestOptionsFile(Path directory) throws IOException { + try (Stream files = Files.list(directory)) { + return files.filter(path -> path.getFileName().toString().startsWith("OPTIONS-")) + .max(java.util.Comparator.comparing(path -> path.getFileName().toString())) + .orElseThrow(() -> new IOException("RocksDB OPTIONS file is missing")); + } + } } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index fc5788642ab..fe7cee96f56 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -9,6 +9,7 @@ import static org.junit.Assert.assertTrue; import java.io.File; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -27,6 +28,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.tron.common.arch.Arch; +import org.tron.core.config.args.StorageConfig.NativeDbConfig; import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.core.CommonCheckpointBaseline; @@ -78,6 +80,36 @@ public void nativeStoreRejectsInvalidEntriesAndUseAfterClose() throws Exception assertThrows(IllegalStateException.class, () -> store.get(new byte[0])); } + @Test + public void physicalStoresUseFixedSmallLargeAndGiantProfiles() { + assertEquals("giant", PathStatePhysicalStoreSet.storageProfileNameFor("account")); + assertEquals("giant", PathStatePhysicalStoreSet.storageProfileNameFor("account-asset")); + assertEquals("giant", PathStatePhysicalStoreSet.storageProfileNameFor("storage-row")); + assertEquals("large", PathStatePhysicalStoreSet.storageProfileNameFor("code")); + assertEquals("large", PathStatePhysicalStoreSet.storageProfileNameFor("contract")); + assertEquals("large", PathStatePhysicalStoreSet.storageProfileNameFor("delegation")); + assertEquals("small", PathStatePhysicalStoreSet.storageProfileNameFor("proposal")); + } + + @Test + public void rocksProfileIsPersistedInNativeOptions() throws Exception { + Path directory = temporaryFolder.newFolder("rocks-profile-options").toPath(); + try (PathStateNativeNodeStore store = PathStateNativeNodeStore.open(directory, + Engine.ROCKSDB, "giant", NativeDbConfig.giant())) { + assertEquals("giant", store.getStorageProfile()); + store.put(new byte[]{1}, new byte[]{2}); + } + + String nativeOptions = new String(Files.readAllBytes(latestOptionsFile(directory)), + StandardCharsets.US_ASCII); + assertTrue(nativeOptions.contains("write_buffer_size=67108864")); + assertTrue(nativeOptions.contains("max_write_buffer_number=2")); + assertTrue(nativeOptions.contains("compression=kSnappyCompression")); + assertTrue(nativeOptions.contains("block_size=4096")); + assertTrue(nativeOptions.contains("filter_policy=rocksdb.BuiltinBloomFilter")); + assertTrue(nativeOptions.contains("checksum=kCRC32c")); + } + @Test public void transitionRecordingStoreCachesBaseReadsAndOwnsReturnedBytes() { AtomicInteger reads = new AtomicInteger(); @@ -1845,6 +1877,14 @@ private static int compareUnsigned(byte[] left, byte[] right) { return Integer.compare(left.length, right.length); } + private static Path latestOptionsFile(Path directory) throws java.io.IOException { + try (Stream files = Files.list(directory)) { + return files.filter(path -> path.getFileName().toString().startsWith("OPTIONS-")) + .max(java.util.Comparator.comparing(path -> path.getFileName().toString())) + .orElseThrow(() -> new java.io.IOException("RocksDB OPTIONS file is missing")); + } + } + private static void awaitTestLatch(CountDownLatch started, CountDownLatch release) { started.countDown(); try { From a2fc535a5a48d17c5ba4cede70c899e5e03e7779 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 5 Sep 2026 17:38:19 +0800 Subject: [PATCH 117/161] fix(db): preserve path state format compatibility --- .../stateroot/PathStateCommitmentCodec.java | 5 +- .../tron/core/config/args/StorageConfig.java | 6 +- common/src/main/resources/reference.conf | 4 +- .../core/config/args/StorageConfigTest.java | 4 +- .../main/java/org/tron/core/db/Manager.java | 9 ++- framework/src/main/resources/config.conf | 2 +- .../PathStateCommitmentCodecTest.java | 18 ++--- ...athStateManagerStartupIntegrationTest.java | 65 ++++++++++++++++++- .../PathStatePersistentFormatTest.java | 2 +- .../core/db2/stateroot/PathStateRootTest.java | 5 +- 10 files changed, 95 insertions(+), 25 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java index c17ae632a9c..d839e0af41d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCommitmentCodec.java @@ -15,7 +15,7 @@ */ public final class PathStateCommitmentCodec { - public static final int FORMAT_VERSION = 2; + public static final int FORMAT_VERSION = 1; public static final int ROOT_LENGTH = 32; private static final byte PRESENT_TAG = 1; @@ -37,8 +37,9 @@ public static byte[] storeLeafKey(int stableStoreId, byte[] physicalRawKey) { requireStoreId(stableStoreId); byte[] key = copy(physicalRawKey, "physicalRawKey"); ByteBuffer material = ByteBuffer.allocate(Short.BYTES + STORE_LEAF_KEY_DOMAIN.length - + Integer.BYTES + Integer.BYTES + key.length); + + Short.BYTES + Integer.BYTES + Integer.BYTES + key.length); putDomain(material, STORE_LEAF_KEY_DOMAIN); + material.putShort((short) FORMAT_VERSION); material.putInt(stableStoreId); material.putInt(key.length); material.put(key); diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index 7e4eaeec4c8..76078315d95 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -191,7 +191,7 @@ public static class PathStateRootConfig { private boolean enabled = false; private String mode = "shadow"; private String directory = "path-state-root"; - private int formatVersion = 2; + private int formatVersion = 1; private int reversibleLayerLimit = 128; private long reversibleLayerBytes = 2147483648L; private long writeBufferBytes = 268435456L; @@ -211,8 +211,8 @@ void postProcess() { if (directory == null || directory.trim().isEmpty()) { throw new IllegalArgumentException("pathStateRoot.directory must not be empty"); } - if (formatVersion != 2) { - throw new IllegalArgumentException("pathStateRoot.formatVersion must be 2"); + if (formatVersion != 1) { + throw new IllegalArgumentException("pathStateRoot.formatVersion must be 1"); } if (reversibleLayerLimit <= 0 || reversibleLayerBytes <= 0 || writeBufferBytes <= 0 || nodeCacheBytes <= 0) { diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 317376a00bc..2ac8bfc9b64 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -154,7 +154,7 @@ storage { backgroundFlushes = 1 backgroundCompactions = 1 } - # Next-format three-authority checkpoint. Requires fresh Archive and PathState directories. + # Three-authority checkpoint. Admits a verified format-v1 PathState baseline. commonCheckpoint.enabled = false commonCheckpoint.directory = "common-checkpoint" @@ -162,7 +162,7 @@ storage { pathStateRoot.enabled = false pathStateRoot.mode = "shadow" pathStateRoot.directory = "path-state-root" - pathStateRoot.formatVersion = 2 + pathStateRoot.formatVersion = 1 pathStateRoot.reversibleLayerLimit = 128 pathStateRoot.reversibleLayerBytes = 2147483648 # 2 GiB pathStateRoot.writeBufferBytes = 268435456 # 256 MiB diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java index 9c71056626e..7e90e6370fc 100644 --- a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -144,7 +144,7 @@ public void testPathStateRootDefaultsAndOverrides() { assertFalse(defaults.getPathStateRoot().isEnabled()); assertEquals("shadow", defaults.getPathStateRoot().getMode()); assertEquals("path-state-root", defaults.getPathStateRoot().getDirectory()); - assertEquals(2, defaults.getPathStateRoot().getFormatVersion()); + assertEquals(1, defaults.getPathStateRoot().getFormatVersion()); assertEquals(128, defaults.getPathStateRoot().getReversibleLayerLimit()); assertEquals(2147483648L, defaults.getPathStateRoot().getReversibleLayerBytes()); assertEquals(268435456L, defaults.getPathStateRoot().getWriteBufferBytes()); @@ -158,7 +158,7 @@ public void testPathStateRootDefaultsAndOverrides() { StorageConfig configured = StorageConfig.fromConfig(withRef( "storage.pathStateRoot { enabled = true, mode = shadow, directory = root-test, " - + "formatVersion = 2, reversibleLayerLimit = 8, reversibleLayerBytes = 4096, " + + "formatVersion = 1, reversibleLayerLimit = 8, reversibleLayerBytes = 4096, " + "writeBufferBytes = 1024, nodeCacheBytes = 2048, participantThreads = 2, " + "branchThreads = 3, rebuildFromGenesis = false, " + "verifyEveryBlock = true, volatileSnapshotBenchmark = true, " diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index e72f46542d4..3b0b07d7acc 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -792,8 +792,13 @@ private void initCommonCheckpoint() { requireEmptyOrMissing(checkpointDirectory, "common checkpoint"); baselineFile.beginBootstrap(formatIdentity); } else if (!baselineFile.hasBootstrapIntent(formatIdentity)) { - throw new IllegalStateException( - "Common checkpoint refuses an existing legacy PathState directory"); + requireEmptyOrMissing(checkpointDirectory, "common checkpoint"); + if (!Files.isRegularFile( + pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), + LinkOption.NOFOLLOW_LINKS)) { + throw new IllegalStateException( + "Common checkpoint existing PathState requires one legacy CURRENT"); + } } } if (!pathExisted) { diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index f78fe965254..cb454edae9a 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -49,7 +49,7 @@ storage { pathStateRoot.enabled = false pathStateRoot.mode = "shadow" pathStateRoot.directory = "path-state-root" - pathStateRoot.formatVersion = 2 + pathStateRoot.formatVersion = 1 pathStateRoot.reversibleLayerLimit = 128 pathStateRoot.reversibleLayerBytes = 2147483648 pathStateRoot.writeBufferBytes = 268435456 diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java index d88817e5d96..fb54c1ba4f9 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCommitmentCodecTest.java @@ -32,21 +32,21 @@ public void fixedStoreKeyGoldensMatchIndependentKeccakOracle() throws Exception storageKey[i] = (byte) i; } - assertGolden("18d18850670fc1314f55e5346718606abf66777fdd67228eb193bbffffd9a2d7", + assertGolden("0b7f18d3381a9e44da93058f4214d7f0d818d1824be0f30839ed5200eb7f946a", 4, accountKey); - assertGolden("ec0aa93f7d668a1604e02eef0876edc5ff8e3904b117c172f066085fb290a26f", + assertGolden("90ad9575451bd26f005db5063deaeac71d8b221edd0a0bb0a345f21b40c16ff5", 22, storageKey); - assertGolden("21bccc1258bd8d933e0c1de0cb40c3c33e3c5b12beb832d29313f2f99dd9ce0d", + assertGolden("ec6a48ade48f24cd456a89de3a5f86d282b0ae9892f265be58e5e8a704856350", 21, new byte[]{1}); } @Test public void approvedAbiAndAssetIssueStoresHaveIndependentLeafDomains() throws Exception { - assertGolden("29f5801fed0819272800fc0bb431887f257e7e52d18086edbb61b21dd38a2aaa", + assertGolden("14af9866899065b509f6ea5d45902d443a4357efad5e6478c47ef108d603b8d7", 1, new byte[]{1}); - assertGolden("94ae32adcf9abf3bab5286ae66f5f1939083e78918f4621bbf7cd3ace8305101", + assertGolden("5c0a5639b07fa98f7e067c3e0c1d067ca1de752810b7576bc20fd2c75ba89eb5", 6, new byte[]{1}); - assertGolden("01c15364ad6927f41150cd5900739eb1a117c2f5e4d71c63c3a456eea63e401b", + assertGolden("99951328ba6d7d4a7fa199cf727a7c4494bd885ce2ac8c04475dd1fd699af7de", 7, new byte[]{1}); } @@ -77,8 +77,9 @@ public void superLeafGoldensBindStableIdentityFormatAndRoot() throws Exception { storeRoot[i] = (byte) i; } - assertEquals("8c8018ac64709921cac7388f659d7396acef5e373bf3b329f9d93088536434a1", - Hex.toHexString(PathStateCommitmentCodec.superLeafKey(4))); + assertArrayEquals( + Hex.decode("cf8715b85b2ac18d2b63e57b9e8902887f1986df7dc6a46da01fbc1f8f99f8bf"), + PathStateCommitmentCodec.superLeafKey(4)); assertArrayEquals(referenceSuperKey(4), PathStateCommitmentCodec.superLeafKey(4)); assertArrayEquals(Hex.decode("f38400000004876163636f756e748400000001a0000102030405060708090a0b" + "0c0d0e0f101112131415161718191a1b1c1d1e1f"), @@ -113,6 +114,7 @@ private static byte[] referenceStoreKey(int storeId, byte[] key) throws IOExcept try (DataOutputStream output = new DataOutputStream(bytes)) { output.writeShort(STORE_DOMAIN.length); output.write(STORE_DOMAIN); + output.writeShort(PathStateCommitmentCodec.FORMAT_VERSION); output.writeInt(storeId); output.writeInt(key.length); output.write(key); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index ba74c88f35a..5be947bdf95 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -391,6 +391,62 @@ public void commonCheckpointFreshFlushAndRestartUseOneDurableBoundary() throws E invoke(manager, "closePathStateRoot"); } + @Test + public void commonCheckpointAdoptsCompatibleLegacyCurrentWithoutRebuild() throws Exception { + Path output = temporaryFolder.newFolder("common-checkpoint-legacy-current").toPath(); + long baseNumber = 100L; + long timestamp = 300L; + Path root = output.resolve("path-state-root"); + BlockId baseId = new BlockId(Sha256Hash.wrap(bytes(41)), baseNumber); + byte[] parentHash = bytes(40); + PathStateRootMetadata legacy = publishEmptyPhysicalCurrent(root, baseNumber, + baseId.getBytes(), parentHash); + + DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(baseNumber); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(baseId); + when(dynamic.getLatestBlockHeaderTimestamp()).thenReturn(timestamp); + when(dynamic.getAllowAccountAssetOptimizationFromRoot()).thenReturn(1L); + BlockCapsule baseBlock = mock(BlockCapsule.class); + when(baseBlock.getNum()).thenReturn(baseNumber); + when(baseBlock.getBlockId()).thenReturn(baseId); + when(baseBlock.getParentHash()).thenReturn(Sha256Hash.wrap(parentHash)); + when(baseBlock.getTimeStamp()).thenReturn(timestamp); + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); + when(chainBase.getBlockByNum(baseNumber)).thenReturn(baseBlock); + when(chainBase.getAccountAssetStore()).thenReturn(mock(AccountAssetStore.class)); + + AtomicInteger pinnedSources = new AtomicInteger(); + Manager manager = new Manager(); + setChainBaseManager(manager, chainBase); + withCommonConfig(output, () -> { + SnapshotManager snapshots = new SnapshotManager(""); + for (PathStateParticipantDescriptor.StoreIdentity participant + : PathStateParticipantDescriptor.current().getStores()) { + snapshots.getDbs().add(emptyNativeStore(participant.getDbName(), baseNumber, + baseId.getBytes(), pinnedSources)); + } + snapshots.enable(); + snapshots.setUnChecked(false); + setField(manager, "revokingStore", snapshots); + invoke(manager, "initCommonCheckpoint"); + }); + + assertEquals(0, pinnedSources.get()); + assertArrayEquals(legacy.getStateRoot(), manager.getPathStateSnapshotHead().getHead() + .getStateRoot()); + assertTrue(Files.isRegularFile(output.resolve("common-checkpoint") + .resolve(CommonCheckpointBaselineFile.FILE_NAME))); + assertTrue(Files.isRegularFile(root.resolve(PathStateCheckpointMaterializer.COMMON_MODE_FILE))); + assertTrue(Files.isRegularFile(root.resolve("LEGACY_BASELINE"))); + assertTrue(Files.isRegularFile(root.resolve( + PathStateCheckpointMaterializer.COMMON_BASELINE_HEAD_FILE))); + assertFalse(Files.exists(root.resolve("CURRENT"))); + invoke(manager, "closeCommonCheckpoint"); + invoke(manager, "closePathStateRoot"); + } + @SuppressWarnings("unchecked") private static Chainbase propertiesStoreWithP66Enabled() { DB database = mock(DB.class); @@ -526,11 +582,16 @@ private static byte[] bytes(int seed) { private static PathStateRootMetadata publishEmptyPhysicalCurrent(Path root, long blockNumber, int blockSeed, int parentSeed) throws Exception { + return publishEmptyPhysicalCurrent(root, blockNumber, bytes(blockSeed), bytes(parentSeed)); + } + + private static PathStateRootMetadata publishEmptyPhysicalCurrent(Path root, + long blockNumber, byte[] blockHash, byte[] parentHash) throws Exception { try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(root, new PathStateCanonicalizer().participantScope(), Engine.ROCKSDB)) { PathStateRoot state = stores.buildRootFromFlat(); - PathStateRootMetadata metadata = PathStateRootMetadata.base(blockNumber, bytes(blockSeed), - bytes(parentSeed), 300, P66Phase.P66_ON, stores.getFormatDigest(), state.rootHash(), + PathStateRootMetadata metadata = PathStateRootMetadata.base(blockNumber, blockHash, + parentHash, 300, P66Phase.P66_ON, stores.getFormatDigest(), state.rootHash(), bytes(3)); stores.publishCurrent(metadata); return metadata; diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java index 32ad614fdda..c3d4195ab98 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStatePersistentFormatTest.java @@ -43,7 +43,7 @@ public void manifestCreatesIndependentCurrentOnlyLayoutAndReopensWithoutRewrite( assertTrue(Files.isDirectory(created.getLayersDirectory())); assertArrayEquals(original, Files.readAllBytes(manifest)); assertEquals( - "37fc0b69dae958872a3088ee060353e370c4ad7b9ff1804e5e151b47da1efa20", + "d0fc17ad2ea70578b2400c8c3563b05407ff6d7f53f26ea7ad47b513565d404e", ByteArray.toHexString(Hashing.sha256().hashBytes(original).asBytes())); assertFalse(Files.exists(root.resolve("history"))); } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java index fa11feda9f5..8d3cc7cf7c3 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateRootTest.java @@ -43,8 +43,9 @@ public void aggregatesEveryParticipantIntoIndependentOracleSuperRoot() { } byte[] expected = referenceRoot(participants, mutations); - assertEquals("16a59be5527b6c746e4bc2b0a67046989116f7f855a10ae0fb65263e9fb7bfda", - Hex.toHexString(expected)); + assertArrayEquals( + Hex.decode("f8d0364fdb0432016c12f9a660de2bd34513257014e35d90ac289d9024e6d216"), + expected); assertArrayEquals(expected, stateRoot.rootHash()); } From e92aae0c8922726f4b255764a7dd1981fe8a9ebb Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 5 Sep 2026 18:38:24 +0800 Subject: [PATCH 118/161] perf(db): reuse archive checkpoint index handle Keep one serving-index database handle open while a common checkpoint crosses both redo barriers. Continue reading and validating the marker at every barrier, then release the handle on both success and failure. --- .../StateArchiveCheckpointMaterializer.java | 34 +++- .../StateArchiveCheckpointServingIndex.java | 162 ++++++++++++------ .../core/CommonCheckpointMaterializer.java | 8 + .../core/CommonCheckpointRedoCoordinator.java | 50 ++++++ .../CommonCheckpointRedoCoordinatorTest.java | 35 ++++ 5 files changed, 235 insertions(+), 54 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java index 69a5c7dc37d..dd8b5b4a4ec 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java @@ -55,6 +55,7 @@ public final class StateArchiveCheckpointMaterializer implements CommonCheckpoin private final FaultHook faultHook; private final CommonCheckpointBaseline baseline; private final Engine engine; + private StateArchiveCheckpointServingIndex.Session checkpointServingIndex; public StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity) { this(directory, formatIdentity, null, StateArchiveCheckpointServingIndex.configuredEngine(), @@ -97,6 +98,27 @@ public Authority authority() { return Authority.STATE_ARCHIVE; } + @Override + public synchronized void beginCheckpoint(CommonCheckpointTarget target) throws IOException { + requireTarget(target); + if (checkpointServingIndex != null) { + throw new IOException("State Archive checkpoint serving session is already open"); + } + checkpointServingIndex = StateArchiveCheckpointServingIndex.session(directory, engine); + } + + @Override + public synchronized void endCheckpoint(CommonCheckpointTarget target) throws IOException { + requireTarget(target); + if (checkpointServingIndex != null) { + try { + checkpointServingIndex.close(); + } finally { + checkpointServingIndex = null; + } + } + } + @Override public synchronized Status inspect(CommonCheckpointTarget target) throws IOException { CommonCheckpointTarget admitted = requireTarget(target); @@ -184,7 +206,11 @@ public synchronized void materialize(CommonCheckpointPayload payload, faultHook.after(Stage.AFTER_BLOCK_FILE, index); } requireExactBlockSet(blocks, expectedNames); - StateArchiveCheckpointServingIndex.apply(directory, admittedPayload, admittedTarget, engine); + if (checkpointServingIndex == null) { + StateArchiveCheckpointServingIndex.apply(directory, admittedPayload, admittedTarget, engine); + } else { + checkpointServingIndex.apply(admittedPayload, admittedTarget); + } faultHook.after(Stage.AFTER_SERVING_INDEX_BATCH, -1); publishImmutable(materializedPath(admittedTarget), encodeTarget(admittedTarget)); faultHook.after(Stage.AFTER_MATERIALIZED_TARGET, -1); @@ -245,8 +271,10 @@ private void requireParent(TargetMarker current, CommonCheckpointTarget target) } private void requireServingIndex(CommonCheckpointTarget target) throws IOException { - if (StateArchiveCheckpointServingIndex.inspect(directory, target, engine) - != StateArchiveCheckpointServingIndex.Status.EXACT) { + StateArchiveCheckpointServingIndex.Status status = checkpointServingIndex == null + ? StateArchiveCheckpointServingIndex.inspect(directory, target, engine) + : checkpointServingIndex.inspect(target); + if (status != StateArchiveCheckpointServingIndex.Status.EXACT) { throw new IOException("State Archive checkpoint serving index target differs"); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java index 5a630d71dce..da4c76e9245 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java @@ -57,17 +57,7 @@ static Status inspect(Path archiveDirectory, CommonCheckpointTarget target, Engi StateArchiveIndexEngineManifest.require(archiveDirectory.resolve(DIRECTORY), engine); try (StateArchiveIndexDatabase.Reader database = StateArchiveIndexDatabase.openReader(databasePath, engine)) { - byte[] encoded = database.get(MARKER_KEY); - if (encoded == null) { - throw new IOException("State Archive checkpoint serving marker is missing"); - } - Marker marker = decodeMarker(encoded); - if (Arrays.equals(encoded, encodeMarker(target, marker.baseBlockNumber, - marker.baseBlockHash))) { - return Status.EXACT; - } - requireParent(marker, target); - return Status.PARENT; + return inspect(database.get(MARKER_KEY), target); } } @@ -78,47 +68,27 @@ static void apply(Path archiveDirectory, CommonCheckpointPayload payload, static void apply(Path archiveDirectory, CommonCheckpointPayload payload, CommonCheckpointTarget target, Engine engine) throws IOException { - Status status = inspect(archiveDirectory, target, engine); - if (status == Status.EXACT) { - return; - } - Path indexDirectory = archiveDirectory.resolve(DIRECTORY); - Files.createDirectories(indexDirectory); - if (!Files.isDirectory(indexDirectory, LinkOption.NOFOLLOW_LINKS)) { - throw new IOException("State Archive checkpoint serving path is not a directory"); - } - StateArchiveIndexEngineManifest.openOrCreate(indexDirectory, engine); - long baseBlockNumber = target.getFirstBlock().getBlockNumber() - 1; - byte[] baseBlockHash = target.getFirstBlock().getParentHash(); - Path databasePath = databasePath(archiveDirectory); - try (StateArchiveIndexDatabase.Writer database = - StateArchiveIndexDatabase.openWriter(databasePath, engine)) { - byte[] existing = database.get(MARKER_KEY); - if (existing != null) { - Marker parent = decodeMarker(existing); - requireParent(parent, target); - baseBlockNumber = parent.baseBlockNumber; - baseBlockHash = parent.baseBlockHash; - } - List mutations = new ArrayList<>(); - for (int index = 0; index < payload.getBlocks().size(); index++) { - CommonCheckpointPayload.BlockPayload block = payload.getBlocks().get(index); - long blockNumber = block.getMeta().getBlockNumber(); - for (DbGroup group : block.getArchiveDiff().getGroups()) { - requireStateDatabase(group.getDbName()); - for (Entry entry : group.getEntries()) { - mutations.add(StateArchiveIndexDatabase.put( - changeKey(group.getDbName(), entry.getKey(), blockNumber), new byte[]{1})); - } - } - mutations.add(StateArchiveIndexDatabase.put(blockKey(blockNumber), - encodeLocation(target.getPayloadDigest(), index, block.getMeta()))); - } - mutations.add(StateArchiveIndexDatabase.put(MARKER_KEY, - encodeMarker(target, baseBlockNumber, baseBlockHash))); - database.write(mutations); + try (Session session = session(archiveDirectory, engine)) { + session.apply(payload, target); + } + } + + static Session session(Path archiveDirectory, Engine engine) { + return new Session(archiveDirectory, engine); + } + + private static Status inspect(byte[] encoded, CommonCheckpointTarget target) + throws IOException { + if (encoded == null) { + throw new IOException("State Archive checkpoint serving marker is missing"); + } + Marker marker = decodeMarker(encoded); + if (Arrays.equals(encoded, encodeMarker(target, marker.baseBlockNumber, + marker.baseBlockHash))) { + return Status.EXACT; } - HistorySegmentStore.syncDirectory(indexDirectory); + requireParent(marker, target); + return Status.PARENT; } static Reader openReader(Path archiveDirectory, CommonCheckpointTarget target) @@ -294,6 +264,96 @@ enum Status { EXACT } + static final class Session implements AutoCloseable { + + private final Path archiveDirectory; + private final Engine engine; + private StateArchiveIndexDatabase.Writer database; + + private Session(Path archiveDirectory, Engine engine) { + this.archiveDirectory = Objects.requireNonNull(archiveDirectory, "archiveDirectory"); + this.engine = Objects.requireNonNull(engine, "engine"); + } + + Status inspect(CommonCheckpointTarget target) throws IOException { + Path databasePath = databasePath(archiveDirectory); + if (database == null && !Files.exists(databasePath, LinkOption.NOFOLLOW_LINKS)) { + return Status.ABSENT; + } + return StateArchiveCheckpointServingIndex.inspect( + openExisting().get(MARKER_KEY), target); + } + + void apply(CommonCheckpointPayload payload, CommonCheckpointTarget target) + throws IOException { + Path indexDirectory = archiveDirectory.resolve(DIRECTORY); + Path databasePath = databasePath(archiveDirectory); + boolean databaseExisted = Files.exists(databasePath, LinkOption.NOFOLLOW_LINKS); + Files.createDirectories(indexDirectory); + if (!Files.isDirectory(indexDirectory, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("State Archive checkpoint serving path is not a directory"); + } + StateArchiveIndexEngineManifest.openOrCreate(indexDirectory, engine); + StateArchiveIndexDatabase.Writer writer = open(); + byte[] existing = writer.get(MARKER_KEY); + long baseBlockNumber = target.getFirstBlock().getBlockNumber() - 1; + byte[] baseBlockHash = target.getFirstBlock().getParentHash(); + if (existing != null) { + Marker parent = decodeMarker(existing); + if (Arrays.equals(existing, + encodeMarker(target, parent.baseBlockNumber, parent.baseBlockHash))) { + return; + } + requireParent(parent, target); + baseBlockNumber = parent.baseBlockNumber; + baseBlockHash = parent.baseBlockHash; + } else if (databaseExisted) { + throw new IOException("State Archive checkpoint serving marker is missing"); + } + List mutations = new ArrayList<>(); + for (int index = 0; index < payload.getBlocks().size(); index++) { + CommonCheckpointPayload.BlockPayload block = payload.getBlocks().get(index); + long blockNumber = block.getMeta().getBlockNumber(); + for (DbGroup group : block.getArchiveDiff().getGroups()) { + requireStateDatabase(group.getDbName()); + for (Entry entry : group.getEntries()) { + mutations.add(StateArchiveIndexDatabase.put( + changeKey(group.getDbName(), entry.getKey(), blockNumber), new byte[]{1})); + } + } + mutations.add(StateArchiveIndexDatabase.put(blockKey(blockNumber), + encodeLocation(target.getPayloadDigest(), index, block.getMeta()))); + } + mutations.add(StateArchiveIndexDatabase.put(MARKER_KEY, + encodeMarker(target, baseBlockNumber, baseBlockHash))); + writer.write(mutations); + HistorySegmentStore.syncDirectory(indexDirectory); + } + + private StateArchiveIndexDatabase.Writer openExisting() throws IOException { + StateArchiveIndexEngineManifest.require(archiveDirectory.resolve(DIRECTORY), engine); + return open(); + } + + private StateArchiveIndexDatabase.Writer open() throws IOException { + if (database == null) { + database = StateArchiveIndexDatabase.openWriter(databasePath(archiveDirectory), engine); + } + return database; + } + + @Override + public void close() throws IOException { + if (database != null) { + try { + database.close(); + } finally { + database = null; + } + } + } + } + static final class Reader implements AutoCloseable { private final Path archiveDirectory; diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java index d3c63886305..67af11c84bb 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java @@ -7,6 +7,14 @@ public interface CommonCheckpointMaterializer { Authority authority(); + /** Opens resources that may be reused while one target crosses both coordinator barriers. */ + default void beginCheckpoint(CommonCheckpointTarget target) throws IOException { + } + + /** Releases resources opened for one target after publication, retirement, or failure. */ + default void endCheckpoint(CommonCheckpointTarget target) throws IOException { + } + /** * Returns only an exact state for {@code target}. Implementations must throw when durable state * is corrupt, ambiguous, or belongs to a different target. diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java index f0ec6ec586b..e640ee59a57 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java @@ -48,6 +48,13 @@ public synchronized RecoveryAction recover() throws IOException { private RecoveryAction redo(CommonCheckpointPayload payload) throws IOException { CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + try (CheckpointScope ignored = new CheckpointScope(target)) { + return redo(payload, target); + } + } + + private RecoveryAction redo(CommonCheckpointPayload payload, CommonCheckpointTarget target) + throws IOException { Map initial = inspectAll(target); if (initial.containsValue(Status.PUBLISHED) && initial.containsValue(Status.NEEDS_MATERIALIZATION)) { @@ -88,6 +95,49 @@ private RecoveryAction redo(CommonCheckpointPayload payload) throws IOException return RecoveryAction.COMPLETED_REDO; } + private final class CheckpointScope implements AutoCloseable { + + private final CommonCheckpointTarget target; + private int opened; + + private CheckpointScope(CommonCheckpointTarget target) throws IOException { + this.target = target; + try { + for (Authority authority : ORDER) { + materializers.get(authority).beginCheckpoint(target); + opened++; + } + } catch (IOException | RuntimeException failure) { + try { + close(); + } catch (IOException closing) { + failure.addSuppressed(closing); + } + throw failure; + } + } + + @Override + public void close() throws IOException { + IOException failure = null; + while (opened > 0) { + CommonCheckpointMaterializer materializer = materializers.get(ORDER[--opened]); + try { + materializer.endCheckpoint(target); + } catch (IOException closing) { + if (failure == null) { + failure = closing; + } else { + failure.addSuppressed(closing); + } + } + } + if (failure != null) { + throw failure; + } + } + } + private Map inspectAll(CommonCheckpointTarget target) throws IOException { Map statuses = new EnumMap<>(Authority.class); for (Authority authority : ORDER) { diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java index 73f845184e2..b826a1e8877 100644 --- a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java @@ -92,6 +92,26 @@ public void rejectsWrongAuthorityAndMaterializerThatDoesNotReachExactStatus() th assertEquals(Collections.singletonList("materialize-CHAINBASE"), fixture.actions); } + @Test + public void closesEveryCheckpointScopeAfterSuccessAndFailure() throws Exception { + Fixture completed = fixture("scope-success", null); + assertEquals(RecoveryAction.COMPLETED_REDO, completed.coordinator.apply(completed.payload)); + for (FakeMaterializer materializer : completed.materializers) { + assertEquals(1, materializer.scopesStarted); + assertEquals(1, materializer.scopesEnded); + assertFalse(materializer.scopeOpen); + } + + Fixture failed = fixture("scope-failure", + CommonCheckpointRedoCoordinator.Stage.AFTER_CHAINBASE_MATERIALIZE); + assertThrows(IOException.class, () -> failed.coordinator.apply(failed.payload)); + for (FakeMaterializer materializer : failed.materializers) { + assertEquals(1, materializer.scopesStarted); + assertEquals(1, materializer.scopesEnded); + assertFalse(materializer.scopeOpen); + } + } + @Test public void runtimeOwnerRequiresStartupRecoveryAndGatesReadsAroundApply() throws Exception { Fixture fixture = fixture("runtime-owner", null); @@ -239,6 +259,9 @@ private static final class FakeMaterializer implements CommonCheckpointMateriali private Status status = Status.NEEDS_MATERIALIZATION; private CommonCheckpointTarget target; private boolean advanceAfterMaterialize = true; + private int scopesStarted; + private int scopesEnded; + private boolean scopeOpen; private FakeMaterializer(Authority authority, List actions) { this.authority = authority; @@ -250,6 +273,18 @@ public Authority authority() { return authority; } + @Override + public void beginCheckpoint(CommonCheckpointTarget expected) { + scopesStarted++; + scopeOpen = true; + } + + @Override + public void endCheckpoint(CommonCheckpointTarget expected) { + scopesEnded++; + scopeOpen = false; + } + @Override public Status inspect(CommonCheckpointTarget expected) throws IOException { if (target != null && !target.equals(expected)) { From 37723917cbddd98df067a7c9576300ebd9f14725 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sat, 5 Sep 2026 23:12:44 +0800 Subject: [PATCH 119/161] fix(chainbase): bound path state snapshot retention --- .../core/CommonCheckpointMemoryRebaser.java | 17 ++ .../db2/core/CommonCheckpointRuntime.java | 20 +- .../core/CommonCheckpointSnapshotRebaser.java | 35 +++- .../core/db2/stateroot/PathMerkleTrie.java | 10 + .../PathStatePhysicalOverlayHead.java | 174 +++++++++++++++++- .../core/db2/stateroot/PathStateRoot.java | 43 +++++ .../db2/stateroot/PathStateSnapshotDelta.java | 14 +- .../main/java/org/tron/core/db/Manager.java | 3 +- .../ChainbaseCheckpointMaterializerTest.java | 42 ++++- ...CommonCheckpointRuntimeAttachmentTest.java | 13 +- .../PathStateNativeNodeStoreTest.java | 116 +++++++++++- 11 files changed, 452 insertions(+), 35 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMemoryRebaser.java diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMemoryRebaser.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMemoryRebaser.java new file mode 100644 index 00000000000..56e7eba893e --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMemoryRebaser.java @@ -0,0 +1,17 @@ +package org.tron.core.db2.core; + +import java.io.IOException; + +/** Builds an in-memory completion plan after every durable authority published one target. */ +@FunctionalInterface +public interface CommonCheckpointMemoryRebaser { + + RebasePlan prepare(CommonCheckpointTarget target) throws IOException; + + /** A fully validated pointer-only completion that must not perform fallible work. */ + @FunctionalInterface + interface RebasePlan { + + void apply(); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java index fb387f97ee5..b6f06ce0413 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java @@ -19,20 +19,15 @@ public final class CommonCheckpointRuntime implements AutoCloseable { private final byte[] formatIdentity; private final Engine engine; private final StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory; + private final CommonCheckpointMemoryRebaser memoryRebaser; private final CommonCheckpointPayloadFactory payloadFactory = new CommonCheckpointPayloadFactory(); private final CommonCheckpointSnapshotRebaser rebaser = new CommonCheckpointSnapshotRebaser(); private CommonCheckpointTarget publishedTarget; - public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, - Path archiveDirectory, byte[] formatIdentity, - StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory) { - this(owner, databases, archiveDirectory, formatIdentity, - StateArchiveCheckpointMaterializer.configuredEngine(), latestFactory); - } - public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, Path archiveDirectory, byte[] formatIdentity, Engine engine, - StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory) { + StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory, + CommonCheckpointMemoryRebaser memoryRebaser) { this.owner = Objects.requireNonNull(owner, "owner"); this.databases = new ArrayList<>(Objects.requireNonNull(databases, "databases")); if (this.databases.isEmpty() || this.databases.contains(null)) { @@ -42,6 +37,7 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List rebaser.rebase(databases, target, flushCount)); + owner.apply(payload, () -> { + CommonCheckpointSnapshotRebaser.Plan chainbasePlan = + rebaser.prepare(databases, target, flushCount); + CommonCheckpointMemoryRebaser.RebasePlan pathStatePlan = memoryRebaser.prepare(target); + chainbasePlan.apply(); + pathStatePlan.apply(); + }); publishedTarget = target; return target; } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointSnapshotRebaser.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointSnapshotRebaser.java index 1c7aa645a01..af0ce568b12 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointSnapshotRebaser.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointSnapshotRebaser.java @@ -16,23 +16,27 @@ public final class CommonCheckpointSnapshotRebaser { */ public void rebase(List databases, CommonCheckpointTarget target, int count) throws IOException { + prepare(databases, target, count).apply(); + } + + /** Fully validates every Store before returning a pointer-only rebase plan. */ + Plan prepare(List databases, CommonCheckpointTarget target, int count) + throws IOException { CommonCheckpointTarget admittedTarget = Objects.requireNonNull(target, "target"); if (count <= 0) { throw new IllegalArgumentException("common checkpoint rebase count must be positive"); } - List plans = new ArrayList<>(); + List plans = new ArrayList<>(); for (Chainbase database : Objects.requireNonNull(databases, "databases")) { plans.add(validate(Objects.requireNonNull(database, "database"), admittedTarget, count)); } if (plans.isEmpty()) { throw new IOException("common checkpoint rebase requires registered Stores"); } - for (Plan plan : plans) { - plan.apply(); - } + return new Plan(plans); } - private static Plan validate(Chainbase database, CommonCheckpointTarget target, int count) + private static StorePlan validate(Chainbase database, CommonCheckpointTarget target, int count) throws IOException { Snapshot rootSnapshot = database.getHead().getRoot(); if (!(rootSnapshot instanceof SnapshotRoot)) { @@ -69,7 +73,7 @@ private static Plan validate(Chainbase database, CommonCheckpointTarget target, throw new IOException("common checkpoint rebase Store chain is disconnected: " + database.getDbName()); } - return new Plan(database, root, next, successor, head == next); + return new StorePlan(database, root, next, successor, head == next); } private static boolean isChild(BlockSnapshotMeta parent, BlockSnapshotMeta child) { @@ -78,7 +82,22 @@ private static boolean isChild(BlockSnapshotMeta parent, BlockSnapshotMeta child && Arrays.equals(child.getParentHash(), parent.getBlockHash()); } - private static final class Plan { + static final class Plan { + + private final List stores; + + private Plan(List stores) { + this.stores = new ArrayList<>(stores); + } + + void apply() { + for (StorePlan store : stores) { + store.apply(); + } + } + } + + private static final class StorePlan { private final Chainbase database; private final SnapshotRoot root; @@ -86,7 +105,7 @@ private static final class Plan { private final Snapshot successor; private final boolean consumesHead; - private Plan(Chainbase database, SnapshotRoot root, Snapshot last, Snapshot successor, + private StorePlan(Chainbase database, SnapshotRoot root, Snapshot last, Snapshot successor, boolean consumesHead) { this.database = database; this.root = root; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index da8dd8f9346..c6d3d339f35 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -1755,6 +1755,16 @@ private BytesKey materializedPath(Node node) { byte[] rootHash() { return Arrays.copyOf(rootHash, rootHash.length); } + + int depth() { + int depth = 0; + Snapshot cursor = this; + while (cursor != null) { + depth++; + cursor = cursor.parent; + } + return depth; + } } static final class LeafEntry { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java index d32194e0eee..88495219738 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java @@ -17,6 +17,8 @@ import lombok.extern.slf4j.Slf4j; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.core.CommonCheckpointBaseline; +import org.tron.core.db2.core.CommonCheckpointMemoryRebaser; +import org.tron.core.db2.core.CommonCheckpointTarget; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; @@ -153,6 +155,62 @@ public synchronized PathStateCheckpointMaterializer checkpointMaterializer( return new PathStateCheckpointMaterializer(stores, scope, formatIdentity, baseline); } + /** Builds a fully validated parentless-target plus reversible-suffix memory rebase. */ + public synchronized CommonCheckpointMemoryRebaser.RebasePlan prepareCommonCheckpointRebase( + CommonCheckpointTarget target) throws IOException { + requireHealthy(); + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + if (pending != null) { + throw new IOException("path-state checkpoint cannot rebase a pending transition"); + } + PathStateCheckpointMaterializer.PublishedHead published = + PathStateCheckpointMaterializer.loadPublishedHead(stores.getDirectory(), + admitted.getFormatIdentity()); + BlockSnapshotMeta targetBlock = admitted.getLastBlock(); + if (published.getEpoch() != targetBlock.getEpoch() + || published.getBlockNumber() != targetBlock.getBlockNumber() + || !Arrays.equals(published.getBlockHash(), targetBlock.getBlockHash()) + || !Arrays.equals(published.getStateRoot(), admitted.getStateRoot()) + || !Arrays.equals(published.getPayloadDigest(), admitted.getPayloadDigest())) { + throw new IOException("PathState published CURRENT differs from common checkpoint target"); + } + + TargetPosition position = targetPosition(admitted); + PathStateRoot baseline = new PathStateRoot(scope, + participant -> stores.participant(participant.getDbName()).nodeStore(), + stores.superStore().nodeStore()); + baseline.restoreStoredRoots(admitted.getStateRoot()); + PathStateRoot.Snapshot candidateSnapshot = baseline.snapshot(); + PathStateRootMetadata candidateHead = PathStateRootMetadata.base( + targetBlock.getBlockNumber(), targetBlock.getBlockHash(), targetBlock.getParentHash(), + targetBlock.getTimestamp(), position.metadata.getPhase(), formatDigest, + admitted.getStateRoot(), admitted.getPayloadDigest()); + List candidateHistory = new ArrayList<>(); + BlockSnapshotMeta replayParent = targetBlock; + + for (int index = position.historyIndex; index < history.size(); index++) { + HeadState retained = history.get(index); + PathStateRootMetadata child = index + 1 < history.size() + ? history.get(index + 1).metadata : head; + validateReplayStep(candidateHead, replayParent, retained.deltaToChild, child); + candidateHistory.add(new HeadState(candidateHead, candidateSnapshot, + retained.deltaToChild)); + candidateSnapshot = replay(candidateSnapshot, retained.deltaToChild); + candidateHead = copy(child); + replayParent = retained.deltaToChild.getMeta(); + } + if (!Arrays.equals(candidateSnapshot.getStateRoot(), head.getStateRoot()) + || candidateHead.getBlockNumber() != head.getBlockNumber() + || !Arrays.equals(candidateHead.getBlockHash(), head.getBlockHash())) { + throw new IOException("path-state checkpoint rebase candidate differs from live head"); + } + + PathStateRootMetadata installedHead = candidateHead; + PathStateRoot.Snapshot installedSnapshot = candidateSnapshot; + List installedHistory = new ArrayList<>(candidateHistory); + return () -> installRebase(installedHead, installedSnapshot, installedHistory); + } + /** Retires this freshly rebuilt legacy pointer before the first common checkpoint is enabled. */ public synchronized void admitFreshCommonBaseline(CommonCheckpointBaseline baseline) throws IOException { @@ -205,7 +263,7 @@ public synchronized PathStateRootMetadata advance(PathStateBlockTransition trans if (pending == null || pending.transition != admitted) { throw new IOException("path-state overlay publication differs from prepared transition"); } - history.add(new HeadState(head, snapshot)); + history.add(new HeadState(head, snapshot, pending.delta)); while (history.size() > maxHistory) { history.remove(0); } @@ -311,6 +369,12 @@ long durableWriteBatchCalls() { return calls; } + synchronized RetentionCensus retentionCensus() throws IOException { + requireHealthy(); + return new RetentionCensus(head.getBlockNumber(), history.size(), snapshot.trieCount(), + snapshot.maxTrieDepth()); + } + @Override public synchronized void close() throws IOException { if (closed) { @@ -389,6 +453,66 @@ private PreparedOverlay prepare(BlockSnapshotMeta meta, PathStateBlockTransition candidate.hashReferenceCreateCount(), candidate.hashReferenceResolveCount(), stats); } + private TargetPosition targetPosition(CommonCheckpointTarget target) throws IOException { + if (matches(head, target.getLastBlock().getBlockNumber(), + target.getLastBlock().getBlockHash()) + && Arrays.equals(head.getStateRoot(), target.getStateRoot())) { + return new TargetPosition(head, history.size()); + } + for (int index = history.size() - 1; index >= 0; index--) { + HeadState candidate = history.get(index); + if (matches(candidate.metadata, target.getLastBlock().getBlockNumber(), + target.getLastBlock().getBlockHash()) + && Arrays.equals(candidate.metadata.getStateRoot(), target.getStateRoot())) { + return new TargetPosition(candidate.metadata, index); + } + } + throw new IOException("PathState checkpoint target is outside reversible history"); + } + + private PathStateRoot.Snapshot replay(PathStateRoot.Snapshot parent, + PathStateSnapshotDelta delta) throws IOException { + Map recordings = new LinkedHashMap<>(); + PathStateRoot candidate = PathStateRoot.fromSnapshot(scope, + participant -> recordings.computeIfAbsent(participant.getStoreId(), ignored -> + new RecordingStore(stores.participant(participant.getDbName()).nodeStore())), + recordings.computeIfAbsent(0, ignored -> + new RecordingStore(stores.superStore().nodeStore())), parent); + try { + candidate.replaySnapshotDelta(delta); + return candidate.snapshot(); + } catch (IllegalArgumentException | IllegalStateException failure) { + throw new IOException("path-state checkpoint suffix replay failed", failure); + } + } + + private void validateReplayStep(PathStateRootMetadata parent, BlockSnapshotMeta parentBlock, + PathStateSnapshotDelta delta, PathStateRootMetadata child) throws IOException { + if (delta == null || child.getKind() != PathStateRootMetadata.Kind.LAYER + || delta.getMeta().getEpoch() != parentBlock.getEpoch() + 1 + || delta.getMeta().getBlockNumber() != child.getBlockNumber() + || !Arrays.equals(delta.getMeta().getBlockHash(), child.getBlockHash()) + || !Arrays.equals(delta.getMeta().getParentHash(), child.getParentHash()) + || delta.getMeta().getTimestamp() != child.getTimestamp() + || child.getBlockNumber() != parent.getBlockNumber() + 1 + || !Arrays.equals(child.getParentHash(), parent.getBlockHash()) + || !Arrays.equals(delta.getParentStateRoot(), parent.getStateRoot()) + || !Arrays.equals(child.getParentStateRoot(), parent.getStateRoot()) + || !Arrays.equals(delta.getStateRoot(), child.getStateRoot()) + || !Arrays.equals(delta.getTransitionPayloadDigest(), child.getPayloadDigest()) + || !Arrays.equals(child.getFormatDigest(), formatDigest)) { + throw new IOException("path-state checkpoint suffix identity mismatch"); + } + } + + private synchronized void installRebase(PathStateRootMetadata installedHead, + PathStateRoot.Snapshot installedSnapshot, List installedHistory) { + head = installedHead; + snapshot = installedSnapshot; + history.clear(); + history.addAll(installedHistory); + } + private void requireChild(PathStateBlockTransition transition) throws IOException { if (transition.getBlockNumber() != head.getBlockNumber() + 1 || !Arrays.equals(transition.getParentHash(), head.getBlockHash())) { @@ -433,10 +557,56 @@ private static final class HeadState { private final PathStateRootMetadata metadata; private final PathStateRoot.Snapshot snapshot; + private final PathStateSnapshotDelta deltaToChild; - private HeadState(PathStateRootMetadata metadata, PathStateRoot.Snapshot snapshot) { + private HeadState(PathStateRootMetadata metadata, PathStateRoot.Snapshot snapshot, + PathStateSnapshotDelta deltaToChild) { this.metadata = metadata; this.snapshot = snapshot; + this.deltaToChild = deltaToChild; + } + } + + private static final class TargetPosition { + + private final PathStateRootMetadata metadata; + private final int historyIndex; + + private TargetPosition(PathStateRootMetadata metadata, int historyIndex) { + this.metadata = metadata; + this.historyIndex = historyIndex; + } + } + + static final class RetentionCensus { + + private final long headBlockNumber; + private final int suffixBlocks; + private final int trieCount; + private final int maxSnapshotDepth; + + private RetentionCensus(long headBlockNumber, int suffixBlocks, int trieCount, + int maxSnapshotDepth) { + this.headBlockNumber = headBlockNumber; + this.suffixBlocks = suffixBlocks; + this.trieCount = trieCount; + this.maxSnapshotDepth = maxSnapshotDepth; + } + + long getHeadBlockNumber() { + return headBlockNumber; + } + + int getSuffixBlocks() { + return suffixBlocks; + } + + int getTrieCount() { + return trieCount; + } + + int getMaxSnapshotDepth() { + return maxSnapshotDepth; } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index 9c2c9b2833f..b017f99585f 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -330,6 +330,37 @@ synchronized void restoreStoredRoots(byte[] expectedRoot) { rootMaterialized = true; } + /** Replays one immutable secure-key delta without requiring the original raw Store keys. */ + synchronized void replaySnapshotDelta(PathStateSnapshotDelta delta) { + PathStateSnapshotDelta admitted = Objects.requireNonNull(delta, "delta"); + if (!Arrays.equals(rootHash(), admitted.getParentStateRoot())) { + throw new IllegalArgumentException("path-state replay parent root mismatch"); + } + Set changed = new LinkedHashSet<>(); + for (PathStateSnapshotDelta.StoreDelta store : admitted.getStores()) { + PathStateParticipant participant = scope.require(store.getDbName()); + if (participant.getStoreId() != store.getStoreId() || !changed.add(store.getStoreId())) { + throw new IllegalArgumentException("path-state replay participant identity mismatch"); + } + PathMerkleTrie trie = participantTries.get(participant.getDbName()); + for (PathStateSnapshotDelta.Mutation mutation : store.getFlatMutations()) { + if (mutation.isDelete()) { + trie.delete(mutation.getKey()); + } else { + trie.put(mutation.getKey(), mutation.getValue()); + } + } + if (!Arrays.equals(trie.rootHash(), store.getStoreRoot())) { + throw new IllegalArgumentException("path-state replay Store root mismatch: " + + store.getDbName()); + } + } + rootMaterialized = false; + if (!Arrays.equals(rootHash(), admitted.getStateRoot())) { + throw new IllegalArgumentException("path-state replay state root mismatch"); + } + } + synchronized void recordPendingLeafMutations(Collection mutations) { recordPendingLeafMutations(prepare(mutations)); } @@ -682,6 +713,18 @@ byte[] participantRoot(String dbName) { } return participant.rootHash(); } + + int maxTrieDepth() { + int depth = superTrie.depth(); + for (PathMerkleTrie.Snapshot participant : participants.values()) { + depth = Math.max(depth, participant.depth()); + } + return depth; + } + + int trieCount() { + return participants.size() + 1; + } } static final class LeafRecord { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java index a3d652ea4bf..9fbbe12aae7 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java @@ -28,12 +28,10 @@ public final class PathStateSnapshotDelta { private final byte[] mutationViewDigest; private final List stores; private final List superNodeMutations; - private final PathStateRoot.Snapshot trieSnapshot; private PathStateSnapshotDelta(BlockSnapshotMeta meta, byte[] parentStateRoot, byte[] stateRoot, byte[] transitionPayloadDigest, byte[] mutationViewDigest, - List stores, List superNodeMutations, - PathStateRoot.Snapshot trieSnapshot) { + List stores, List superNodeMutations) { this.meta = Objects.requireNonNull(meta, "meta"); this.parentStateRoot = root(parentStateRoot, "parentStateRoot"); this.stateRoot = root(stateRoot, "stateRoot"); @@ -41,7 +39,6 @@ private PathStateSnapshotDelta(BlockSnapshotMeta meta, byte[] parentStateRoot, this.mutationViewDigest = root(mutationViewDigest, "mutationViewDigest"); this.stores = Collections.unmodifiableList(new ArrayList<>(stores)); this.superNodeMutations = immutableMutations(superNodeMutations); - this.trieSnapshot = Objects.requireNonNull(trieSnapshot, "trieSnapshot"); } static PathStateSnapshotDelta from(BlockSnapshotMeta meta, @@ -86,7 +83,7 @@ static PathStateSnapshotDelta from(BlockSnapshotMeta meta, } return new PathStateSnapshotDelta(admittedMeta, candidate.getParent().getStateRoot(), candidate.getStateRoot(), transition.getPayloadDigest(), - transition.getMutationViewDigest(), deltas, superMutations, candidate.getSnapshot()); + transition.getMutationViewDigest(), deltas, superMutations); } static PathStateSnapshotDelta fromPhysical(BlockSnapshotMeta meta, @@ -101,8 +98,7 @@ static PathStateSnapshotDelta fromPhysical(BlockSnapshotMeta meta, requireSameBlock(admittedMeta, admittedTransition); return new PathStateSnapshotDelta(admittedMeta, admittedParent.getStateRoot(), admittedSnapshot.getStateRoot(), admittedTransition.getPayloadDigest(), - admittedTransition.getMutationViewDigest(), stores, superNodeMutations, - admittedSnapshot); + admittedTransition.getMutationViewDigest(), stores, superNodeMutations); } public BlockSnapshotMeta getMeta() { @@ -133,10 +129,6 @@ public List getSuperNodeMutations() { return superNodeMutations; } - PathStateRoot.Snapshot getTrieSnapshot() { - return trieSnapshot; - } - private static void requireSameBlock(BlockSnapshotMeta meta, PathStateBlockTransition transition) { if (meta.getBlockNumber() != transition.getBlockNumber() diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 3b0b07d7acc..e3e67dee0a1 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -872,7 +872,8 @@ private void initCommonCheckpoint() { PathStatePhysicalOverlayHead admittedOwner = pathOwner; attachment = CommonCheckpointRuntimeAttachment.open(true, () -> new CommonCheckpointRuntime(new CommonCheckpointRuntimeOwner(coordinator), - snapshots.getDbs(), archiveDirectory, formatIdentity, engine, latest::pin)); + snapshots.getDbs(), archiveDirectory, formatIdentity, engine, latest::pin, + admittedOwner::prepareCommonCheckpointRebase)); canonical = currentCanonicalBlockMeta(); if (Files.isRegularFile(pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), diff --git a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java index eae8369918a..f7396dcfdad 100644 --- a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java @@ -300,6 +300,45 @@ public void snapshotRebaserPrevalidatesEveryStoreBeforeChangingAnyChain() { assertSame(storageLayer, storageChainbase.getHead()); } + @Test + public void memoryRebaseFailureLeavesEverySnapshotPointerUnchanged() throws Exception { + java.nio.file.Path root = temporaryFolder.newFolder("memory-rebase-failure").toPath(); + byte[] format = hash(94); + MemoryDb code = new MemoryDb("code"); + Chainbase database = new Chainbase(new SnapshotRoot(code)); + List databases = Collections.singletonList(database); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); + byte[] view = hash(41); + PathStateSnapshotDelta path = pathDelta(meta, hash(10), hash(11), view); + BlockReverseDiff archiveBlock = new BlockReverseDiff(meta, Collections.emptyList(), view); + SnapshotImpl layer = append(database, meta, archiveBlock, path); + layer.put(new byte[]{1}, new byte[]{2}); + + ChainbaseCheckpointMaterializer chainbase = new ChainbaseCheckpointMaterializer( + root.resolve("chainbase"), format, databases); + PublishingMaterializer pathState = new PublishingMaterializer(Authority.PATH_STATE); + StateArchiveCheckpointMaterializer archive = new StateArchiveCheckpointMaterializer( + root.resolve("archive"), format); + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( + new CommonCheckpointFile(root.resolve("wal")), chainbase, pathState, archive); + CommonCheckpointRuntime runtime = new CommonCheckpointRuntime( + new CommonCheckpointRuntimeOwner(coordinator), databases, root.resolve("archive"), + format, Engine.LEVELDB, + (blockNumber, blockHash) -> new TestLatest(code, blockNumber, blockHash), + target -> { + throw new IOException("injected memory rebase prepare failure"); + }); + + runtime.recoverBeforeServing(); + IOException failure = assertThrows(IOException.class, () -> runtime.checkpointAndRebase(1)); + assertEquals("injected memory rebase prepare failure", failure.getMessage()); + assertSame(layer, database.getHead()); + assertSame(layer.getRoot(), layer.getPrevious()); + assertEquals(CommonCheckpointRuntimeOwner.State.FAILED, runtime.getState()); + assertFalse(java.nio.file.Files.exists(root.resolve("wal").resolve( + CommonCheckpointFile.FILE_NAME))); + } + @Test public void runtimeComposesStartupCheckpointRebaseAndPointQuery() throws Exception { java.nio.file.Path root = temporaryFolder.newFolder("composed-runtime").toPath(); @@ -325,7 +364,8 @@ public void runtimeComposesStartupCheckpointRebaseAndPointQuery() throws Excepti CommonCheckpointRuntime runtime = new CommonCheckpointRuntime( new CommonCheckpointRuntimeOwner(coordinator), databases, root.resolve("archive"), format, Engine.LEVELDB, - (blockNumber, blockHash) -> new TestLatest(code, blockNumber, blockHash)); + (blockNumber, blockHash) -> new TestLatest(code, blockNumber, blockHash), + target -> () -> { }); assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.NO_CHECKPOINT, runtime.recoverBeforeServing()); diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java index 9eb21874365..97d3d215cdb 100644 --- a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java @@ -10,18 +10,27 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; public class CommonCheckpointRuntimeAttachmentTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + public void everyRuntimeConstructorRequiresMemoryRebaser() { + assertTrue(Arrays.stream(CommonCheckpointRuntime.class.getConstructors()) + .allMatch(constructor -> Arrays.asList(constructor.getParameterTypes()) + .contains(CommonCheckpointMemoryRebaser.class))); + } + @Test public void disabledAttachmentDoesNotConstructRuntimeOrCreateDirectory() throws Exception { Path root = temporaryFolder.getRoot().toPath().resolve("disabled"); @@ -90,10 +99,10 @@ private static CommonCheckpointRuntime runtime(Path root, Chainbase database) { new CommonCheckpointFile(root.resolve("wal")), materializer(Authority.CHAINBASE), materializer(Authority.PATH_STATE), materializer(Authority.STATE_ARCHIVE)); return new CommonCheckpointRuntime(new CommonCheckpointRuntimeOwner(coordinator), - Collections.singletonList(database), root.resolve("archive"), hash(1), + Collections.singletonList(database), root.resolve("archive"), hash(1), Engine.LEVELDB, (blockNumber, blockHash) -> { throw new IOException("latest state is intentionally unavailable"); - }); + }, target -> () -> { }); } private static CommonCheckpointMaterializer materializer(Authority authority) { diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index fe7cee96f56..370dfad8e95 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -731,7 +731,8 @@ public void physicalSnapshotDeltaPreparesWithoutWritesAndReusesPlanForPublicatio assertArrayEquals(PathStateCommitmentCodec.storeLeafKey( scope.require("code").getStoreId(), key), delta.getStores().get(0).getFlatMutations().get(0).getKey()); - assertArrayEquals(delta.getStateRoot(), delta.getTrieSnapshot().getStateRoot()); + assertTrue(Arrays.stream(PathStateSnapshotDelta.class.getDeclaredFields()) + .noneMatch(field -> field.getType() == PathStateRoot.Snapshot.class)); PathStateRootMetadata committed = stores.applyAndPublish(prepared, PathStateLayerLimits.defaults()); @@ -848,6 +849,100 @@ Engine.ROCKSDB, new PathStateLayerLimits(4, 1L << 20))) { } } + @Test + public void commonCheckpointRebasePreservesSuffixAndCutsSnapshotParents() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-common-memory-rebase").toPath(); + preparePublishedPhysicalTarget(root, scope); + byte[] formatIdentity = bytes(110); + + try (PathStatePhysicalOverlayHead head = PathStatePhysicalOverlayHead.open(root, + Engine.ROCKSDB, new PathStateLayerLimits(8, 1L << 20))) { + PathStateRootMetadata initial = head.getHead(); + CommonCheckpointBaseline baseline = commonBaseline(formatIdentity, initial); + head.admitFreshCommonBaseline(baseline); + PathStateCheckpointMaterializer materializer = head.checkpointMaterializer(formatIdentity, + baseline); + List deltas = new ArrayList<>(); + for (int number = 1; number <= 3; number++) { + byte[] blockHash = bytes(110 + number); + byte[] parentHash = number == 1 ? initial.getBlockHash() : bytes(109 + number); + PathStateBlockTransition transition = new PathStateBlockTransition(number, blockHash, + parentHash, number * 3L, P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{(byte) number}))); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(number, blockHash, parentHash, + number * 3L); + deltas.add(head.prepareSnapshotDelta(meta, transition)); + head.advance(transition); + } + PathStateRootMetadata liveHead = head.getHead(); + assertEquals(4, head.retentionCensus().getMaxSnapshotDepth()); + + CommonCheckpointPayload payload = commonPayload(formatIdentity, + deltas.subList(0, 2)); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + materializer.materialize(payload, target); + materializer.publish(target); + head.prepareCommonCheckpointRebase(target).apply(); + + PathStatePhysicalOverlayHead.RetentionCensus census = head.retentionCensus(); + assertEquals(3, census.getHeadBlockNumber()); + assertEquals(1, census.getSuffixBlocks()); + assertEquals(28, census.getTrieCount()); + assertEquals(2, census.getMaxSnapshotDepth()); + assertArrayEquals(liveHead.encode(), head.getHead().encode()); + + PathStateRootMetadata rewound = head.rewindTo(2, bytes(112)); + assertArrayEquals(target.getStateRoot(), rewound.getStateRoot()); + PathStateBlockTransition sibling = new PathStateBlockTransition(3, bytes(119), bytes(112), + 10, P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{2}, new byte[]{9}))); + head.prepareSnapshotDelta(BlockSnapshotMeta.forBlock(3, bytes(119), bytes(112), 10), + sibling); + assertEquals(3, head.advance(sibling).getBlockNumber()); + } + } + + @Test + public void repeatedCommonCheckpointRebaseKeepsParentDepthConstant() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-common-memory-rebase-long").toPath(); + preparePublishedPhysicalTarget(root, scope); + byte[] formatIdentity = bytes(120); + + try (PathStatePhysicalOverlayHead head = PathStatePhysicalOverlayHead.open(root, + Engine.ROCKSDB, new PathStateLayerLimits(8, 1L << 20))) { + PathStateRootMetadata initial = head.getHead(); + CommonCheckpointBaseline baseline = commonBaseline(formatIdentity, initial); + head.admitFreshCommonBaseline(baseline); + PathStateCheckpointMaterializer materializer = head.checkpointMaterializer(formatIdentity, + baseline); + byte[] parentHash = initial.getBlockHash(); + for (int number = 1; number <= 100; number++) { + byte[] blockHash = bytes(120 + number); + PathStateBlockTransition transition = new PathStateBlockTransition(number, blockHash, + parentHash, number * 3L, P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{(byte) number}))); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(number, blockHash, parentHash, + number * 3L); + PathStateSnapshotDelta delta = head.prepareSnapshotDelta(meta, transition); + head.advance(transition); + CommonCheckpointPayload payload = commonPayload(formatIdentity, + Collections.singletonList(delta)); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + materializer.materialize(payload, target); + materializer.publish(target); + head.prepareCommonCheckpointRebase(target).apply(); + PathStatePhysicalOverlayHead.RetentionCensus census = head.retentionCensus(); + assertEquals(0, census.getSuffixBlocks()); + assertEquals(1, census.getMaxSnapshotDepth()); + parentHash = blockHash; + } + assertEquals(100, head.getHead().getBlockNumber()); + assertEquals(100, head.retentionCensus().getHeadBlockNumber()); + } + } + @Test public void asyncPrepareQueuesTransitionAndCompletesOffCallerThread() throws Exception { PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); @@ -1662,6 +1757,25 @@ private byte[] preparePublishedPhysicalTarget(Path directory, return expectedRoot; } + private static CommonCheckpointBaseline commonBaseline(byte[] formatIdentity, + PathStateRootMetadata head) { + return new CommonCheckpointBaseline(formatIdentity, + BlockSnapshotMeta.forBlock(head.getBlockNumber(), head.getBlockHash(), + head.getParentHash(), head.getTimestamp()), head.getStateRoot()); + } + + private static CommonCheckpointPayload commonPayload(byte[] formatIdentity, + List deltas) { + PathStateFlushTarget target = PathStateFlushTarget.coalesce(deltas); + List archive = new ArrayList<>(); + for (PathStateSnapshotDelta delta : deltas) { + archive.add(new BlockReverseDiff(delta.getMeta(), Collections.emptyList(), + delta.getMutationViewDigest())); + } + return CommonCheckpointPayload.create(formatIdentity, target, archive, + Collections.emptyList()); + } + private void assertPublicationRejected(Path directory, PathStateParticipantScope scope) throws Exception { try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(directory, scope, From ddb0d6ef33858c127c95600f86ab4c1118b17ef3 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sun, 6 Sep 2026 11:38:41 +0800 Subject: [PATCH 120/161] feat(chainbase): add checkpoint phase timings Record structured timing for payload capture, durable redo barriers, and in-memory rebase while preserving checkpoint ordering and persistent bytes. Add deterministic apply and recovery coverage, including idempotent recovery and diagnostic sink isolation. --- .../core/CommonCheckpointRedoCoordinator.java | 241 ++++++++++++++++-- .../db2/core/CommonCheckpointRuntime.java | 117 ++++++++- .../ChainbaseCheckpointMaterializerTest.java | 17 +- .../CommonCheckpointRedoCoordinatorTest.java | 87 +++++++ 4 files changed, 436 insertions(+), 26 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java index e640ee59a57..173ad47275a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java @@ -4,18 +4,24 @@ import java.util.EnumMap; import java.util.Map; import java.util.Objects; +import java.util.function.LongSupplier; import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; import org.tron.core.db2.core.CommonCheckpointMaterializer.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Two-barrier, idempotent redo coordinator for one durable common checkpoint. */ public final class CommonCheckpointRedoCoordinator { + private static final Logger logger = LoggerFactory.getLogger("DB"); private static final Authority[] ORDER = { Authority.CHAINBASE, Authority.PATH_STATE, Authority.STATE_ARCHIVE}; private final CommonCheckpointFile checkpointFile; private final Map materializers; private final FaultHook faultHook; + private final LongSupplier nanoTime; + private final TimingSink timingSink; public CommonCheckpointRedoCoordinator(CommonCheckpointFile checkpointFile, CommonCheckpointMaterializer chainbase, CommonCheckpointMaterializer pathState, @@ -26,36 +32,66 @@ public CommonCheckpointRedoCoordinator(CommonCheckpointFile checkpointFile, CommonCheckpointRedoCoordinator(CommonCheckpointFile checkpointFile, CommonCheckpointMaterializer chainbase, CommonCheckpointMaterializer pathState, CommonCheckpointMaterializer stateArchive, FaultHook faultHook) { + this(checkpointFile, chainbase, pathState, stateArchive, faultHook, System::nanoTime, + CommonCheckpointRedoCoordinator::logTiming); + } + + CommonCheckpointRedoCoordinator(CommonCheckpointFile checkpointFile, + CommonCheckpointMaterializer chainbase, CommonCheckpointMaterializer pathState, + CommonCheckpointMaterializer stateArchive, FaultHook faultHook, LongSupplier nanoTime, + TimingSink timingSink) { this.checkpointFile = Objects.requireNonNull(checkpointFile, "checkpointFile"); this.materializers = new EnumMap<>(Authority.class); admit(Authority.CHAINBASE, chainbase); admit(Authority.PATH_STATE, pathState); admit(Authority.STATE_ARCHIVE, stateArchive); this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + this.nanoTime = Objects.requireNonNull(nanoTime, "nanoTime"); + this.timingSink = Objects.requireNonNull(timingSink, "timingSink"); } /** Durably publishes the redo payload before applying it to any authority. */ public synchronized RecoveryAction apply(CommonCheckpointPayload payload) throws IOException { - checkpointFile.publish(Objects.requireNonNull(payload, "payload")); - return redo(checkpointFile.loadRequired()); + CommonCheckpointPayload admitted = Objects.requireNonNull(payload, "payload"); + Timing timing = new Timing("apply", CommonCheckpointTarget.from(admitted), + admitted.getBlocks().size()); + long totalStart = nanoTime.getAsLong(); + timing.walPublishUs = timed(() -> checkpointFile.publish(admitted)); + Holder loaded = new Holder<>(); + timing.walLoadUs = timed(() -> loaded.value = checkpointFile.loadRequired()); + RecoveryAction action = redo(loaded.value, timing); + timing.totalUs = elapsedUs(totalStart); + emitTiming(timing); + return action; } /** Resumes the only durable checkpoint, or performs no work when none exists. */ public synchronized RecoveryAction recover() throws IOException { - CommonCheckpointPayload payload = checkpointFile.loadIfPresent(); - return payload == null ? RecoveryAction.NO_CHECKPOINT : redo(payload); + long totalStart = nanoTime.getAsLong(); + Holder loaded = new Holder<>(); + long loadUs = timed(() -> loaded.value = checkpointFile.loadIfPresent()); + if (loaded.value == null) { + return RecoveryAction.NO_CHECKPOINT; + } + Timing timing = new Timing("recover", CommonCheckpointTarget.from(loaded.value), + loaded.value.getBlocks().size()); + timing.walLoadUs = loadUs; + RecoveryAction action = redo(loaded.value, timing); + timing.totalUs = elapsedUs(totalStart); + emitTiming(timing); + return action; } - private RecoveryAction redo(CommonCheckpointPayload payload) throws IOException { + private RecoveryAction redo(CommonCheckpointPayload payload, Timing timing) throws IOException { CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); - try (CheckpointScope ignored = new CheckpointScope(target)) { - return redo(payload, target); + try (CheckpointScope ignored = new CheckpointScope(target, timing)) { + return redo(payload, target, timing); } } - private RecoveryAction redo(CommonCheckpointPayload payload, CommonCheckpointTarget target) - throws IOException { - Map initial = inspectAll(target); + private RecoveryAction redo(CommonCheckpointPayload payload, CommonCheckpointTarget target, + Timing timing) throws IOException { + Map initial = inspectAll(target, timing); if (initial.containsValue(Status.PUBLISHED) && initial.containsValue(Status.NEEDS_MATERIALIZATION)) { throw new IOException("common checkpoint has published authority before materialization " @@ -65,32 +101,36 @@ private RecoveryAction redo(CommonCheckpointPayload payload, CommonCheckpointTar for (Authority authority : ORDER) { CommonCheckpointMaterializer materializer = materializers.get(authority); if (initial.get(authority) == Status.NEEDS_MATERIALIZATION) { - materializer.materialize(payload, target); - requireStatus(authority, Status.MATERIALIZED, materializer.inspect(target), + timing.materializeUs[authority.ordinal()] += timed( + () -> materializer.materialize(payload, target)); + timing.materializeCount[authority.ordinal()]++; + requireStatus(authority, Status.MATERIALIZED, inspect(authority, target, timing), "materialization"); faultHook.after(materializeStage(authority)); } } - Map materialized = inspectAll(target); + Map materialized = inspectAll(target, timing); if (materialized.containsValue(Status.NEEDS_MATERIALIZATION)) { throw new IOException("common checkpoint materialization barrier is incomplete"); } for (Authority authority : ORDER) { CommonCheckpointMaterializer materializer = materializers.get(authority); if (materialized.get(authority) == Status.MATERIALIZED) { - materializer.publish(target); - requireStatus(authority, Status.PUBLISHED, materializer.inspect(target), "publication"); + timing.publishUs[authority.ordinal()] += timed(() -> materializer.publish(target)); + timing.publishCount[authority.ordinal()]++; + requireStatus(authority, Status.PUBLISHED, inspect(authority, target, timing), + "publication"); faultHook.after(publishStage(authority)); } } - Map published = inspectAll(target); + Map published = inspectAll(target, timing); for (Authority authority : ORDER) { requireStatus(authority, Status.PUBLISHED, published.get(authority), "retirement"); } faultHook.after(Stage.BEFORE_CHECKPOINT_RETIRE); - checkpointFile.retire(); + timing.walRetireUs = timed(checkpointFile::retire); faultHook.after(Stage.AFTER_CHECKPOINT_RETIRE); return RecoveryAction.COMPLETED_REDO; } @@ -98,13 +138,16 @@ private RecoveryAction redo(CommonCheckpointPayload payload, CommonCheckpointTar private final class CheckpointScope implements AutoCloseable { private final CommonCheckpointTarget target; + private final Timing timing; private int opened; - private CheckpointScope(CommonCheckpointTarget target) throws IOException { + private CheckpointScope(CommonCheckpointTarget target, Timing timing) throws IOException { this.target = target; + this.timing = timing; try { for (Authority authority : ORDER) { - materializers.get(authority).beginCheckpoint(target); + timing.beginUs[authority.ordinal()] += timed( + () -> materializers.get(authority).beginCheckpoint(target)); opened++; } } catch (IOException | RuntimeException failure) { @@ -121,9 +164,10 @@ private CheckpointScope(CommonCheckpointTarget target) throws IOException { public void close() throws IOException { IOException failure = null; while (opened > 0) { - CommonCheckpointMaterializer materializer = materializers.get(ORDER[--opened]); + Authority authority = ORDER[--opened]; + CommonCheckpointMaterializer materializer = materializers.get(authority); try { - materializer.endCheckpoint(target); + timing.endUs[authority.ordinal()] += timed(() -> materializer.endCheckpoint(target)); } catch (IOException closing) { if (failure == null) { failure = closing; @@ -138,10 +182,11 @@ public void close() throws IOException { } } - private Map inspectAll(CommonCheckpointTarget target) throws IOException { + private Map inspectAll(CommonCheckpointTarget target, Timing timing) + throws IOException { Map statuses = new EnumMap<>(Authority.class); for (Authority authority : ORDER) { - Status status = materializers.get(authority).inspect(target); + Status status = inspect(authority, target, timing); if (status == null) { throw new IOException("common checkpoint " + authority + " returned null status"); } @@ -150,6 +195,57 @@ private Map inspectAll(CommonCheckpointTarget target) throws return statuses; } + private Status inspect(Authority authority, CommonCheckpointTarget target, Timing timing) + throws IOException { + Holder status = new Holder<>(); + timing.inspectUs[authority.ordinal()] += timed( + () -> status.value = materializers.get(authority).inspect(target)); + timing.inspectCount[authority.ordinal()]++; + return status.value; + } + + private long timed(IoAction action) throws IOException { + long start = nanoTime.getAsLong(); + action.run(); + return elapsedUs(start); + } + + private long elapsedUs(long start) { + return Math.max(0L, (nanoTime.getAsLong() - start) / 1_000L); + } + + private void emitTiming(Timing timing) { + try { + timingSink.accept(timing); + } catch (RuntimeException ignored) { + // Diagnostics must not turn an already durable checkpoint into a caller-visible failure. + } + } + + private static void logTiming(Timing timing) { + logger.info("Common checkpoint redo stages: mode={}, head={}, blocks={}, walPublishUs={}, " + + "walLoadUs={}, chainbaseBeginUs={}, pathStateBeginUs={}, archiveBeginUs={}, " + + "chainbaseInspectUs={}/{}, pathStateInspectUs={}/{}, archiveInspectUs={}/{}, " + + "chainbaseMaterializeUs={}/{}, pathStateMaterializeUs={}/{}, " + + "archiveMaterializeUs={}/{}, chainbasePublishUs={}/{}, " + + "pathStatePublishUs={}/{}, archivePublishUs={}/{}, walRetireUs={}, " + + "chainbaseEndUs={}, pathStateEndUs={}, archiveEndUs={}, totalUs={}", + timing.mode, timing.head, timing.blocks, timing.walPublishUs, timing.walLoadUs, + timing.begin(Authority.CHAINBASE), timing.begin(Authority.PATH_STATE), + timing.begin(Authority.STATE_ARCHIVE), timing.inspect(Authority.CHAINBASE), + timing.inspectCount(Authority.CHAINBASE), timing.inspect(Authority.PATH_STATE), + timing.inspectCount(Authority.PATH_STATE), timing.inspect(Authority.STATE_ARCHIVE), + timing.inspectCount(Authority.STATE_ARCHIVE), timing.materialize(Authority.CHAINBASE), + timing.materializeCount(Authority.CHAINBASE), timing.materialize(Authority.PATH_STATE), + timing.materializeCount(Authority.PATH_STATE), timing.materialize(Authority.STATE_ARCHIVE), + timing.materializeCount(Authority.STATE_ARCHIVE), timing.publish(Authority.CHAINBASE), + timing.publishCount(Authority.CHAINBASE), timing.publish(Authority.PATH_STATE), + timing.publishCount(Authority.PATH_STATE), timing.publish(Authority.STATE_ARCHIVE), + timing.publishCount(Authority.STATE_ARCHIVE), timing.walRetireUs, + timing.end(Authority.CHAINBASE), timing.end(Authority.PATH_STATE), + timing.end(Authority.STATE_ARCHIVE), timing.totalUs); + } + private void admit(Authority expected, CommonCheckpointMaterializer materializer) { CommonCheckpointMaterializer admitted = Objects.requireNonNull(materializer, expected + " materializer"); @@ -167,6 +263,105 @@ private static void requireStatus(Authority authority, Status expected, Status a } } + static final class Timing { + + private final String mode; + private final long head; + private final int blocks; + private final long[] beginUs = new long[Authority.values().length]; + private final long[] inspectUs = new long[Authority.values().length]; + private final int[] inspectCount = new int[Authority.values().length]; + private final long[] materializeUs = new long[Authority.values().length]; + private final int[] materializeCount = new int[Authority.values().length]; + private final long[] publishUs = new long[Authority.values().length]; + private final int[] publishCount = new int[Authority.values().length]; + private final long[] endUs = new long[Authority.values().length]; + private long walPublishUs; + private long walLoadUs; + private long walRetireUs; + private long totalUs; + + private Timing(String mode, CommonCheckpointTarget target, int blocks) { + this.mode = mode; + this.head = target.getLastBlock().getBlockNumber(); + this.blocks = blocks; + } + + String getMode() { + return mode; + } + + long getHead() { + return head; + } + + int getBlocks() { + return blocks; + } + + long getWalPublishUs() { + return walPublishUs; + } + + long getWalLoadUs() { + return walLoadUs; + } + + long getWalRetireUs() { + return walRetireUs; + } + + long getTotalUs() { + return totalUs; + } + + long begin(Authority authority) { + return beginUs[authority.ordinal()]; + } + + long inspect(Authority authority) { + return inspectUs[authority.ordinal()]; + } + + int inspectCount(Authority authority) { + return inspectCount[authority.ordinal()]; + } + + long materialize(Authority authority) { + return materializeUs[authority.ordinal()]; + } + + int materializeCount(Authority authority) { + return materializeCount[authority.ordinal()]; + } + + long publish(Authority authority) { + return publishUs[authority.ordinal()]; + } + + int publishCount(Authority authority) { + return publishCount[authority.ordinal()]; + } + + long end(Authority authority) { + return endUs[authority.ordinal()]; + } + } + + @FunctionalInterface + interface TimingSink { + void accept(Timing timing); + } + + @FunctionalInterface + private interface IoAction { + void run() throws IOException; + } + + private static final class Holder { + private T value; + } + private static Stage materializeStage(Authority authority) { switch (authority) { case CHAINBASE: diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java index b6f06ce0413..8322c70b22e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java @@ -6,13 +6,18 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.function.LongSupplier; import org.tron.core.db2.archive.StateArchiveCheckpointMaterializer; import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Isolated composition boundary for the next-format common-checkpoint runtime. */ public final class CommonCheckpointRuntime implements AutoCloseable { + private static final Logger logger = LoggerFactory.getLogger("DB"); + private final CommonCheckpointRuntimeOwner owner; private final List databases; private final Path archiveDirectory; @@ -20,6 +25,8 @@ public final class CommonCheckpointRuntime implements AutoCloseable { private final Engine engine; private final StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory; private final CommonCheckpointMemoryRebaser memoryRebaser; + private final LongSupplier nanoTime; + private final TimingSink timingSink; private final CommonCheckpointPayloadFactory payloadFactory = new CommonCheckpointPayloadFactory(); private final CommonCheckpointSnapshotRebaser rebaser = new CommonCheckpointSnapshotRebaser(); private CommonCheckpointTarget publishedTarget; @@ -28,6 +35,15 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, + Path archiveDirectory, byte[] formatIdentity, Engine engine, + StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory, + CommonCheckpointMemoryRebaser memoryRebaser, LongSupplier nanoTime, + TimingSink timingSink) { this.owner = Objects.requireNonNull(owner, "owner"); this.databases = new ArrayList<>(Objects.requireNonNull(databases, "databases")); if (this.databases.isEmpty() || this.databases.contains(null)) { @@ -38,6 +54,8 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List { + long chainbasePrepareStart = nanoTime.getAsLong(); CommonCheckpointSnapshotRebaser.Plan chainbasePlan = rebaser.prepare(databases, target, flushCount); + timing.chainbaseRebasePrepareUs = elapsedUs(chainbasePrepareStart); + long pathStatePrepareStart = nanoTime.getAsLong(); CommonCheckpointMemoryRebaser.RebasePlan pathStatePlan = memoryRebaser.prepare(target); + timing.pathStateRebasePrepareUs = elapsedUs(pathStatePrepareStart); + long chainbaseApplyStart = nanoTime.getAsLong(); chainbasePlan.apply(); + timing.chainbaseRebaseApplyUs = elapsedUs(chainbaseApplyStart); + long pathStateApplyStart = nanoTime.getAsLong(); pathStatePlan.apply(); + timing.pathStateRebaseApplyUs = elapsedUs(pathStateApplyStart); }); + timing.ownerApplyUs = elapsedUs(ownerApplyStart); publishedTarget = target; + timing.totalUs = elapsedUs(totalStart); + emitTiming(timing); return target; } @@ -97,4 +131,83 @@ private static byte[] requireDigest(byte[] value) { } return admitted; } + + private long elapsedUs(long start) { + return Math.max(0L, (nanoTime.getAsLong() - start) / 1_000L); + } + + private void emitTiming(Timing timing) { + try { + timingSink.accept(timing); + } catch (RuntimeException ignored) { + // Diagnostics must not turn an already completed checkpoint into a caller-visible failure. + } + } + + private static void logTiming(Timing timing) { + logger.info("Common checkpoint runtime stages: head={}, blocks={}, payloadCaptureUs={}, " + + "ownerApplyUs={}, chainbaseRebasePrepareUs={}, pathStateRebasePrepareUs={}, " + + "chainbaseRebaseApplyUs={}, pathStateRebaseApplyUs={}, totalUs={}", + timing.head, timing.blocks, timing.payloadCaptureUs, timing.ownerApplyUs, + timing.chainbaseRebasePrepareUs, timing.pathStateRebasePrepareUs, + timing.chainbaseRebaseApplyUs, timing.pathStateRebaseApplyUs, timing.totalUs); + } + + static final class Timing { + + private long head; + private final int blocks; + private long payloadCaptureUs; + private long ownerApplyUs; + private long chainbaseRebasePrepareUs; + private long pathStateRebasePrepareUs; + private long chainbaseRebaseApplyUs; + private long pathStateRebaseApplyUs; + private long totalUs; + + private Timing(int blocks) { + this.blocks = blocks; + } + + long getHead() { + return head; + } + + int getBlocks() { + return blocks; + } + + long getPayloadCaptureUs() { + return payloadCaptureUs; + } + + long getOwnerApplyUs() { + return ownerApplyUs; + } + + long getChainbaseRebasePrepareUs() { + return chainbaseRebasePrepareUs; + } + + long getPathStateRebasePrepareUs() { + return pathStateRebasePrepareUs; + } + + long getChainbaseRebaseApplyUs() { + return chainbaseRebaseApplyUs; + } + + long getPathStateRebaseApplyUs() { + return pathStateRebaseApplyUs; + } + + long getTotalUs() { + return totalUs; + } + } + + @FunctionalInterface + interface TimingSink { + void accept(Timing timing); + } } diff --git a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java index f7396dcfdad..e1d0adf119c 100644 --- a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -17,6 +18,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; @@ -361,16 +363,29 @@ public void runtimeComposesStartupCheckpointRebaseAndPointQuery() throws Excepti root.resolve("archive"), format); CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( new CommonCheckpointFile(root.resolve("wal")), chainbase, pathState, archive); + AtomicLong clock = new AtomicLong(); + List timings = new ArrayList<>(); CommonCheckpointRuntime runtime = new CommonCheckpointRuntime( new CommonCheckpointRuntimeOwner(coordinator), databases, root.resolve("archive"), format, Engine.LEVELDB, (blockNumber, blockHash) -> new TestLatest(code, blockNumber, blockHash), - target -> () -> { }); + target -> () -> { }, () -> clock.addAndGet(1_000L), timings::add); assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.NO_CHECKPOINT, runtime.recoverBeforeServing()); CommonCheckpointTarget target = runtime.checkpointAndRebase(1); assertEquals(meta, target.getLastBlock()); + assertEquals(1, timings.size()); + CommonCheckpointRuntime.Timing timing = timings.get(0); + assertEquals(1, timing.getHead()); + assertEquals(1, timing.getBlocks()); + assertEquals(1, timing.getPayloadCaptureUs()); + assertTrue(timing.getOwnerApplyUs() > 0); + assertEquals(1, timing.getChainbaseRebasePrepareUs()); + assertEquals(1, timing.getPathStateRebasePrepareUs()); + assertEquals(1, timing.getChainbaseRebaseApplyUs()); + assertEquals(1, timing.getPathStateRebaseApplyUs()); + assertTrue(timing.getTotalUs() >= timing.getOwnerApplyUs()); assertSame(database.getHead().getRoot(), database.getHead()); assertEquals(1, code.syncedFlushes); try (StateArchiveCheckpointReadSnapshot snapshot = runtime.pinPoint(0)) { diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java index b826a1e8877..2071852368c 100644 --- a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java @@ -19,6 +19,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -48,6 +49,92 @@ public void appliesTwoBarriersThenRetiresAndSecondRecoveryDoesNothing() throws E assertEquals(actionCount, fixture.actions.size()); } + @Test + public void recordsDeterministicRedoPhaseTimingsWithoutChangingBarrierCalls() + throws Exception { + Fixture fixture = fixture("timings", null); + AtomicLong clock = new AtomicLong(); + List timings = new ArrayList<>(); + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( + fixture.file, fixture.materializers.get(0), fixture.materializers.get(1), + fixture.materializers.get(2), stage -> { }, () -> clock.addAndGet(1_000L), + timings::add); + + assertEquals(RecoveryAction.COMPLETED_REDO, coordinator.apply(fixture.payload)); + assertEquals(1, timings.size()); + CommonCheckpointRedoCoordinator.Timing timing = timings.get(0); + assertEquals("apply", timing.getMode()); + assertEquals(1, timing.getHead()); + assertEquals(1, timing.getBlocks()); + assertEquals(1, timing.getWalPublishUs()); + assertEquals(1, timing.getWalLoadUs()); + assertEquals(1, timing.getWalRetireUs()); + for (Authority authority : Authority.values()) { + assertEquals(1, timing.begin(authority)); + assertEquals(5, timing.inspect(authority)); + assertEquals(5, timing.inspectCount(authority)); + assertEquals(1, timing.materialize(authority)); + assertEquals(1, timing.materializeCount(authority)); + assertEquals(1, timing.publish(authority)); + assertEquals(1, timing.publishCount(authority)); + assertEquals(1, timing.end(authority)); + } + assertTrue(timing.getTotalUs() > 0); + } + + @Test + public void ignoresTimingSinkFailureAfterDurableCheckpointCompletes() throws Exception { + Fixture fixture = fixture("timing-sink-failure", null); + AtomicLong clock = new AtomicLong(); + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( + fixture.file, fixture.materializers.get(0), fixture.materializers.get(1), + fixture.materializers.get(2), stage -> { }, () -> clock.addAndGet(1_000L), + timing -> { + throw new IllegalStateException("injected timing sink failure"); + }); + + assertEquals(RecoveryAction.COMPLETED_REDO, coordinator.apply(fixture.payload)); + assertFalse(Files.exists(fixture.file.getCheckpointPath())); + for (FakeMaterializer materializer : fixture.materializers) { + assertEquals(Status.PUBLISHED, materializer.status); + } + } + + @Test + public void recordsRecoveryTimingsWithoutRepeatingPublishedAuthorityWork() throws Exception { + Fixture fixture = fixture("recovery-timings", + CommonCheckpointRedoCoordinator.Stage.BEFORE_CHECKPOINT_RETIRE); + assertThrows(IOException.class, () -> fixture.coordinator.apply(fixture.payload)); + assertTrue(Files.isRegularFile(fixture.file.getCheckpointPath())); + + AtomicLong clock = new AtomicLong(); + List timings = new ArrayList<>(); + CommonCheckpointRedoCoordinator recovered = new CommonCheckpointRedoCoordinator( + fixture.file, fixture.materializers.get(0), fixture.materializers.get(1), + fixture.materializers.get(2), stage -> { }, () -> clock.addAndGet(1_000L), + timings::add); + + assertEquals(RecoveryAction.COMPLETED_REDO, recovered.recover()); + assertEquals(1, timings.size()); + CommonCheckpointRedoCoordinator.Timing timing = timings.get(0); + assertEquals("recover", timing.getMode()); + assertEquals(0, timing.getWalPublishUs()); + assertEquals(1, timing.getWalLoadUs()); + assertEquals(1, timing.getWalRetireUs()); + for (Authority authority : Authority.values()) { + assertEquals(1, timing.begin(authority)); + assertEquals(3, timing.inspect(authority)); + assertEquals(3, timing.inspectCount(authority)); + assertEquals(0, timing.materialize(authority)); + assertEquals(0, timing.materializeCount(authority)); + assertEquals(0, timing.publish(authority)); + assertEquals(0, timing.publishCount(authority)); + assertEquals(1, timing.end(authority)); + } + assertTrue(timing.getTotalUs() > 0); + assertFalse(Files.exists(fixture.file.getCheckpointPath())); + } + @Test public void resumesEveryCoordinatorBoundaryAgainstTheSameTarget() throws Exception { for (CommonCheckpointRedoCoordinator.Stage stage From c8e4a14129b3a6140f482c2e041244022e7539be Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Sun, 6 Sep 2026 19:09:32 +0800 Subject: [PATCH 121/161] perf(chainbase): reuse archive checkpoint writer retain the State Archive serving-index writer across common checkpoint targets and release it through explicit runtime ownership. preserve synchronous index writes and checkpoint barriers while covering normal, startup, capture, apply, and close-failure lifecycle paths. --- .../StateArchiveCheckpointMaterializer.java | 40 +++++++++- .../StateArchiveCheckpointServingIndex.java | 4 + .../archive/StateArchiveIndexDatabase.java | 10 +++ .../core/CommonCheckpointMaterializer.java | 7 +- .../core/CommonCheckpointRedoCoordinator.java | 39 +++++++++- .../db2/core/CommonCheckpointRuntime.java | 75 +++++++++++-------- .../core/CommonCheckpointRuntimeOwner.java | 35 ++++++++- ...tateArchiveCheckpointMaterializerTest.java | 51 +++++++++++++ .../CommonCheckpointRedoCoordinatorTest.java | 29 +++++++ ...CommonCheckpointRuntimeAttachmentTest.java | 35 ++++++++- 10 files changed, 283 insertions(+), 42 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java index dd8b5b4a4ec..1cf528d4c91 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java @@ -56,6 +56,8 @@ public final class StateArchiveCheckpointMaterializer implements CommonCheckpoin private final CommonCheckpointBaseline baseline; private final Engine engine; private StateArchiveCheckpointServingIndex.Session checkpointServingIndex; + private CommonCheckpointTarget activeCheckpoint; + private boolean closed; public StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity) { this(directory, formatIdentity, null, StateArchiveCheckpointServingIndex.configuredEngine(), @@ -100,16 +102,37 @@ public Authority authority() { @Override public synchronized void beginCheckpoint(CommonCheckpointTarget target) throws IOException { - requireTarget(target); - if (checkpointServingIndex != null) { + CommonCheckpointTarget admitted = requireTarget(target); + requireOpen(); + if (activeCheckpoint != null) { throw new IOException("State Archive checkpoint serving session is already open"); } - checkpointServingIndex = StateArchiveCheckpointServingIndex.session(directory, engine); + if (checkpointServingIndex == null) { + checkpointServingIndex = StateArchiveCheckpointServingIndex.session(directory, engine); + } + activeCheckpoint = admitted; } @Override public synchronized void endCheckpoint(CommonCheckpointTarget target) throws IOException { - requireTarget(target); + CommonCheckpointTarget admitted = requireTarget(target); + if (activeCheckpoint == null) { + return; + } + if (!activeCheckpoint.equals(admitted)) { + throw new IOException("State Archive checkpoint serving session target differs"); + } + activeCheckpoint = null; + } + + /** Releases the serving-index writer retained across checkpoint targets. */ + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + activeCheckpoint = null; if (checkpointServingIndex != null) { try { checkpointServingIndex.close(); @@ -121,6 +144,7 @@ public synchronized void endCheckpoint(CommonCheckpointTarget target) throws IOE @Override public synchronized Status inspect(CommonCheckpointTarget target) throws IOException { + requireOpen(); CommonCheckpointTarget admitted = requireTarget(target); byte[] expected = encodeTarget(admitted); Path readable = directory.resolve(READABLE_FILE); @@ -184,6 +208,7 @@ public static Engine configuredEngine() { @Override public synchronized void materialize(CommonCheckpointPayload payload, CommonCheckpointTarget target) throws IOException { + requireOpen(); CommonCheckpointPayload admittedPayload = Objects.requireNonNull(payload, "payload"); CommonCheckpointTarget admittedTarget = requireTarget(target); if (!admittedTarget.equals(CommonCheckpointTarget.from(admittedPayload))) { @@ -218,6 +243,7 @@ public synchronized void materialize(CommonCheckpointPayload payload, @Override public synchronized void publish(CommonCheckpointTarget target) throws IOException { + requireOpen(); CommonCheckpointTarget admitted = requireTarget(target); Status status = inspect(admitted); if (status == Status.PUBLISHED) { @@ -257,6 +283,12 @@ private CommonCheckpointTarget requireTarget(CommonCheckpointTarget target) thro return admitted; } + private void requireOpen() throws IOException { + if (closed) { + throw new IOException("State Archive checkpoint materializer is closed"); + } + } + private void requireParent(TargetMarker current, CommonCheckpointTarget target) throws IOException { BlockSnapshotMeta first = target.getFirstBlock(); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java index da4c76e9245..83033f627e3 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java @@ -77,6 +77,10 @@ static Session session(Path archiveDirectory, Engine engine) { return new Session(archiveDirectory, engine); } + static int openReferenceCount(Path archiveDirectory, Engine engine) { + return StateArchiveIndexDatabase.openReferenceCount(databasePath(archiveDirectory), engine); + } + private static Status inspect(byte[] encoded, CommonCheckpointTarget target) throws IOException { if (encoded == null) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java index 8daa4a05bfc..201d2d510d2 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java @@ -58,6 +58,16 @@ static void checkpoint(Path source, Path target, Engine engine) throws IOExcepti checkpointLevel(from, to); } + static synchronized int openReferenceCount(Path directory, Engine engine) { + Path path = normalize(directory); + if (engine == Engine.LEVELDB) { + SharedLevelDatabase shared = LEVEL_DATABASES.get(path); + return shared == null ? 0 : shared.references; + } + SharedRocksDatabase shared = ROCKS_DATABASES.get(path); + return shared == null ? 0 : shared.references; + } + private static void checkpointLevel(Path source, Path target) throws IOException { SharedLevelDatabase shared = acquireLevel(source, false, configuredOptions()); boolean suspended = false; diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java index 67af11c84bb..0f24508fc74 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java @@ -3,7 +3,7 @@ import java.io.IOException; /** One idempotent authority participant in common-checkpoint redo and publication. */ -public interface CommonCheckpointMaterializer { +public interface CommonCheckpointMaterializer extends AutoCloseable { Authority authority(); @@ -15,6 +15,11 @@ default void beginCheckpoint(CommonCheckpointTarget target) throws IOException { default void endCheckpoint(CommonCheckpointTarget target) throws IOException { } + /** Releases resources owned across checkpoint targets; implementations must be idempotent. */ + @Override + default void close() throws IOException { + } + /** * Returns only an exact state for {@code target}. Implementations must throw when durable state * is corrupt, ambiguous, or belongs to a different target. diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java index 173ad47275a..45f81ade009 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java @@ -11,7 +11,7 @@ import org.slf4j.LoggerFactory; /** Two-barrier, idempotent redo coordinator for one durable common checkpoint. */ -public final class CommonCheckpointRedoCoordinator { +public final class CommonCheckpointRedoCoordinator implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger("DB"); private static final Authority[] ORDER = { @@ -22,6 +22,7 @@ public final class CommonCheckpointRedoCoordinator { private final FaultHook faultHook; private final LongSupplier nanoTime; private final TimingSink timingSink; + private boolean closed; public CommonCheckpointRedoCoordinator(CommonCheckpointFile checkpointFile, CommonCheckpointMaterializer chainbase, CommonCheckpointMaterializer pathState, @@ -52,6 +53,7 @@ public CommonCheckpointRedoCoordinator(CommonCheckpointFile checkpointFile, /** Durably publishes the redo payload before applying it to any authority. */ public synchronized RecoveryAction apply(CommonCheckpointPayload payload) throws IOException { + requireOpen(); CommonCheckpointPayload admitted = Objects.requireNonNull(payload, "payload"); Timing timing = new Timing("apply", CommonCheckpointTarget.from(admitted), admitted.getBlocks().size()); @@ -67,6 +69,7 @@ public synchronized RecoveryAction apply(CommonCheckpointPayload payload) throws /** Resumes the only durable checkpoint, or performs no work when none exists. */ public synchronized RecoveryAction recover() throws IOException { + requireOpen(); long totalStart = nanoTime.getAsLong(); Holder loaded = new Holder<>(); long loadUs = timed(() -> loaded.value = checkpointFile.loadIfPresent()); @@ -82,6 +85,34 @@ public synchronized RecoveryAction recover() throws IOException { return action; } + /** Closes runtime-owned authority resources in reverse publication order. */ + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + Throwable failure = null; + for (int index = ORDER.length - 1; index >= 0; index--) { + CommonCheckpointMaterializer materializer = materializers.get(ORDER[index]); + try { + materializer.close(); + } catch (IOException | RuntimeException closing) { + if (failure == null) { + failure = closing; + } else { + failure.addSuppressed(closing); + } + } + } + if (failure instanceof IOException) { + throw (IOException) failure; + } + if (failure != null) { + throw (RuntimeException) failure; + } + } + private RecoveryAction redo(CommonCheckpointPayload payload, Timing timing) throws IOException { CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); try (CheckpointScope ignored = new CheckpointScope(target, timing)) { @@ -222,6 +253,12 @@ private void emitTiming(Timing timing) { } } + private void requireOpen() throws IOException { + if (closed) { + throw new IOException("common checkpoint coordinator is closed"); + } + } + private static void logTiming(Timing timing) { logger.info("Common checkpoint redo stages: mode={}, head={}, blocks={}, walPublishUs={}, " + "walLoadUs={}, chainbaseBeginUs={}, pathStateBeginUs={}, archiveBeginUs={}, " diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java index 8322c70b22e..437eaddc350 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java @@ -61,10 +61,15 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List { - long chainbasePrepareStart = nanoTime.getAsLong(); - CommonCheckpointSnapshotRebaser.Plan chainbasePlan = - rebaser.prepare(databases, target, flushCount); - timing.chainbaseRebasePrepareUs = elapsedUs(chainbasePrepareStart); - long pathStatePrepareStart = nanoTime.getAsLong(); - CommonCheckpointMemoryRebaser.RebasePlan pathStatePlan = memoryRebaser.prepare(target); - timing.pathStateRebasePrepareUs = elapsedUs(pathStatePrepareStart); - long chainbaseApplyStart = nanoTime.getAsLong(); - chainbasePlan.apply(); - timing.chainbaseRebaseApplyUs = elapsedUs(chainbaseApplyStart); - long pathStateApplyStart = nanoTime.getAsLong(); - pathStatePlan.apply(); - timing.pathStateRebaseApplyUs = elapsedUs(pathStateApplyStart); - }); - timing.ownerApplyUs = elapsedUs(ownerApplyStart); - publishedTarget = target; - timing.totalUs = elapsedUs(totalStart); - emitTiming(timing); - return target; + try { + long totalStart = nanoTime.getAsLong(); + Timing timing = new Timing(flushCount); + long captureStart = nanoTime.getAsLong(); + CommonCheckpointPayload payload = payloadFactory.capture(formatIdentity, databases, + flushCount); + timing.payloadCaptureUs = elapsedUs(captureStart); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + timing.head = target.getLastBlock().getBlockNumber(); + long ownerApplyStart = nanoTime.getAsLong(); + owner.apply(payload, () -> { + long chainbasePrepareStart = nanoTime.getAsLong(); + CommonCheckpointSnapshotRebaser.Plan chainbasePlan = + rebaser.prepare(databases, target, flushCount); + timing.chainbaseRebasePrepareUs = elapsedUs(chainbasePrepareStart); + long pathStatePrepareStart = nanoTime.getAsLong(); + CommonCheckpointMemoryRebaser.RebasePlan pathStatePlan = memoryRebaser.prepare(target); + timing.pathStateRebasePrepareUs = elapsedUs(pathStatePrepareStart); + long chainbaseApplyStart = nanoTime.getAsLong(); + chainbasePlan.apply(); + timing.chainbaseRebaseApplyUs = elapsedUs(chainbaseApplyStart); + long pathStateApplyStart = nanoTime.getAsLong(); + pathStatePlan.apply(); + timing.pathStateRebaseApplyUs = elapsedUs(pathStateApplyStart); + }); + timing.ownerApplyUs = elapsedUs(ownerApplyStart); + publishedTarget = target; + timing.totalUs = elapsedUs(totalStart); + emitTiming(timing); + return target; + } catch (IOException | RuntimeException failure) { + owner.fail(failure); + throw failure; + } } /** Pins one point-only historical request under the same publication gate. */ diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java index b3f8349788a..649262823c2 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java @@ -1,6 +1,7 @@ package org.tron.core.db2.core; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Objects; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -28,6 +29,7 @@ public CommonCheckpointRedoCoordinator.RecoveryAction recoverBeforeServing() return action; } catch (IOException | RuntimeException failure) { state = State.FAILED; + closeAfterFailure(failure); throw failure; } } finally { @@ -56,6 +58,7 @@ CommonCheckpointRedoCoordinator.RecoveryAction apply(CommonCheckpointPayload pay return action; } catch (IOException | RuntimeException failure) { state = State.FAILED; + closeAfterFailure(failure); throw failure; } } finally { @@ -86,16 +89,46 @@ public State getState() { return state; } + /** Permanently fails this owner and releases authority resources after an outer runtime error. */ + void fail(Throwable failure) { + gate.writeLock().lock(); + try { + if (state != State.CLOSED) { + state = State.FAILED; + closeAfterFailure(Objects.requireNonNull(failure, "failure")); + } + } finally { + gate.writeLock().unlock(); + } + } + @Override public void close() { gate.writeLock().lock(); try { - state = State.CLOSED; + if (state == State.CLOSED) { + return; + } + try { + coordinator.close(); + } catch (IOException failure) { + throw new UncheckedIOException("Failed to close common checkpoint authorities", failure); + } finally { + state = State.CLOSED; + } } finally { gate.writeLock().unlock(); } } + private void closeAfterFailure(Throwable failure) { + try { + coordinator.close(); + } catch (IOException | RuntimeException closing) { + failure.addSuppressed(closing); + } + } + private void requireState(State expected, String message) throws IOException { if (state != expected) { throw new IOException(message + ": " + state); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java index 15bced40829..fdcd22e6fca 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java @@ -112,6 +112,57 @@ public void preservesEveryBlockBoundaryBeforePublishingReadableAcrossReopen() } } + @Test + public void reusesServingWriterAcrossTargetsAndClosesItWithMaterializer() throws Exception { + for (Engine engine : Engine.values()) { + Path root = temporaryFolder.newFolder("writer-lifecycle-" + engine).toPath(); + byte[] format = hash(89); + StateArchiveCheckpointMaterializer materializer = + new StateArchiveCheckpointMaterializer(root, format, null, engine); + + CommonCheckpointPayload first = payload(format, 1, 2, hash(0), hash(10), hash(12)); + CommonCheckpointTarget firstTarget = CommonCheckpointTarget.from(first); + materializer.beginCheckpoint(firstTarget); + materializer.materialize(first, firstTarget); + materializer.publish(firstTarget); + materializer.endCheckpoint(firstTarget); + assertEquals(1, StateArchiveCheckpointServingIndex.openReferenceCount(root, engine)); + + CommonCheckpointPayload second = payload(format, 3, 2, hash(2), hash(12), hash(14)); + CommonCheckpointTarget secondTarget = CommonCheckpointTarget.from(second); + materializer.beginCheckpoint(secondTarget); + materializer.materialize(second, secondTarget); + materializer.publish(secondTarget); + materializer.endCheckpoint(secondTarget); + assertEquals(1, StateArchiveCheckpointServingIndex.openReferenceCount(root, engine)); + + materializer.close(); + materializer.close(); + assertEquals(0, StateArchiveCheckpointServingIndex.openReferenceCount(root, engine)); + assertThrows(IOException.class, () -> materializer.inspect(secondTarget)); + } + } + + @Test + public void releasesRetainedWriterAfterCheckpointFailure() throws Exception { + for (Engine engine : Engine.values()) { + Path root = temporaryFolder.newFolder("writer-failure-" + engine).toPath(); + byte[] format = hash(88); + CommonCheckpointPayload payload = payload(format, 1, 2, hash(0), hash(10), hash(12)); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + StateArchiveCheckpointMaterializer materializer = new StateArchiveCheckpointMaterializer( + root, format, engine, + failAt(StateArchiveCheckpointMaterializer.Stage.AFTER_SERVING_INDEX_BATCH)); + + materializer.beginCheckpoint(target); + assertThrows(IOException.class, () -> materializer.materialize(payload, target)); + assertEquals(1, StateArchiveCheckpointServingIndex.openReferenceCount(root, engine)); + materializer.endCheckpoint(target); + materializer.close(); + assertEquals(0, StateArchiveCheckpointServingIndex.openReferenceCount(root, engine)); + } + } + @Test public void resumesEveryDurabilityBoundaryUsingOnlyCheckpointRedo() throws Exception { for (StateArchiveCheckpointMaterializer.Stage stage diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java index 2071852368c..b682bd94a76 100644 --- a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java @@ -199,6 +199,18 @@ public void closesEveryCheckpointScopeAfterSuccessAndFailure() throws Exception } } + @Test + public void closesEveryAuthorityOnceWhenOneCloseFails() throws Exception { + Fixture fixture = fixture("close-failure", null); + fixture.materializers.get(1).closeFailure = true; + + assertThrows(IOException.class, fixture.coordinator::close); + fixture.coordinator.close(); + for (FakeMaterializer materializer : fixture.materializers) { + assertEquals(1, materializer.closed); + } + } + @Test public void runtimeOwnerRequiresStartupRecoveryAndGatesReadsAroundApply() throws Exception { Fixture fixture = fixture("runtime-owner", null); @@ -212,6 +224,10 @@ public void runtimeOwnerRequiresStartupRecoveryAndGatesReadsAroundApply() throws assertEquals(CommonCheckpointRuntimeOwner.State.READY, owner.getState()); assertEquals("published", owner.read(() -> "published")); owner.close(); + owner.close(); + for (FakeMaterializer materializer : fixture.materializers) { + assertEquals(1, materializer.closed); + } assertThrows(IOException.class, () -> owner.read(() -> "unreachable")); } @@ -250,6 +266,9 @@ public void runtimeOwnerFailsClosedThenFreshOwnerRedoesDurableCheckpoint() throw assertEquals(RecoveryAction.NO_CHECKPOINT, failed.recoverBeforeServing()); assertThrows(IOException.class, () -> failed.apply(fixture.payload)); assertEquals(CommonCheckpointRuntimeOwner.State.FAILED, failed.getState()); + for (FakeMaterializer materializer : fixture.materializers) { + assertEquals(1, materializer.closed); + } assertThrows(IOException.class, () -> failed.read(() -> "unreachable")); CommonCheckpointRuntimeOwner recovered = new CommonCheckpointRuntimeOwner( @@ -348,6 +367,8 @@ private static final class FakeMaterializer implements CommonCheckpointMateriali private boolean advanceAfterMaterialize = true; private int scopesStarted; private int scopesEnded; + private int closed; + private boolean closeFailure; private boolean scopeOpen; private FakeMaterializer(Authority authority, List actions) { @@ -372,6 +393,14 @@ public void endCheckpoint(CommonCheckpointTarget expected) { scopeOpen = false; } + @Override + public void close() throws IOException { + closed++; + if (closeFailure) { + throw new IOException("injected close failure"); + } + } + @Override public Status inspect(CommonCheckpointTarget expected) throws IOException { if (target != null && !target.equals(expected)) { diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java index 97d3d215cdb..5e4c38ceca6 100644 --- a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRuntimeAttachmentTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; @@ -82,22 +83,50 @@ public void startupAndCheckpointFailuresRemainFailClosed() throws Exception { () -> CommonCheckpointRuntimeAttachment.open(true, () -> corrupt)); assertEquals(CommonCheckpointRuntimeOwner.State.CLOSED, corrupt.getState()); + Path publishedRoot = temporaryFolder.getRoot().toPath().resolve("published-failure"); + Path archive = publishedRoot.resolve("archive"); + Files.createDirectories(archive); + Files.write(archive.resolve("READABLE"), new byte[]{1}); + CommonCheckpointMaterializer startupChainbase = materializer(Authority.CHAINBASE); + CommonCheckpointMaterializer startupPathState = materializer(Authority.PATH_STATE); + CommonCheckpointMaterializer startupStateArchive = materializer(Authority.STATE_ARCHIVE); + CommonCheckpointRuntime published = runtime(publishedRoot, mock(Chainbase.class), + startupChainbase, startupPathState, startupStateArchive); + assertThrows(IOException.class, + () -> CommonCheckpointRuntimeAttachment.open(true, () -> published)); + verify(startupChainbase).close(); + verify(startupPathState).close(); + verify(startupStateArchive).close(); + assertEquals(CommonCheckpointRuntimeOwner.State.CLOSED, published.getState()); + Path failureRoot = temporaryFolder.getRoot().toPath().resolve("checkpoint-failure"); Chainbase database = mock(Chainbase.class); when(database.getHead()).thenThrow(new IllegalStateException("injected capture failure")); + CommonCheckpointMaterializer chainbase = materializer(Authority.CHAINBASE); + CommonCheckpointMaterializer pathState = materializer(Authority.PATH_STATE); + CommonCheckpointMaterializer stateArchive = materializer(Authority.STATE_ARCHIVE); CommonCheckpointRuntimeAttachment attachment = CommonCheckpointRuntimeAttachment.open(true, - () -> runtime(failureRoot, database)); + () -> runtime(failureRoot, database, chainbase, pathState, stateArchive)); assertThrows(IllegalStateException.class, () -> attachment.checkpointAndRebase(1)); assertEquals(CommonCheckpointRuntimeAttachment.State.FAILED, attachment.getState()); + verify(chainbase).close(); + verify(pathState).close(); + verify(stateArchive).close(); assertThrows(IllegalStateException.class, () -> attachment.pinPoint(0)); attachment.close(); assertEquals(CommonCheckpointRuntimeAttachment.State.CLOSED, attachment.getState()); } private static CommonCheckpointRuntime runtime(Path root, Chainbase database) { - CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( - new CommonCheckpointFile(root.resolve("wal")), materializer(Authority.CHAINBASE), + return runtime(root, database, materializer(Authority.CHAINBASE), materializer(Authority.PATH_STATE), materializer(Authority.STATE_ARCHIVE)); + } + + private static CommonCheckpointRuntime runtime(Path root, Chainbase database, + CommonCheckpointMaterializer chainbase, CommonCheckpointMaterializer pathState, + CommonCheckpointMaterializer stateArchive) { + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( + new CommonCheckpointFile(root.resolve("wal")), chainbase, pathState, stateArchive); return new CommonCheckpointRuntime(new CommonCheckpointRuntimeOwner(coordinator), Collections.singletonList(database), root.resolve("archive"), hash(1), Engine.LEVELDB, (blockNumber, blockHash) -> { From 53702079eec6de46e2579af477c1b1e694162df1 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 7 Sep 2026 17:27:50 +0800 Subject: [PATCH 122/161] fix(chainbase): bound common checkpoint markers Centralize common-checkpoint materialization records into one fixed slot per authority and rotate each slot with a forced atomic replacement. Preserve legacy marker reads, common WAL ordering, publication barriers, and archive block history while validating all authorities on startup. --- .../StateArchiveCheckpointMaterializer.java | 80 ++++++++-- .../core/ChainbaseCheckpointMaterializer.java | 55 +++++-- .../CommonCheckpointMaterializedStore.java | 145 ++++++++++++++++++ .../core/CommonCheckpointRedoCoordinator.java | 10 ++ .../db2/core/CommonCheckpointRuntime.java | 28 +++- .../core/CommonCheckpointRuntimeOwner.java | 17 ++ .../PathStateCheckpointMaterializer.java | 51 ++++-- .../PathStatePhysicalOverlayHead.java | 10 +- .../main/java/org/tron/core/db/Manager.java | 18 ++- .../ChainbaseCheckpointMaterializerTest.java | 51 +++++- ...CommonCheckpointMaterializedStoreTest.java | 76 +++++++++ .../CommonCheckpointRedoCoordinatorTest.java | 35 +++++ 12 files changed, 530 insertions(+), 46 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializedStore.java create mode 100644 framework/src/test/java/org/tron/core/db2/core/CommonCheckpointMaterializedStoreTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java index 1cf528d4c91..35cffb60970 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java @@ -22,8 +22,9 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; -import org.tron.core.db2.core.CommonCheckpointMaterializer; import org.tron.core.db2.core.CommonCheckpointBaseline; +import org.tron.core.db2.core.CommonCheckpointMaterializedStore; +import org.tron.core.db2.core.CommonCheckpointMaterializer; import org.tron.core.db2.core.CommonCheckpointPayload; import org.tron.core.db2.core.CommonCheckpointTarget; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; @@ -55,44 +56,54 @@ public final class StateArchiveCheckpointMaterializer implements CommonCheckpoin private final FaultHook faultHook; private final CommonCheckpointBaseline baseline; private final Engine engine; + private final CommonCheckpointMaterializedStore materializedStore; private StateArchiveCheckpointServingIndex.Session checkpointServingIndex; private CommonCheckpointTarget activeCheckpoint; private boolean closed; public StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity) { this(directory, formatIdentity, null, StateArchiveCheckpointServingIndex.configuredEngine(), - (stage, blockIndex) -> { }); + null, (stage, blockIndex) -> { }); } public StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, CommonCheckpointBaseline baseline) { this(directory, formatIdentity, baseline, - StateArchiveCheckpointServingIndex.configuredEngine(), (stage, blockIndex) -> { }); + StateArchiveCheckpointServingIndex.configuredEngine(), null, (stage, blockIndex) -> { }); } public StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, CommonCheckpointBaseline baseline, Engine engine) { - this(directory, formatIdentity, baseline, engine, (stage, blockIndex) -> { }); + this(directory, formatIdentity, baseline, engine, null, (stage, blockIndex) -> { }); + } + + public StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, + CommonCheckpointBaseline baseline, Engine engine, + CommonCheckpointMaterializedStore materializedStore) { + this(directory, formatIdentity, baseline, engine, materializedStore, + (stage, blockIndex) -> { }); } StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, FaultHook faultHook) { this(directory, formatIdentity, null, StateArchiveCheckpointServingIndex.configuredEngine(), - faultHook); + null, faultHook); } StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, Engine engine, FaultHook faultHook) { - this(directory, formatIdentity, null, engine, faultHook); + this(directory, formatIdentity, null, engine, null, faultHook); } private StateArchiveCheckpointMaterializer(Path directory, byte[] formatIdentity, - CommonCheckpointBaseline baseline, Engine engine, FaultHook faultHook) { + CommonCheckpointBaseline baseline, Engine engine, + CommonCheckpointMaterializedStore materializedStore, FaultHook faultHook) { this.directory = Objects.requireNonNull(directory, "directory"); this.formatIdentity = digest(formatIdentity, "formatIdentity"); this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); this.baseline = baseline; this.engine = Objects.requireNonNull(engine, "engine"); + this.materializedStore = materializedStore; } @Override @@ -151,7 +162,7 @@ public synchronized Status inspect(CommonCheckpointTarget target) throws IOExcep if (Files.exists(readable, LinkOption.NOFOLLOW_LINKS)) { TargetMarker current = loadTarget(readable); if (Arrays.equals(current.encoded, expected)) { - requireExact(materializedPath(admitted), expected); + requireMaterialized(admitted, expected); requireServingIndex(admitted); return Status.PUBLISHED; } @@ -159,11 +170,9 @@ public synchronized Status inspect(CommonCheckpointTarget target) throws IOExcep } else if (baseline != null) { baseline.requireParent(admitted, "State Archive"); } - Path materialized = materializedPath(admitted); - if (!Files.exists(materialized, LinkOption.NOFOLLOW_LINKS)) { + if (!isMaterialized(admitted, expected)) { return Status.NEEDS_MATERIALIZATION; } - requireExact(materialized, expected); requireServingIndex(admitted); return Status.MATERIALIZED; } @@ -177,8 +186,15 @@ public static CommonCheckpointTarget loadPublishedTarget(Path directory, public static CommonCheckpointTarget loadPublishedTarget(Path directory, byte[] expectedFormatIdentity, Engine engine) throws IOException { + return loadPublishedTarget(directory, expectedFormatIdentity, engine, null); + } + + public static CommonCheckpointTarget loadPublishedTarget(Path directory, + byte[] expectedFormatIdentity, Engine engine, + CommonCheckpointMaterializedStore materializedStore) throws IOException { StateArchiveCheckpointMaterializer materializer = - new StateArchiveCheckpointMaterializer(directory, expectedFormatIdentity, null, engine); + new StateArchiveCheckpointMaterializer(directory, expectedFormatIdentity, null, engine, + materializedStore); Path readable = directory.resolve(READABLE_FILE); if (!Files.exists(readable, LinkOption.NOFOLLOW_LINKS)) { throw new IOException("State Archive READABLE target is missing"); @@ -194,10 +210,17 @@ public static CommonCheckpointTarget loadPublishedTarget(Path directory, /** Returns the published target when present, validating its complete serving boundary once. */ public static Optional loadPublishedTargetIfPresent(Path directory, byte[] expectedFormatIdentity, Engine engine) throws IOException { + return loadPublishedTargetIfPresent(directory, expectedFormatIdentity, engine, null); + } + + public static Optional loadPublishedTargetIfPresent(Path directory, + byte[] expectedFormatIdentity, Engine engine, + CommonCheckpointMaterializedStore materializedStore) throws IOException { if (!Files.exists(directory.resolve(READABLE_FILE), LinkOption.NOFOLLOW_LINKS)) { return Optional.empty(); } - return Optional.of(loadPublishedTarget(directory, expectedFormatIdentity, engine)); + return Optional.of(loadPublishedTarget(directory, expectedFormatIdentity, engine, + materializedStore)); } /** Resolves the configured Archive index engine at runtime construction boundaries. */ @@ -237,7 +260,7 @@ public synchronized void materialize(CommonCheckpointPayload payload, checkpointServingIndex.apply(admittedPayload, admittedTarget); } faultHook.after(Stage.AFTER_SERVING_INDEX_BATCH, -1); - publishImmutable(materializedPath(admittedTarget), encodeTarget(admittedTarget)); + recordMaterialized(admittedTarget, encodeTarget(admittedTarget)); faultHook.after(Stage.AFTER_MATERIALIZED_TARGET, -1); } @@ -323,6 +346,35 @@ private Path materializedPath(CommonCheckpointTarget target) { return targetPath(target).resolve(MATERIALIZED_FILE); } + private boolean isMaterialized(CommonCheckpointTarget target, byte[] expected) + throws IOException { + if (materializedStore != null && materializedStore.exists(Authority.STATE_ARCHIVE)) { + return materializedStore.matches(Authority.STATE_ARCHIVE, expected); + } + Path legacy = materializedPath(target); + if (!Files.exists(legacy, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + requireExact(legacy, expected); + return true; + } + + private void requireMaterialized(CommonCheckpointTarget target, byte[] expected) + throws IOException { + if (!isMaterialized(target, expected)) { + throw new IOException("State Archive materialized checkpoint target is missing"); + } + } + + private void recordMaterialized(CommonCheckpointTarget target, byte[] encoded) + throws IOException { + if (materializedStore == null) { + publishImmutable(materializedPath(target), encoded); + } else { + materializedStore.replace(Authority.STATE_ARCHIVE, encoded); + } + } + private byte[] encodeBlock(CommonCheckpointPayload.BlockPayload block) { try { byte[] history = historyCodec.encode(block.getArchiveDiff()); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java index 7c41aceae60..41e86880a28 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializer.java @@ -40,29 +40,39 @@ public final class ChainbaseCheckpointMaterializer implements CommonCheckpointMa private final Map databases; private final FaultHook faultHook; private final CommonCheckpointBaseline baseline; + private final CommonCheckpointMaterializedStore materializedStore; public ChainbaseCheckpointMaterializer(Path directory, byte[] formatIdentity, List databases) { - this(directory, formatIdentity, databases, null, (stage, dbName) -> { }); + this(directory, formatIdentity, databases, null, null, (stage, dbName) -> { }); } public ChainbaseCheckpointMaterializer(Path directory, byte[] formatIdentity, List databases, CommonCheckpointBaseline baseline) { - this(directory, formatIdentity, databases, baseline, (stage, dbName) -> { }); + this(directory, formatIdentity, databases, baseline, null, (stage, dbName) -> { }); + } + + public ChainbaseCheckpointMaterializer(Path directory, byte[] formatIdentity, + List databases, CommonCheckpointBaseline baseline, + CommonCheckpointMaterializedStore materializedStore) { + this(directory, formatIdentity, databases, baseline, materializedStore, + (stage, dbName) -> { }); } ChainbaseCheckpointMaterializer(Path directory, byte[] formatIdentity, List databases, FaultHook faultHook) { - this(directory, formatIdentity, databases, null, faultHook); + this(directory, formatIdentity, databases, null, null, faultHook); } private ChainbaseCheckpointMaterializer(Path directory, byte[] formatIdentity, - List databases, CommonCheckpointBaseline baseline, FaultHook faultHook) { + List databases, CommonCheckpointBaseline baseline, + CommonCheckpointMaterializedStore materializedStore, FaultHook faultHook) { this.directory = Objects.requireNonNull(directory, "directory"); this.formatIdentity = digest(formatIdentity, "formatIdentity"); this.databases = index(databases); this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); this.baseline = baseline; + this.materializedStore = materializedStore; } @Override @@ -90,18 +100,16 @@ public synchronized Status inspect(CommonCheckpointTarget target) throws IOExcep if (Files.exists(currentPath, LinkOption.NOFOLLOW_LINKS)) { Marker current = load(currentPath); if (Arrays.equals(current.encoded, expected)) { - requireExact(materializedPath(admitted), expected); + requireMaterialized(admitted, expected); return Status.PUBLISHED; } requireParent(current, admitted); } else if (baseline != null) { baseline.requireParent(admitted, "Chainbase"); } - Path materialized = materializedPath(admitted); - if (!Files.exists(materialized, LinkOption.NOFOLLOW_LINKS)) { + if (!isMaterialized(admitted, expected)) { return Status.NEEDS_MATERIALIZATION; } - requireExact(materialized, expected); return Status.MATERIALIZED; } @@ -132,7 +140,7 @@ public synchronized void materialize(CommonCheckpointPayload payload, ((SnapshotRoot) root).applyCheckpointMutations(batch(store)); faultHook.after(Stage.AFTER_STORE_BATCH, store.getDbName()); } - publishImmutable(materializedPath(admittedTarget), encode(admittedTarget)); + recordMaterialized(admittedTarget, encode(admittedTarget)); faultHook.after(Stage.AFTER_MATERIALIZED_TARGET, null); } @@ -173,6 +181,35 @@ private Path materializedPath(CommonCheckpointTarget target) { return directory.resolve(MATERIALIZED_DIRECTORY).resolve(hex(target.getPayloadDigest())); } + private boolean isMaterialized(CommonCheckpointTarget target, byte[] expected) + throws IOException { + if (materializedStore != null && materializedStore.exists(Authority.CHAINBASE)) { + return materializedStore.matches(Authority.CHAINBASE, expected); + } + Path legacy = materializedPath(target); + if (!Files.exists(legacy, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + requireExact(legacy, expected); + return true; + } + + private void requireMaterialized(CommonCheckpointTarget target, byte[] expected) + throws IOException { + if (!isMaterialized(target, expected)) { + throw new IOException("Chainbase materialized checkpoint target is missing"); + } + } + + private void recordMaterialized(CommonCheckpointTarget target, byte[] encoded) + throws IOException { + if (materializedStore == null) { + publishImmutable(materializedPath(target), encoded); + } else { + materializedStore.replace(Authority.CHAINBASE, encoded); + } + } + private static Map batch( CommonCheckpointPayload.StoreMutations store) { Map batch = new LinkedHashMap<>(); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializedStore.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializedStore.java new file mode 100644 index 00000000000..3ac8c681a44 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializedStore.java @@ -0,0 +1,145 @@ +package org.tron.core.db2.core; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Objects; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; + +/** Central, bounded materialization records for all common-checkpoint authorities. */ +public final class CommonCheckpointMaterializedStore { + + static final String DIRECTORY = "materialized"; + + private final Path directory; + private final FaultHook faultHook; + private final EnumSet sealed = EnumSet.noneOf(Authority.class); + + public CommonCheckpointMaterializedStore(Path checkpointDirectory) { + this(checkpointDirectory, (stage, path) -> { }); + } + + CommonCheckpointMaterializedStore(Path checkpointDirectory, FaultHook faultHook) { + this.directory = Objects.requireNonNull(checkpointDirectory, "checkpointDirectory") + .toAbsolutePath().normalize().resolve(DIRECTORY); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + /** Returns whether this authority's single retained slot exactly matches {@code expected}. */ + public synchronized boolean matches(Authority authority, byte[] expected) throws IOException { + Path path = path(authority); + if (!exists(authority)) { + return false; + } + requireRegular(path); + byte[] actual = Files.readAllBytes(path); + if (!Arrays.equals(actual, Objects.requireNonNull(expected, "expected"))) { + return false; + } + seal(authority); + return true; + } + + public synchronized boolean exists(Authority authority) throws IOException { + if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS) + && (Files.isSymbolicLink(directory) + || !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS))) { + throw new IOException("common checkpoint materialized path is not a direct directory"); + } + return Files.exists(path(authority), LinkOption.NOFOLLOW_LINKS); + } + + /** Atomically replaces this authority's prior slot and durably retires its old target. */ + public synchronized void replace(Authority authority, byte[] encoded) throws IOException { + byte[] admitted = Arrays.copyOf(Objects.requireNonNull(encoded, "encoded"), encoded.length); + if (admitted.length == 0) { + throw new IllegalArgumentException("materialized checkpoint record must not be empty"); + } + requireDirectory(); + Path target = path(authority); + if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) { + requireRegular(target); + if (Arrays.equals(Files.readAllBytes(target), admitted)) { + seal(authority); + return; + } + } + sealed.remove(authority); + Path temporary = directory.resolve("." + authority.name() + ".tmp"); + if (Files.deleteIfExists(temporary)) { + syncDirectory(); + } + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(admitted); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + faultHook.after(Stage.AFTER_TEMPORARY_FORCE, temporary); + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("common checkpoint materialized store requires atomic replace", + unsupported); + } + faultHook.after(Stage.AFTER_ATOMIC_REPLACE, target); + syncDirectory(); + sealed.addAll(EnumSet.allOf(Authority.class)); + faultHook.after(Stage.AFTER_DIRECTORY_FORCE, target); + } + + Path path(Authority authority) { + return directory.resolve(Objects.requireNonNull(authority, "authority").name()); + } + + private void requireDirectory() throws IOException { + if (Files.isSymbolicLink(directory)) { + throw new IOException("common checkpoint materialized directory must not be a symlink"); + } + Files.createDirectories(directory); + if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("common checkpoint materialized path is not a directory"); + } + } + + private void syncDirectory() throws IOException { + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + private void seal(Authority authority) throws IOException { + if (!sealed.contains(authority)) { + syncDirectory(); + sealed.addAll(EnumSet.allOf(Authority.class)); + } + } + + private static void requireRegular(Path path) throws IOException { + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("common checkpoint materialized slot is not a regular file"); + } + } + + enum Stage { + AFTER_TEMPORARY_FORCE, + AFTER_ATOMIC_REPLACE, + AFTER_DIRECTORY_FORCE + } + + @FunctionalInterface + interface FaultHook { + void after(Stage stage, Path path) throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java index 45f81ade009..85e78446918 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java @@ -85,6 +85,16 @@ public synchronized RecoveryAction recover() throws IOException { return action; } + /** Requires every authority to expose the same fully published startup target. */ + synchronized void requirePublished(CommonCheckpointTarget target) throws IOException { + requireOpen(); + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + for (Authority authority : ORDER) { + requireStatus(authority, Status.PUBLISHED, + materializers.get(authority).inspect(admitted), "startup validation"); + } + } + /** Closes runtime-owned authority resources in reverse publication order. */ @Override public synchronized void close() throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java index 437eaddc350..9055213c8e4 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java @@ -25,6 +25,7 @@ public final class CommonCheckpointRuntime implements AutoCloseable { private final Engine engine; private final StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory; private final CommonCheckpointMemoryRebaser memoryRebaser; + private final CommonCheckpointMaterializedStore materializedStore; private final LongSupplier nanoTime; private final TimingSink timingSink; private final CommonCheckpointPayloadFactory payloadFactory = new CommonCheckpointPayloadFactory(); @@ -36,7 +37,16 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, + Path archiveDirectory, byte[] formatIdentity, Engine engine, + StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory, + CommonCheckpointMemoryRebaser memoryRebaser, + CommonCheckpointMaterializedStore materializedStore) { + this(owner, databases, archiveDirectory, formatIdentity, engine, latestFactory, + memoryRebaser, materializedStore, System::nanoTime, CommonCheckpointRuntime::logTiming); } CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, @@ -44,6 +54,16 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, + Path archiveDirectory, byte[] formatIdentity, Engine engine, + StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory, + CommonCheckpointMemoryRebaser memoryRebaser, + CommonCheckpointMaterializedStore materializedStore, LongSupplier nanoTime, + TimingSink timingSink) { this.owner = Objects.requireNonNull(owner, "owner"); this.databases = new ArrayList<>(Objects.requireNonNull(databases, "databases")); if (this.databases.isEmpty() || this.databases.contains(null)) { @@ -54,6 +74,7 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List { }); + this(stores, scope, formatIdentity, null, null, (stage, storeId) -> { }); } public PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, PathStateParticipantScope scope, byte[] formatIdentity, CommonCheckpointBaseline baseline) { - this(stores, scope, formatIdentity, baseline, (stage, storeId) -> { }); + this(stores, scope, formatIdentity, baseline, null, (stage, storeId) -> { }); + } + + public PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, + PathStateParticipantScope scope, byte[] formatIdentity, CommonCheckpointBaseline baseline, + CommonCheckpointMaterializedStore materializedStore) { + this(stores, scope, formatIdentity, baseline, materializedStore, (stage, storeId) -> { }); } PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, PathStateParticipantScope scope, byte[] formatIdentity, FaultHook faultHook) { - this(stores, scope, formatIdentity, null, faultHook); + this(stores, scope, formatIdentity, null, null, faultHook); } private PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, PathStateParticipantScope scope, byte[] formatIdentity, CommonCheckpointBaseline baseline, - FaultHook faultHook) { + CommonCheckpointMaterializedStore materializedStore, FaultHook faultHook) { this.stores = Objects.requireNonNull(stores, "stores"); this.scope = Objects.requireNonNull(scope, "scope"); this.directory = stores.getDirectory(); this.formatIdentity = digest(formatIdentity, "formatIdentity"); this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); this.baseline = baseline; + this.materializedStore = materializedStore; } @Override @@ -153,11 +162,9 @@ public synchronized Status inspect(CommonCheckpointTarget target) throws IOExcep } else if (baseline != null) { baseline.requireParent(admitted, "PathState"); } - Path materialized = materializedPath(admitted); - if (!Files.exists(materialized, LinkOption.NOFOLLOW_LINKS)) { + if (!isMaterialized(admitted, expected)) { return Status.NEEDS_MATERIALIZATION; } - requireExact(materialized, expected); return Status.MATERIALIZED; } @@ -194,7 +201,7 @@ public synchronized void materialize(CommonCheckpointPayload payload, faultHook.after(Stage.AFTER_SUPER_BATCH, 0); } byte[] encoded = encode(admittedTarget); - PathStateMetadataFile.publishImmutableBytes(materializedPath(admittedTarget), encoded); + recordMaterialized(admittedTarget, encoded); faultHook.after(Stage.AFTER_MATERIALIZED_TARGET, 0); } @@ -222,7 +229,31 @@ private CommonCheckpointTarget requireTarget(CommonCheckpointTarget target) thro private void requireMaterialized(CommonCheckpointTarget target, byte[] expected) throws IOException { - requireExact(materializedPath(target), expected); + if (!isMaterialized(target, expected)) { + throw new IOException("PathState materialized checkpoint target is missing"); + } + } + + private boolean isMaterialized(CommonCheckpointTarget target, byte[] expected) + throws IOException { + if (materializedStore != null && materializedStore.exists(Authority.PATH_STATE)) { + return materializedStore.matches(Authority.PATH_STATE, expected); + } + Path legacy = materializedPath(target); + if (!Files.exists(legacy, LinkOption.NOFOLLOW_LINKS)) { + return false; + } + requireExact(legacy, expected); + return true; + } + + private void recordMaterialized(CommonCheckpointTarget target, byte[] encoded) + throws IOException { + if (materializedStore == null) { + PathStateMetadataFile.publishImmutableBytes(materializedPath(target), encoded); + } else { + materializedStore.replace(Authority.PATH_STATE, encoded); + } } private void requireParent(Marker current, CommonCheckpointTarget target) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java index 88495219738..99116b8a25d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java @@ -18,6 +18,7 @@ import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.core.CommonCheckpointBaseline; import org.tron.core.db2.core.CommonCheckpointMemoryRebaser; +import org.tron.core.db2.core.CommonCheckpointMaterializedStore; import org.tron.core.db2.core.CommonCheckpointTarget; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; @@ -151,8 +152,15 @@ public synchronized void synchronizePublishedCheckpoint(byte[] formatIdentity, /** Creates the PathState authority over the same stores used by this in-memory head. */ public synchronized PathStateCheckpointMaterializer checkpointMaterializer( byte[] formatIdentity, CommonCheckpointBaseline baseline) throws IOException { + return checkpointMaterializer(formatIdentity, baseline, null); + } + + public synchronized PathStateCheckpointMaterializer checkpointMaterializer( + byte[] formatIdentity, CommonCheckpointBaseline baseline, + CommonCheckpointMaterializedStore materializedStore) throws IOException { requireHealthy(); - return new PathStateCheckpointMaterializer(stores, scope, formatIdentity, baseline); + return new PathStateCheckpointMaterializer(stores, scope, formatIdentity, baseline, + materializedStore); } /** Builds a fully validated parentless-target plus reversible-suffix memory rebase. */ diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index e3e67dee0a1..88612c5f575 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -142,6 +142,7 @@ import org.tron.core.db2.core.CommonCheckpointBaselineFile; import org.tron.core.db2.core.CommonCheckpointFile; import org.tron.core.db2.core.CommonCheckpointFormat; +import org.tron.core.db2.core.CommonCheckpointMaterializedStore; import org.tron.core.db2.core.CommonCheckpointRedoCoordinator; import org.tron.core.db2.core.CommonCheckpointRuntime; import org.tron.core.db2.core.CommonCheckpointRuntimeAttachment; @@ -860,20 +861,22 @@ private void initCommonCheckpoint() { supplementalStores = commonCheckpointSupplementalStores(snapshots); LatestStateGenerationAdapter latest = LatestStateGenerationCoordinatorFactory.createAdapter( snapshots, supplementalStores); + CommonCheckpointMaterializedStore materializedStore = + new CommonCheckpointMaterializedStore(checkpointDirectory); PathStateCheckpointMaterializer pathMaterializer = pathOwner.checkpointMaterializer( - formatIdentity, baseline); + formatIdentity, baseline, materializedStore); CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( new CommonCheckpointFile(checkpointDirectory), new ChainbaseCheckpointMaterializer(checkpointDirectory, formatIdentity, - snapshots.getDbs(), baseline), + snapshots.getDbs(), baseline, materializedStore), pathMaterializer, new StateArchiveCheckpointMaterializer(archiveDirectory, formatIdentity, baseline, - engine)); + engine, materializedStore)); PathStatePhysicalOverlayHead admittedOwner = pathOwner; attachment = CommonCheckpointRuntimeAttachment.open(true, () -> new CommonCheckpointRuntime(new CommonCheckpointRuntimeOwner(coordinator), snapshots.getDbs(), archiveDirectory, formatIdentity, engine, latest::pin, - admittedOwner::prepareCommonCheckpointRebase)); + admittedOwner::prepareCommonCheckpointRebase, materializedStore)); canonical = currentCanonicalBlockMeta(); if (Files.isRegularFile(pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), @@ -886,7 +889,7 @@ private void initCommonCheckpoint() { if (Files.isRegularFile(pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), LinkOption.NOFOLLOW_LINKS)) { requireCommonPublishedAuthorities(checkpointDirectory, archiveDirectory, pathDirectory, - formatIdentity, engine); + formatIdentity, engine, materializedStore); } PathStateRootMetadata recovered = admittedOwner.getHead(); if (recovered.getBlockNumber() != canonical.getBlockNumber() @@ -956,14 +959,15 @@ private P66Phase currentPathStatePhase() { private static void requireCommonPublishedAuthorities(Path checkpointDirectory, Path archiveDirectory, Path pathDirectory, byte[] formatIdentity, - PathStateStoreManifest.Engine engine) throws java.io.IOException { + PathStateStoreManifest.Engine engine, + CommonCheckpointMaterializedStore materializedStore) throws java.io.IOException { ChainbaseCheckpointMaterializer.PublishedHead chain = ChainbaseCheckpointMaterializer.loadPublishedHead(checkpointDirectory, formatIdentity); PathStateCheckpointMaterializer.PublishedHead path = PathStateCheckpointMaterializer.loadPublishedHead(pathDirectory, formatIdentity); org.tron.core.db2.core.CommonCheckpointTarget archive = StateArchiveCheckpointMaterializer.loadPublishedTarget(archiveDirectory, formatIdentity, - engine); + engine, materializedStore); BlockSnapshotMeta last = archive.getLastBlock(); if (chain.getEpoch() != last.getEpoch() || path.getEpoch() != last.getEpoch() || chain.getBlockNumber() != last.getBlockNumber() diff --git a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java index e1d0adf119c..f0da62962c3 100644 --- a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java @@ -121,6 +121,21 @@ public void resumesEveryStoreAndMarkerBoundaryUsingCheckpointRedo() throws Excep } } + @Test + public void admitsLegacyMarkerOnlyUntilCentralSlotExists() throws Exception { + Fixture fixture = fixture("legacy-central", null); + fixture.materializer.materialize(fixture.payload, fixture.target); + fixture.materializer.publish(fixture.target); + CommonCheckpointMaterializedStore store = new CommonCheckpointMaterializedStore( + fixture.root.resolve("common")); + ChainbaseCheckpointMaterializer migrated = new ChainbaseCheckpointMaterializer( + fixture.root, fixture.format, fixture.databases, null, store); + + assertEquals(Status.PUBLISHED, migrated.inspect(fixture.target)); + store.replace(Authority.CHAINBASE, new byte[]{1}); + assertThrows(IOException.class, () -> migrated.inspect(fixture.target)); + } + @Test public void rejectsUnknownStoreForeignFormatAndNonParentTarget() throws Exception { Fixture fixture = fixture("reject", null); @@ -208,15 +223,17 @@ public void realThreeAuthorityCoordinatorCrossesBothBarriersThenRetiresWal() PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); CommonCheckpointPayload payload = integratedPayload(format, scope); CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + CommonCheckpointMaterializedStore materializedStore = + new CommonCheckpointMaterializedStore(root.resolve("wal")); try (PathStatePhysicalStoreSet pathStores = PathStatePhysicalStoreSet.open( root.resolve("path-state"), scope, Engine.ROCKSDB)) { ChainbaseCheckpointMaterializer chainbase = new ChainbaseCheckpointMaterializer( - root.resolve("chainbase"), format, databases); + root.resolve("chainbase"), format, databases, null, materializedStore); PathStateCheckpointMaterializer pathState = new PathStateCheckpointMaterializer(pathStores, - scope, format); + scope, format, null, materializedStore); StateArchiveCheckpointMaterializer archive = new StateArchiveCheckpointMaterializer( - root.resolve("archive"), format); + root.resolve("archive"), format, null, Engine.LEVELDB, materializedStore); CommonCheckpointFile file = new CommonCheckpointFile(root.resolve("wal")); CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator(file, chainbase, pathState, archive); @@ -233,6 +250,34 @@ public void realThreeAuthorityCoordinatorCrossesBothBarriersThenRetiresWal() new byte[]{3})); assertFalse(java.nio.file.Files.exists(root.resolve("wal").resolve( CommonCheckpointFile.FILE_NAME))); + try (java.util.stream.Stream markers = java.nio.file.Files.list( + root.resolve("wal/materialized"))) { + assertEquals(3L, markers.count()); + } + assertFalse(java.nio.file.Files.exists( + root.resolve("chainbase/chainbase-checkpoint-materialized"))); + assertFalse(java.nio.file.Files.exists( + root.resolve("path-state/checkpoint-materialized"))); + try (java.util.stream.Stream paths = java.nio.file.Files.walk( + root.resolve("archive/checkpoint-targets"))) { + assertFalse(paths.anyMatch(path -> "MATERIALIZED".equals( + path.getFileName().toString()))); + } + coordinator.close(); + CommonCheckpointMaterializedStore reopenedStore = + new CommonCheckpointMaterializedStore(root.resolve("wal")); + ChainbaseCheckpointMaterializer reopenedChainbase = + new ChainbaseCheckpointMaterializer(root.resolve("chainbase"), format, databases, + null, reopenedStore); + PathStateCheckpointMaterializer reopenedPathState = + new PathStateCheckpointMaterializer(pathStores, scope, format, null, reopenedStore); + StateArchiveCheckpointMaterializer reopenedArchive = + new StateArchiveCheckpointMaterializer(root.resolve("archive"), format, null, + Engine.LEVELDB, reopenedStore); + assertEquals(Status.PUBLISHED, reopenedChainbase.inspect(target)); + assertEquals(Status.PUBLISHED, reopenedPathState.inspect(target)); + assertEquals(Status.PUBLISHED, reopenedArchive.inspect(target)); + reopenedArchive.close(); } } diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointMaterializedStoreTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointMaterializedStoreTest.java new file mode 100644 index 00000000000..d76f1f63710 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointMaterializedStoreTest.java @@ -0,0 +1,76 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; + +public class CommonCheckpointMaterializedStoreTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void atomicallyRotatesOneSlotPerAuthorityWithoutHistoryGrowth() throws Exception { + Path root = temporaryFolder.newFolder("materialized").toPath(); + CommonCheckpointMaterializedStore store = new CommonCheckpointMaterializedStore(root); + + for (int target = 0; target < 100; target++) { + for (Authority authority : Authority.values()) { + byte[] encoded = record(target, authority); + store.replace(authority, encoded); + store.replace(authority, encoded); + assertTrue(store.matches(authority, encoded)); + } + } + + try (Stream paths = Files.list(root.resolve( + CommonCheckpointMaterializedStore.DIRECTORY))) { + assertEquals(Authority.values().length, paths.count()); + } + for (Authority authority : Authority.values()) { + assertArrayEquals(record(99, authority), Files.readAllBytes(store.path(authority))); + assertFalse(store.matches(authority, record(98, authority))); + } + } + + @Test + public void resumesEveryAtomicReplaceBoundaryWithoutGrowingTemporaryFiles() throws Exception { + for (CommonCheckpointMaterializedStore.Stage failedStage + : CommonCheckpointMaterializedStore.Stage.values()) { + Path root = temporaryFolder.newFolder("fault-" + failedStage).toPath(); + CommonCheckpointMaterializedStore original = new CommonCheckpointMaterializedStore(root); + original.replace(Authority.CHAINBASE, record(1, Authority.CHAINBASE)); + CommonCheckpointMaterializedStore interrupted = new CommonCheckpointMaterializedStore(root, + (stage, path) -> { + if (stage == failedStage) { + throw new IOException("injected " + stage); + } + }); + + assertThrows(IOException.class, + () -> interrupted.replace(Authority.CHAINBASE, record(2, Authority.CHAINBASE))); + CommonCheckpointMaterializedStore recovered = new CommonCheckpointMaterializedStore(root); + recovered.replace(Authority.CHAINBASE, record(2, Authority.CHAINBASE)); + assertTrue(recovered.matches(Authority.CHAINBASE, record(2, Authority.CHAINBASE))); + try (Stream paths = Files.list(root.resolve( + CommonCheckpointMaterializedStore.DIRECTORY))) { + assertEquals(1L, paths.count()); + } + } + } + + private static byte[] record(int target, Authority authority) { + return new byte[]{(byte) target, (byte) authority.ordinal()}; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java index b682bd94a76..bc91155d87d 100644 --- a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java @@ -278,6 +278,41 @@ public void runtimeOwnerFailsClosedThenFreshOwnerRedoesDurableCheckpoint() throw assertEquals("recovered", recovered.read(() -> "recovered")); } + @Test + public void startupPublishedValidationRequiresEveryAuthority() throws Exception { + Fixture accepted = fixture("runtime-owner-published", null); + CommonCheckpointTarget target = CommonCheckpointTarget.from(accepted.payload); + for (FakeMaterializer materializer : accepted.materializers) { + materializer.target = target; + materializer.status = Status.PUBLISHED; + } + CommonCheckpointRuntimeOwner owner = new CommonCheckpointRuntimeOwner(accepted.coordinator); + assertEquals(RecoveryAction.NO_CHECKPOINT, owner.recoverBeforeServing()); + owner.requirePublishedBeforeServing(target); + assertEquals(CommonCheckpointRuntimeOwner.State.READY, owner.getState()); + owner.close(); + for (FakeMaterializer materializer : accepted.materializers) { + assertEquals(1, materializer.closed); + } + + Fixture rejected = fixture("runtime-owner-partial-published", null); + CommonCheckpointTarget rejectedTarget = CommonCheckpointTarget.from(rejected.payload); + for (FakeMaterializer materializer : rejected.materializers) { + materializer.target = rejectedTarget; + materializer.status = Status.PUBLISHED; + } + rejected.materializers.get(1).status = Status.MATERIALIZED; + CommonCheckpointRuntimeOwner rejectedOwner = + new CommonCheckpointRuntimeOwner(rejected.coordinator); + assertEquals(RecoveryAction.NO_CHECKPOINT, rejectedOwner.recoverBeforeServing()); + assertThrows(IOException.class, + () -> rejectedOwner.requirePublishedBeforeServing(rejectedTarget)); + assertEquals(CommonCheckpointRuntimeOwner.State.FAILED, rejectedOwner.getState()); + for (FakeMaterializer materializer : rejected.materializers) { + assertEquals(1, materializer.closed); + } + } + private Fixture fixture(String name, CommonCheckpointRedoCoordinator.Stage failure) { CommonCheckpointPayload payload = payload(); CommonCheckpointFile file = new CommonCheckpointFile( From 3329b1e83d647432fceb4286a4ae1d7093429583 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 7 Sep 2026 22:22:33 +0800 Subject: [PATCH 123/161] fix(chainbase): redo checkpoint before path open Recover a pending common-checkpoint WAL with a short-lived physical PathState session before restoring the published PathState root. Keep the no-WAL startup path and persistent format unchanged, and cover partial materialization plus a second restart with the Manager integration test. --- .../core/db2/core/CommonCheckpointFile.java | 5 + .../PathStateCheckpointMaterializer.java | 48 +++++- .../main/java/org/tron/core/db/Manager.java | 35 +++++ ...athStateManagerStartupIntegrationTest.java | 141 +++++++++++++++++- 4 files changed, 227 insertions(+), 2 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFile.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFile.java index 5afe0b24e82..11403f1a153 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFile.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFile.java @@ -89,6 +89,11 @@ public synchronized CommonCheckpointPayload loadIfPresent() throws IOException { return loadRequired(); } + /** Returns whether startup must run redo before opening a published physical state. */ + public synchronized boolean isPresent() { + return Files.exists(checkpoint, LinkOption.NOFOLLOW_LINKS); + } + /** Retires only this checkpoint and its non-authoritative temporary file. */ public synchronized void retire() throws IOException { requireDirectory(); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java index 3bf3690851e..90542575f7d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializer.java @@ -20,6 +20,7 @@ import org.tron.core.db2.core.CommonCheckpointMaterializer; import org.tron.core.db2.core.CommonCheckpointPayload; import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; /** Next-format PathState participant for the common-checkpoint two-barrier protocol. */ public final class PathStateCheckpointMaterializer implements CommonCheckpointMaterializer { @@ -64,7 +65,7 @@ public PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, this(stores, scope, formatIdentity, null, null, faultHook); } - private PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, + PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, PathStateParticipantScope scope, byte[] formatIdentity, CommonCheckpointBaseline baseline, CommonCheckpointMaterializedStore materializedStore, FaultHook faultHook) { this.stores = Objects.requireNonNull(stores, "stores"); @@ -76,6 +77,29 @@ private PathStateCheckpointMaterializer(PathStatePhysicalStoreSet stores, this.materializedStore = materializedStore; } + /** + * Opens physical stores for WAL redo without first trusting the possibly stale published + * PathState CURRENT. The session must be closed before the normal overlay is opened. + */ + public static RecoverySession openRecovery(Path directory, Engine engine, + long residentNodeCacheBytes, byte[] formatIdentity, CommonCheckpointBaseline baseline, + CommonCheckpointMaterializedStore materializedStore) throws IOException { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.openExisting(directory, scope, + engine, residentNodeCacheBytes); + try { + return new RecoverySession(stores, new PathStateCheckpointMaterializer(stores, scope, + formatIdentity, baseline, materializedStore)); + } catch (RuntimeException failure) { + try { + stores.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } + } + @Override public Authority authority() { return Authority.PATH_STATE; @@ -365,6 +389,28 @@ interface FaultHook { void after(Stage stage, int storeId) throws IOException; } + /** Short-lived owner used only while replaying a durable common-checkpoint WAL. */ + public static final class RecoverySession implements AutoCloseable { + + private final PathStatePhysicalStoreSet stores; + private final PathStateCheckpointMaterializer materializer; + + private RecoverySession(PathStatePhysicalStoreSet stores, + PathStateCheckpointMaterializer materializer) { + this.stores = stores; + this.materializer = materializer; + } + + public PathStateCheckpointMaterializer getMaterializer() { + return materializer; + } + + @Override + public void close() throws IOException { + stores.close(); + } + } + private static final class Marker { private final byte[] encoded; diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 88612c5f575..2ec21fbbe19 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -816,6 +816,9 @@ private void initCommonCheckpoint() { PathStateLayerLimits limits = new PathStateLayerLimits( storage.getPathStateRootReversibleLayerLimit(), storage.getPathStateRootReversibleLayerBytes()); + recoverPendingCommonCheckpoint(snapshots, checkpointDirectory, archiveDirectory, + pathDirectory, engine, storage.getPathStateRootNodeCacheBytes(), formatIdentity, + baselineFile, baselineExists, modeAdmitted); BlockSnapshotMeta canonical = currentCanonicalBlockMeta(); P66Phase phase = currentPathStatePhase(); if (modeAdmitted && Files.isRegularFile( @@ -940,6 +943,38 @@ private void initCommonCheckpoint() { } } + private void recoverPendingCommonCheckpoint(SnapshotManager snapshots, + Path checkpointDirectory, Path archiveDirectory, Path pathDirectory, + PathStateStoreManifest.Engine engine, long residentNodeCacheBytes, byte[] formatIdentity, + CommonCheckpointBaselineFile baselineFile, boolean baselineExists, boolean modeAdmitted) + throws java.io.IOException { + CommonCheckpointFile checkpointFile = new CommonCheckpointFile(checkpointDirectory); + if (!checkpointFile.isPresent()) { + return; + } + if (!baselineExists || !modeAdmitted) { + throw new java.io.IOException( + "Common checkpoint WAL requires an admitted PathState baseline"); + } + CommonCheckpointBaseline baseline = baselineFile.load(); + CommonCheckpointMaterializedStore materializedStore = + new CommonCheckpointMaterializedStore(checkpointDirectory); + try (PathStateCheckpointMaterializer.RecoverySession pathRecovery = + PathStateCheckpointMaterializer.openRecovery(pathDirectory, engine, + residentNodeCacheBytes, formatIdentity, baseline, materializedStore); + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( + checkpointFile, + new ChainbaseCheckpointMaterializer(checkpointDirectory, formatIdentity, + snapshots.getDbs(), baseline, materializedStore), + pathRecovery.getMaterializer(), + new StateArchiveCheckpointMaterializer(archiveDirectory, formatIdentity, baseline, + engine, materializedStore))) { + CommonCheckpointRedoCoordinator.RecoveryAction action = coordinator.recover(); + logger.info("Common checkpoint startup redo completed before PathState open: action={}", + action); + } + } + private BlockSnapshotMeta currentCanonicalBlockMeta() throws BadItemException, ItemNotFoundException { long number = getDynamicPropertiesStore().getLatestBlockHeaderNumber(); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index 5be947bdf95..11632366616 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -29,12 +29,20 @@ import org.tron.core.config.args.Storage; import org.tron.core.db.Manager; import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.archive.LatestStateGenerationAdapter.SnapshotCapableStore; import org.tron.core.db2.archive.LatestStateGenerationAdapter.StoreSnapshot; import org.tron.core.db2.common.DB; import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.ChainbaseCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointBaseline; import org.tron.core.db2.core.CommonCheckpointBaselineFile; +import org.tron.core.db2.core.CommonCheckpointFile; +import org.tron.core.db2.core.CommonCheckpointFormat; +import org.tron.core.db2.core.CommonCheckpointMaterializedStore; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointTarget; import org.tron.core.db2.core.SnapshotManager; import org.tron.core.db2.core.SnapshotRoot; import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; @@ -391,6 +399,115 @@ public void commonCheckpointFreshFlushAndRestartUseOneDurableBoundary() throws E invoke(manager, "closePathStateRoot"); } + @Test + public void commonCheckpointRedoPrecedesPathStateOpenAfterPartialMaterialization() + throws Exception { + Path output = temporaryFolder.newFolder("common-checkpoint-partial-path-redo").toPath(); + Path pathDirectory = output.resolve("path-state-root"); + Path checkpointDirectory = output.resolve("common-checkpoint"); + Path archiveDirectory = output.resolve("state-archive"); + long baseNumber = 100L; + BlockId baseId = new BlockId(Sha256Hash.wrap(bytes(61)), baseNumber); + Sha256Hash baseParent = Sha256Hash.wrap(bytes(60)); + DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(baseNumber); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(baseId); + when(dynamic.getLatestBlockHeaderTimestamp()).thenReturn(300L); + when(dynamic.getAllowAccountAssetOptimizationFromRoot()).thenReturn(1L); + BlockCapsule baseBlock = block(baseNumber, baseId, baseParent, 300L); + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); + when(chainBase.getBlockByNum(baseNumber)).thenReturn(baseBlock); + when(chainBase.getAccountAssetStore()).thenReturn(mock(AccountAssetStore.class)); + + AtomicInteger closed = new AtomicInteger(); + SnapshotManager[] holder = new SnapshotManager[1]; + Manager manager = new Manager(); + setChainBaseManager(manager, chainBase); + withCommonConfig(output, "LEVELDB", () -> { + SnapshotManager snapshots = new SnapshotManager(""); + for (PathStateParticipantDescriptor.StoreIdentity participant + : PathStateParticipantDescriptor.current().getStores()) { + snapshots.getDbs().add(emptyNativeStore(participant.getDbName(), baseNumber, + baseId.getBytes(), closed)); + } + snapshots.enable(); + snapshots.setUnChecked(false); + holder[0] = snapshots; + setField(manager, "revokingStore", snapshots); + invoke(manager, "initCommonCheckpoint"); + }); + SnapshotManager snapshots = holder[0]; + BlockId firstId = new BlockId(Sha256Hash.wrap(bytes(62)), 101L); + try (ISession session = snapshots.buildSession()) { + session.commit(BlockSnapshotMeta.forBlock(101L, firstId.getBytes(), baseId.getBytes(), + 303L)); + } + setSnapshotField(snapshots, "flushCount", 1); + snapshots.flush(); + invoke(manager, "closeCommonCheckpoint"); + invoke(manager, "closePathStateRoot"); + + byte[] formatIdentity = CommonCheckpointFormat.identity(); + CommonCheckpointBaseline baseline = new CommonCheckpointBaselineFile(checkpointDirectory) + .load(); + BlockId targetId = new BlockId(Sha256Hash.wrap(bytes(63)), 102L); + BlockSnapshotMeta targetMeta = BlockSnapshotMeta.forBlock(102L, targetId.getBytes(), + firstId.getBytes(), 306L); + CommonCheckpointPayload payload; + try (PathStatePhysicalOverlayHead head = PathStatePhysicalOverlayHead.openCommonCheckpoint( + pathDirectory, Engine.LEVELDB, new PathStateLayerLimits(8, 1L << 20), 1L << 20, + 2, 2, formatIdentity, targetMeta, P66Phase.P66_ON)) { + PathStateBlockTransition transition = new PathStateBlockTransition(102L, + targetId.getBytes(), firstId.getBytes(), 306L, P66Phase.P66_ON, + Collections.singletonList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{2}))); + PathStateSnapshotDelta delta = head.prepareSnapshotDelta(targetMeta, transition); + PathStateFlushTarget target = PathStateFlushTarget.coalesce( + Collections.singletonList(delta)); + payload = CommonCheckpointPayload.create(formatIdentity, target, + Collections.singletonList(new BlockReverseDiff(targetMeta, Collections.emptyList(), + delta.getMutationViewDigest())), Collections.emptyList()); + } + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + CommonCheckpointMaterializedStore materializedStore = + new CommonCheckpointMaterializedStore(checkpointDirectory); + new CommonCheckpointFile(checkpointDirectory).publish(payload); + new ChainbaseCheckpointMaterializer(checkpointDirectory, formatIdentity, + snapshots.getDbs(), baseline, materializedStore).materialize(payload, target); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.openExisting(pathDirectory, + scope, Engine.LEVELDB)) { + PathStateCheckpointMaterializer interrupted = new PathStateCheckpointMaterializer(stores, + scope, formatIdentity, baseline, materializedStore, failAfterParticipantBatch()); + assertThrows(java.io.IOException.class, + () -> interrupted.materialize(payload, target)); + } + assertThrows(IllegalStateException.class, + () -> PathStatePhysicalOverlayHead.openCommonCheckpoint(pathDirectory, Engine.LEVELDB, + new PathStateLayerLimits(8, 1L << 20), 1L << 20, 2, 2, formatIdentity, + targetMeta, P66Phase.P66_ON)); + + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(102L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(targetId); + when(dynamic.getLatestBlockHeaderTimestamp()).thenReturn(306L); + BlockCapsule targetBlock = block(102L, targetId, firstId, 306L); + when(chainBase.getBlockByNum(102L)).thenReturn(targetBlock); + withCommonConfig(output, "LEVELDB", () -> invoke(manager, "initCommonCheckpoint")); + assertFalse(Files.exists(checkpointDirectory.resolve("COMMON_CHECKPOINT"))); + assertEquals(102L, manager.getPathStateSnapshotHead().getHead().getBlockNumber()); + assertArrayEquals(targetId.getBytes(), + manager.getPathStateSnapshotHead().getHead().getBlockHash()); + invoke(manager, "closeCommonCheckpoint"); + invoke(manager, "closePathStateRoot"); + + withCommonConfig(output, "LEVELDB", () -> invoke(manager, "initCommonCheckpoint")); + assertEquals(102L, manager.getPathStateSnapshotHead().getHead().getBlockNumber()); + assertFalse(Files.exists(checkpointDirectory.resolve("COMMON_CHECKPOINT"))); + invoke(manager, "closeCommonCheckpoint"); + invoke(manager, "closePathStateRoot"); + } + @Test public void commonCheckpointAdoptsCompatibleLegacyCurrentWithoutRebuild() throws Exception { Path output = temporaryFolder.newFolder("common-checkpoint-legacy-current").toPath(); @@ -492,6 +609,23 @@ private static Chainbase emptyNativeStore(String dbName, long blockNumber, byte[ return new Chainbase(new SnapshotRoot(database)); } + private static BlockCapsule block(long number, BlockId id, Sha256Hash parent, long timestamp) { + BlockCapsule block = mock(BlockCapsule.class); + when(block.getNum()).thenReturn(number); + when(block.getBlockId()).thenReturn(id); + when(block.getParentHash()).thenReturn(parent); + when(block.getTimeStamp()).thenReturn(timestamp); + return block; + } + + private static PathStateCheckpointMaterializer.FaultHook failAfterParticipantBatch() { + return (stage, storeId) -> { + if (stage == PathStateCheckpointMaterializer.Stage.AFTER_PARTICIPANT_BATCH) { + throw new java.io.IOException("simulated process death during PathState"); + } + }; + } + private static void withConfig(Path output, boolean enabled, ThrowingRunnable action) throws Exception { CommonParameter args = CommonParameter.getInstance(); @@ -514,6 +648,11 @@ private static void withConfig(Path output, boolean enabled, ThrowingRunnable ac } private static void withCommonConfig(Path output, ThrowingRunnable action) throws Exception { + withCommonConfig(output, "ROCKSDB", action); + } + + private static void withCommonConfig(Path output, String engine, ThrowingRunnable action) + throws Exception { CommonParameter args = CommonParameter.getInstance(); Storage oldStorage = args.getStorage(); String oldOutput = args.outputDirectory; @@ -521,7 +660,7 @@ private static void withCommonConfig(Path output, ThrowingRunnable action) throw Storage storage = new Storage(); args.outputDirectory = output.toString(); args.storage = storage; - storage.setDbEngine("ROCKSDB"); + storage.setDbEngine(engine); storage.setStateArchiveEnabled(true); storage.setStateArchiveDirectory("state-archive"); storage.setCommonCheckpointEnabled(true); From 2103d17a4296a1027393cd778c3c3fe7ffe57652 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 8 Sep 2026 01:03:49 +0800 Subject: [PATCH 124/161] feat(chainbase): add state archive hot store Add independent current and frozen generation storage with default-off common checkpoint v2 bindings.\n\nRecover prepared tails from the common WAL or persisted Chainbase identity, and wire dual-gated Manager startup with crash and restart coverage. --- .../StateArchiveHotBatchDescriptor.java | 237 +++ .../StateArchiveHotBatchDescriptorCodec.java | 163 ++ ...StateArchiveHotCheckpointMaterializer.java | 122 ++ .../db2/archive/StateArchiveHotStore.java | 1616 +++++++++++++++++ .../archive/StateArchiveIndexDatabase.java | 38 +- .../db2/core/CommonCheckpointCapture.java | 60 + .../db2/core/CommonCheckpointHotRecovery.java | 181 ++ .../db2/core/CommonCheckpointPayload.java | 184 +- .../core/CommonCheckpointPayloadCodec.java | 115 +- .../core/CommonCheckpointPayloadFactory.java | 19 + .../CommonCheckpointRecoveryStateAdapter.java | 66 + .../core/CommonCheckpointRedoCoordinator.java | 9 + .../db2/core/CommonCheckpointRuntime.java | 74 +- .../core/CommonCheckpointRuntimeOwner.java | 5 + .../core/db2/core/CommonCheckpointTarget.java | 19 +- .../core/store/DynamicPropertiesStore.java | 15 + .../org/tron/core/config/args/Storage.java | 4 + .../tron/core/config/args/StorageConfig.java | 38 + common/src/main/resources/reference.conf | 26 + .../core/config/args/StorageConfigTest.java | 41 +- .../java/org/tron/core/config/args/Args.java | 1 + .../main/java/org/tron/core/db/Manager.java | 94 +- .../tron/core/config/args/StorageTest.java | 3 + ...eArchiveHotCheckpointMaterializerTest.java | 177 ++ .../StateArchiveHotProcessRecoveryTest.java | 129 ++ .../db2/archive/StateArchiveHotStoreTest.java | 415 +++++ .../ChainbaseCheckpointMaterializerTest.java | 153 +- .../core/CommonCheckpointHotRecoveryTest.java | 224 +++ .../core/CommonCheckpointPayloadV2Test.java | 144 ++ ...monCheckpointRecoveryStateAdapterTest.java | 84 + ...athStateManagerStartupIntegrationTest.java | 60 + 31 files changed, 4467 insertions(+), 49 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptor.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptorCodec.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializer.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointCapture.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointHotRecovery.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRecoveryStateAdapter.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializerTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/core/CommonCheckpointHotRecoveryTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/core/CommonCheckpointPayloadV2Test.java create mode 100644 framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRecoveryStateAdapterTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptor.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptor.java new file mode 100644 index 00000000000..3eb70ecfda1 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptor.java @@ -0,0 +1,237 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Immutable logical binding between transient Archive diffs and one Hot DB prepare batch. */ +public final class StateArchiveHotBatchDescriptor { + + public static final int HOT_FORMAT_VERSION = 1; + private static final int DIGEST_LENGTH = 32; + + private final Engine engine; + private final long parentPublishedBlock; + private final byte[] parentPublishedHash; + private final BlockSnapshotMeta firstBlock; + private final BlockSnapshotMeta lastBlock; + private final long encodedBytes; + private final byte[] parentContentDigest; + private final byte[] resultContentDigest; + private final byte[] orderedRecordDigest; + private final byte[] mutationViewRangeDigest; + private final List blocks; + + StateArchiveHotBatchDescriptor(Engine engine, long parentPublishedBlock, + byte[] parentPublishedHash, BlockSnapshotMeta firstBlock, BlockSnapshotMeta lastBlock, + long encodedBytes, byte[] parentContentDigest, byte[] resultContentDigest, + byte[] orderedRecordDigest, byte[] mutationViewRangeDigest, List blocks) { + this.engine = Objects.requireNonNull(engine, "engine"); + if (parentPublishedBlock < 0 || encodedBytes <= 0) { + throw new IllegalArgumentException("Hot Archive batch counters are invalid"); + } + this.parentPublishedBlock = parentPublishedBlock; + this.parentPublishedHash = digest(parentPublishedHash, "parentPublishedHash"); + this.firstBlock = Objects.requireNonNull(firstBlock, "firstBlock"); + this.lastBlock = Objects.requireNonNull(lastBlock, "lastBlock"); + this.encodedBytes = encodedBytes; + this.parentContentDigest = digest(parentContentDigest, "parentContentDigest"); + this.resultContentDigest = digest(resultContentDigest, "resultContentDigest"); + this.orderedRecordDigest = digest(orderedRecordDigest, "orderedRecordDigest"); + this.mutationViewRangeDigest = digest(mutationViewRangeDigest, + "mutationViewRangeDigest"); + List admitted = new ArrayList<>(Objects.requireNonNull(blocks, "blocks")); + if (admitted.isEmpty() || admitted.size() != lastBlock.getBlockNumber() + - firstBlock.getBlockNumber() + 1L + || firstBlock.getBlockNumber() != parentPublishedBlock + 1 + || !firstBlock.equals(admitted.get(0).meta) + || !lastBlock.equals(admitted.get(admitted.size() - 1).meta)) { + throw new IllegalArgumentException("Hot Archive batch block range is invalid"); + } + BlockDigest previous = null; + Hasher orderedRecords = Hashing.sha256().newHasher(); + Hasher mutationViews = Hashing.sha256().newHasher(); + for (BlockDigest block : admitted) { + BlockDigest current = Objects.requireNonNull(block, "block"); + if (current.meta.getEpoch() != current.meta.getBlockNumber()) { + throw new IllegalArgumentException("Hot Archive batch requires block epochs"); + } else if (previous == null) { + if (!Arrays.equals(current.meta.getParentHash(), this.parentPublishedHash)) { + throw new IllegalArgumentException("Hot Archive batch parent hash differs"); + } + } else if (current.meta.getBlockNumber() != previous.meta.getBlockNumber() + 1 + || !Arrays.equals(current.meta.getParentHash(), previous.meta.getBlockHash())) { + throw new IllegalArgumentException("Hot Archive batch block chain is not consecutive"); + } + orderedRecords.putLong(current.meta.getBlockNumber()) + .putBytes(current.archiveRecordDigest); + mutationViews.putLong(current.meta.getBlockNumber()) + .putBytes(current.mutationViewDigest); + previous = current; + } + if (!Arrays.equals(this.orderedRecordDigest, orderedRecords.hash().asBytes()) + || !Arrays.equals(this.mutationViewRangeDigest, mutationViews.hash().asBytes())) { + throw new IllegalArgumentException("Hot Archive batch aggregate digest differs"); + } + this.blocks = Collections.unmodifiableList(admitted); + } + + /** Reconstructs and validates a descriptor decoded from a coordination payload. */ + public static StateArchiveHotBatchDescriptor restore(Engine engine, + long parentPublishedBlock, byte[] parentPublishedHash, BlockSnapshotMeta firstBlock, + BlockSnapshotMeta lastBlock, long encodedBytes, byte[] parentContentDigest, + byte[] resultContentDigest, byte[] orderedRecordDigest, + byte[] mutationViewRangeDigest, List blocks) { + return new StateArchiveHotBatchDescriptor(engine, parentPublishedBlock, + parentPublishedHash, firstBlock, lastBlock, encodedBytes, parentContentDigest, + resultContentDigest, orderedRecordDigest, mutationViewRangeDigest, blocks); + } + + public Engine getEngine() { + return engine; + } + + public long getParentPublishedBlock() { + return parentPublishedBlock; + } + + public byte[] getParentPublishedHash() { + return copy(parentPublishedHash); + } + + public BlockSnapshotMeta getFirstBlock() { + return firstBlock; + } + + public BlockSnapshotMeta getLastBlock() { + return lastBlock; + } + + public long getBlockCount() { + return blocks.size(); + } + + public long getEncodedBytes() { + return encodedBytes; + } + + public byte[] getParentContentDigest() { + return copy(parentContentDigest); + } + + public byte[] getResultContentDigest() { + return copy(resultContentDigest); + } + + public byte[] getOrderedRecordDigest() { + return copy(orderedRecordDigest); + } + + public byte[] getMutationViewRangeDigest() { + return copy(mutationViewRangeDigest); + } + + public List getBlocks() { + return blocks; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof StateArchiveHotBatchDescriptor)) { + return false; + } + StateArchiveHotBatchDescriptor that = (StateArchiveHotBatchDescriptor) object; + return engine == that.engine + && parentPublishedBlock == that.parentPublishedBlock + && encodedBytes == that.encodedBytes + && firstBlock.equals(that.firstBlock) + && lastBlock.equals(that.lastBlock) + && blocks.equals(that.blocks) + && Arrays.equals(parentPublishedHash, that.parentPublishedHash) + && Arrays.equals(parentContentDigest, that.parentContentDigest) + && Arrays.equals(resultContentDigest, that.resultContentDigest) + && Arrays.equals(orderedRecordDigest, that.orderedRecordDigest) + && Arrays.equals(mutationViewRangeDigest, that.mutationViewRangeDigest); + } + + @Override + public int hashCode() { + int result = Objects.hash(engine, parentPublishedBlock, firstBlock, lastBlock, encodedBytes, + blocks); + result = 31 * result + Arrays.hashCode(parentPublishedHash); + result = 31 * result + Arrays.hashCode(parentContentDigest); + result = 31 * result + Arrays.hashCode(resultContentDigest); + result = 31 * result + Arrays.hashCode(orderedRecordDigest); + result = 31 * result + Arrays.hashCode(mutationViewRangeDigest); + return result; + } + + private static byte[] digest(byte[] value, String name) { + byte[] admitted = copy(Objects.requireNonNull(value, name)); + if (admitted.length != DIGEST_LENGTH) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return admitted; + } + + private static byte[] copy(byte[] value) { + return Arrays.copyOf(value, value.length); + } + + /** Digest-only per-block Archive identity retained by the coordination payload. */ + public static final class BlockDigest { + private final BlockSnapshotMeta meta; + private final byte[] mutationViewDigest; + private final byte[] archiveRecordDigest; + + BlockDigest(BlockSnapshotMeta meta, byte[] mutationViewDigest, byte[] archiveRecordDigest) { + this.meta = Objects.requireNonNull(meta, "meta"); + this.mutationViewDigest = digest(mutationViewDigest, "mutationViewDigest"); + this.archiveRecordDigest = digest(archiveRecordDigest, "archiveRecordDigest"); + } + + public static BlockDigest restore(BlockSnapshotMeta meta, byte[] mutationViewDigest, + byte[] archiveRecordDigest) { + return new BlockDigest(meta, mutationViewDigest, archiveRecordDigest); + } + + public BlockSnapshotMeta getMeta() { + return meta; + } + + public byte[] getMutationViewDigest() { + return copy(mutationViewDigest); + } + + public byte[] getArchiveRecordDigest() { + return copy(archiveRecordDigest); + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof BlockDigest)) { + return false; + } + BlockDigest that = (BlockDigest) object; + return meta.equals(that.meta) + && Arrays.equals(mutationViewDigest, that.mutationViewDigest) + && Arrays.equals(archiveRecordDigest, that.archiveRecordDigest); + } + + @Override + public int hashCode() { + int result = meta.hashCode(); + result = 31 * result + Arrays.hashCode(mutationViewDigest); + result = 31 * result + Arrays.hashCode(archiveRecordDigest); + return result; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptorCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptorCodec.java new file mode 100644 index 00000000000..1d3b8c8934a --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptorCodec.java @@ -0,0 +1,163 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.tron.core.db2.archive.StateArchiveHotBatchDescriptor.BlockDigest; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Checksummed persistent encoding for one exact Hot Archive batch descriptor. */ +final class StateArchiveHotBatchDescriptorCodec { + + private static final int MAGIC = 0x53414844; // SAHD + private static final short VERSION = 1; + private static final int DIGEST_LENGTH = 32; + private static final int HEADER_LENGTH = 44; + private static final int MAX_BLOCKS = 100_000; + private static final int MAX_ENCODED_LENGTH = 32 * 1024 * 1024; + + byte[] encode(StateArchiveHotBatchDescriptor descriptor) { + try { + ByteArrayOutputStream bodyBytes = new ByteArrayOutputStream(); + DataOutputStream body = new DataOutputStream(bodyBytes); + body.writeShort(StateArchiveHotBatchDescriptor.HOT_FORMAT_VERSION); + body.writeShort(engineTag(descriptor.getEngine())); + body.writeLong(descriptor.getParentPublishedBlock()); + body.write(descriptor.getParentPublishedHash()); + writeMeta(body, descriptor.getFirstBlock()); + writeMeta(body, descriptor.getLastBlock()); + body.writeLong(descriptor.getBlockCount()); + body.writeLong(descriptor.getEncodedBytes()); + body.write(descriptor.getParentContentDigest()); + body.write(descriptor.getResultContentDigest()); + body.write(descriptor.getOrderedRecordDigest()); + body.write(descriptor.getMutationViewRangeDigest()); + for (BlockDigest block : descriptor.getBlocks()) { + writeMeta(body, block.getMeta()); + body.write(block.getMutationViewDigest()); + body.write(block.getArchiveRecordDigest()); + } + body.flush(); + byte[] payload = bodyBytes.toByteArray(); + ByteArrayOutputStream encodedBytes = new ByteArrayOutputStream(HEADER_LENGTH + + payload.length); + DataOutputStream encoded = new DataOutputStream(encodedBytes); + encoded.writeInt(MAGIC); + encoded.writeShort(VERSION); + encoded.writeShort(0); + encoded.writeInt(payload.length); + encoded.write(Hashing.sha256().hashBytes(payload).asBytes()); + encoded.write(payload); + encoded.flush(); + return encodedBytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("in-memory Hot descriptor encoding failed", impossible); + } + } + + StateArchiveHotBatchDescriptor decode(byte[] supplied) throws IOException { + if (supplied == null || supplied.length < HEADER_LENGTH + || supplied.length > MAX_ENCODED_LENGTH) { + throw new ArchivePersistenceException("Hot Archive descriptor length is invalid"); + } + byte[] encoded = Arrays.copyOf(supplied, supplied.length); + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded))) { + if (input.readInt() != MAGIC || input.readShort() != VERSION || input.readShort() != 0) { + throw new ArchivePersistenceException("Hot Archive descriptor format is unsupported"); + } + int bodyLength = input.readInt(); + byte[] checksum = readExact(input, DIGEST_LENGTH); + if (bodyLength < 0 || HEADER_LENGTH + (long) bodyLength != encoded.length) { + throw new ArchivePersistenceException("Hot Archive descriptor length is invalid"); + } + byte[] body = readExact(input, bodyLength); + if (!Arrays.equals(checksum, Hashing.sha256().hashBytes(body).asBytes())) { + throw new ArchivePersistenceException("Hot Archive descriptor checksum differs"); + } + return decodeBody(body); + } catch (EOFException truncated) { + throw new ArchivePersistenceException("Hot Archive descriptor is truncated", truncated); + } + } + + private StateArchiveHotBatchDescriptor decodeBody(byte[] body) throws IOException { + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(body))) { + if (input.readUnsignedShort() != StateArchiveHotBatchDescriptor.HOT_FORMAT_VERSION) { + throw new ArchivePersistenceException("Hot Archive descriptor version differs"); + } + Engine engine = engine(input.readUnsignedShort()); + long parentBlock = input.readLong(); + byte[] parentHash = readExact(input, DIGEST_LENGTH); + BlockSnapshotMeta first = readMeta(input); + BlockSnapshotMeta last = readMeta(input); + long blockCount = input.readLong(); + long encodedBytes = input.readLong(); + byte[] parentContent = readExact(input, DIGEST_LENGTH); + byte[] resultContent = readExact(input, DIGEST_LENGTH); + byte[] orderedRecords = readExact(input, DIGEST_LENGTH); + byte[] mutationViews = readExact(input, DIGEST_LENGTH); + if (blockCount <= 0 || blockCount > MAX_BLOCKS) { + throw new ArchivePersistenceException("Hot Archive descriptor block count is invalid"); + } + List blocks = new ArrayList<>((int) blockCount); + for (long index = 0; index < blockCount; index++) { + BlockSnapshotMeta meta = readMeta(input); + blocks.add(BlockDigest.restore(meta, readExact(input, DIGEST_LENGTH), + readExact(input, DIGEST_LENGTH))); + } + if (input.available() != 0) { + throw new ArchivePersistenceException("Hot Archive descriptor has trailing bytes"); + } + try { + return StateArchiveHotBatchDescriptor.restore(engine, parentBlock, parentHash, first, + last, encodedBytes, parentContent, resultContent, orderedRecords, mutationViews, + blocks); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Hot Archive descriptor is inconsistent", invalid); + } + } catch (EOFException truncated) { + throw new ArchivePersistenceException("Hot Archive descriptor is truncated", truncated); + } + } + + private static void writeMeta(DataOutputStream output, BlockSnapshotMeta meta) + throws IOException { + output.writeLong(meta.getEpoch()); + output.writeLong(meta.getBlockNumber()); + output.write(meta.getBlockHash()); + output.write(meta.getParentHash()); + output.writeLong(meta.getTimestamp()); + } + + private static BlockSnapshotMeta readMeta(DataInputStream input) throws IOException { + return new BlockSnapshotMeta(input.readLong(), input.readLong(), + readExact(input, DIGEST_LENGTH), readExact(input, DIGEST_LENGTH), input.readLong()); + } + + private static byte[] readExact(DataInputStream input, int length) throws IOException { + byte[] value = new byte[length]; + input.readFully(value); + return value; + } + + private static int engineTag(Engine engine) { + return engine == Engine.LEVELDB ? 1 : 2; + } + + private static Engine engine(int tag) throws IOException { + if (tag == 1) { + return Engine.LEVELDB; + } + if (tag == 2) { + return Engine.ROCKSDB; + } + throw new ArchivePersistenceException("Hot Archive descriptor engine is unsupported"); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializer.java new file mode 100644 index 00000000000..ae237b3bc24 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializer.java @@ -0,0 +1,122 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.core.CommonCheckpointCapture; +import org.tron.core.db2.core.CommonCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointTarget; + +/** Default-off State Archive participant backed only by the independent Hot DB. */ +public final class StateArchiveHotCheckpointMaterializer + implements CommonCheckpointMaterializer { + + private final StateArchiveHotStore hotStore; + + public StateArchiveHotCheckpointMaterializer(StateArchiveHotStore hotStore) { + this.hotStore = Objects.requireNonNull(hotStore, "hotStore"); + } + + /** Computes the exact Hot batch identity without writing bodies or checkpoint metadata. */ + public synchronized StateArchiveHotBatchDescriptor planCheckpoint( + List diffs) throws IOException { + return hotStore.planCheckpoint(Objects.requireNonNull(diffs, "diffs")); + } + + /** Prepares one capture whose payload, descriptor and transient bodies share one identity. */ + public synchronized CommonCheckpointTarget prepare(CommonCheckpointCapture capture) + throws IOException { + CommonCheckpointCapture admitted = Objects.requireNonNull(capture, "capture"); + CommonCheckpointTarget target = CommonCheckpointTarget.from(admitted.getPayload()); + prepare(target, admitted.getArchiveBinding(), admitted.getArchiveDiffs()); + return target; + } + + synchronized void prepare(CommonCheckpointTarget target, List diffs) + throws IOException { + if (inspect(Objects.requireNonNull(target, "target")) != Status.NEEDS_MATERIALIZATION) { + hotStore.prepareCheckpoint(target.getPayloadDigest(), diffs); + return; + } + prepare(target, hotStore.planCheckpoint(diffs), diffs); + } + + /** Prepares transient bodies only when their computed identity equals the v2 binding. */ + synchronized void prepare(CommonCheckpointTarget target, + StateArchiveHotBatchDescriptor descriptor, List diffs) + throws IOException { + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + List admittedDiffs = Objects.requireNonNull(diffs, "diffs"); + if (admittedDiffs.isEmpty() + || !admitted.getFirstBlock().equals(admittedDiffs.get(0).getMeta()) + || !admitted.getLastBlock().equals( + admittedDiffs.get(admittedDiffs.size() - 1).getMeta())) { + throw new ArchivePersistenceException( + "Hot Archive checkpoint block range differs from target"); + } + hotStore.requireFormatIdentity(admitted.getFormatIdentity()); + hotStore.prepareCheckpoint(admitted.getPayloadDigest(), descriptor, admittedDiffs); + } + + /** Reconciles only an unpublished Hot DB tail to a caller-validated recovery authority. */ + public synchronized long reconcilePreparedTail(BlockSnapshotMeta authority) + throws IOException { + return hotStore.reconcilePreparedTail(authority); + } + + @Override + public Authority authority() { + return Authority.STATE_ARCHIVE; + } + + @Override + public synchronized Status inspect(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + hotStore.requireFormatIdentity(admitted.getFormatIdentity()); + StateArchiveHotStore.HotCheckpointStatus status = hotStore.inspectCheckpoint( + admitted.getPayloadDigest(), admitted.getArchiveBinding()); + switch (status) { + case MATERIALIZED: + return Status.MATERIALIZED; + case PUBLISHED: + return Status.PUBLISHED; + default: + return Status.NEEDS_MATERIALIZATION; + } + } + + /** + * The common coordinator may verify an already prepared target, but must never ask its WAL + * payload to materialize Archive bodies. + */ + @Override + public synchronized void materialize(CommonCheckpointPayload payload, + CommonCheckpointTarget target) throws IOException { + CommonCheckpointPayload admittedPayload = Objects.requireNonNull(payload, "payload"); + CommonCheckpointTarget admittedTarget = Objects.requireNonNull(target, "target"); + if (!admittedTarget.equals(CommonCheckpointTarget.from(admittedPayload))) { + throw new IOException("Hot Archive checkpoint payload and target differ"); + } + if (admittedPayload.getVersion() + != CommonCheckpointPayload.COORDINATION_FORMAT_VERSION) { + throw new IOException("Hot Archive materializer requires coordination payload v2"); + } + if (inspect(admittedTarget) == Status.NEEDS_MATERIALIZATION) { + throw new IOException( + "Hot Archive target must be prepared before common checkpoint WAL publication"); + } + } + + @Override + public synchronized void publish(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + hotStore.requireFormatIdentity(admitted.getFormatIdentity()); + hotStore.publishCheckpoint(admitted.getPayloadDigest(), admitted.getArchiveBinding()); + } + + @Override + public synchronized void close() throws IOException { + hotStore.close(); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java new file mode 100644 index 00000000000..257cf5a2efb --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java @@ -0,0 +1,1616 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import com.google.common.hash.Hasher; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.tron.core.config.args.StorageConfig.NativeDbConfig; +import org.tron.core.config.args.StorageConfig.StateArchiveHotStoreConfig; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** + * Independent hot State Archive database with one writable and bounded sealed generations. + * + *

This store owns history data and native WAL durability. It deliberately has no dependency on + * the common-checkpoint WAL; a later coordinator integration may only consume its durable metadata. + */ +public final class StateArchiveHotStore implements Closeable { + + static final String CURRENT = "CURRENT"; + static final String CURRENT_TEMP = "CURRENT.tmp"; + static final String GENERATIONS = "generations"; + static final String DATABASE = "keys"; + + private static final int HASH_LENGTH = 32; + private static final int CATALOG_MAGIC = 0x53414843; // SAHC + private static final short CATALOG_VERSION = 1; + private static final int CATALOG_LENGTH = Integer.BYTES + 2 * Short.BYTES + Long.BYTES + + HASH_LENGTH + Integer.BYTES; + private static final int RECORD_MAGIC = 0x53414842; // SAHB + private static final short RECORD_VERSION = 1; + private static final short RECORD_VIEW_DIGEST = 1; + private static final byte[] BODY_PREFIX = new byte[]{0x42}; + private static final byte[] INDEX_PREFIX = new byte[]{0x4b}; + private static final byte[] META_FORMAT = bytes("meta/format"); + private static final byte[] META_ID = bytes("meta/id"); + private static final byte[] META_BASE_BLOCK = bytes("meta/base-block"); + private static final byte[] META_BASE_HASH = bytes("meta/base-hash"); + private static final byte[] META_START_BLOCK = bytes("meta/start-block"); + private static final byte[] META_END_BLOCK = bytes("meta/end-block"); + private static final byte[] META_HEAD_HASH = bytes("meta/head-hash"); + private static final byte[] META_BLOCK_COUNT = bytes("meta/block-count"); + private static final byte[] META_ENCODED_BYTES = bytes("meta/encoded-bytes"); + private static final byte[] META_CONTENT_DIGEST = bytes("meta/content-digest"); + private static final byte[] META_SEALED = bytes("meta/sealed"); + private static final byte[] META_PUBLISHED_BLOCK = bytes("meta/published-block"); + private static final byte[] META_PUBLISHED_HASH = bytes("meta/published-hash"); + private static final byte[] META_PUBLISHED_CONTENT_DIGEST = + bytes("meta/published-content-digest"); + private static final byte[] META_PUBLISHED_TARGET = bytes("meta/published-target"); + private static final byte[] META_PUBLISHED_DESCRIPTOR = bytes("meta/published-descriptor"); + private static final byte[] META_PREPARED_TARGET = bytes("meta/prepared-target"); + private static final byte[] META_PREPARED_DESCRIPTOR = bytes("meta/prepared-descriptor"); + private static final byte[] META_TRUNCATE_BLOCK = bytes("meta/truncate-block"); + private static final byte[] META_TRUNCATE_HASH = bytes("meta/truncate-hash"); + private static final byte[] ZERO_DIGEST = new byte[HASH_LENGTH]; + + private final Path root; + private final Path generations; + private final byte[] formatIdentity; + private final Engine engine; + private final int maxFrozenGenerations; + private final long maxBlocks; + private final long maxEncodedBytes; + private final int yellowFrozenGenerations; + private final int redFrozenGenerations; + private final NativeDbConfig dbSettings; + private final BlockHistoryCodec historyCodec = new BlockHistoryCodec(); + private final StateArchiveHotBatchDescriptorCodec descriptorCodec = + new StateArchiveHotBatchDescriptorCodec(); + private final FaultHook faultHook; + private final List frozen = new ArrayList<>(); + + private GenerationMeta current; + private StateArchiveIndexDatabase.Writer writer; + private boolean closed; + + private StateArchiveHotStore(Path root, byte[] formatIdentity, Engine engine, + int maxFrozenGenerations, long maxBlocks, long maxEncodedBytes, + int yellowFrozenGenerations, int redFrozenGenerations, NativeDbConfig dbSettings, + FaultHook faultHook) { + this.root = root; + this.generations = root.resolve(GENERATIONS); + this.formatIdentity = copyDigest(formatIdentity, "formatIdentity"); + this.engine = Objects.requireNonNull(engine, "engine"); + this.maxFrozenGenerations = positive(maxFrozenGenerations, "maxFrozenGenerations"); + this.maxBlocks = positive(maxBlocks, "maxBlocks"); + this.maxEncodedBytes = positive(maxEncodedBytes, "maxEncodedBytes"); + this.yellowFrozenGenerations = positive(yellowFrozenGenerations, + "yellowFrozenGenerations"); + this.redFrozenGenerations = positive(redFrozenGenerations, "redFrozenGenerations"); + if (yellowFrozenGenerations > redFrozenGenerations + || redFrozenGenerations > maxFrozenGenerations) { + throw new IllegalArgumentException("frozen watermarks must satisfy yellow <= red <= max"); + } + this.dbSettings = Objects.requireNonNull(dbSettings, "dbSettings"); + this.faultHook = Objects.requireNonNull(faultHook, "faultHook"); + } + + /** Opens the isolated Hot DB with its dedicated, default-off configuration object. */ + public static StateArchiveHotStore openOrCreate(Path root, byte[] formatIdentity, + Engine engine, long baseBlockNumber, byte[] baseBlockHash, + StateArchiveHotStoreConfig config) throws IOException { + Objects.requireNonNull(config, "config"); + config.validate(); + if (!config.isEnabled()) { + throw new IllegalStateException("State Archive Hot DB is disabled"); + } + return openOrCreate(root, formatIdentity, engine, baseBlockNumber, baseBlockHash, + config.getMaxFrozenGenerations(), config.getMaxBlocks(), config.getMaxEncodedBytes(), + config.getYellowFrozenGenerations(), config.getRedFrozenGenerations(), + config.getDbSettings(), stage -> { }); + } + + public static StateArchiveHotStore openOrCreate(Path root, byte[] formatIdentity, + Engine engine, long baseBlockNumber, byte[] baseBlockHash, int maxFrozenGenerations, + long maxBlocks, long maxEncodedBytes) throws IOException { + return openOrCreate(root, formatIdentity, engine, baseBlockNumber, baseBlockHash, + maxFrozenGenerations, maxBlocks, maxEncodedBytes, + Math.max(1, maxFrozenGenerations / 2), maxFrozenGenerations, + NativeDbConfig.large(), stage -> { }); + } + + static StateArchiveHotStore openOrCreate(Path suppliedRoot, byte[] formatIdentity, + Engine engine, long baseBlockNumber, byte[] baseBlockHash, int maxFrozenGenerations, + long maxBlocks, long maxEncodedBytes, FaultHook faultHook) throws IOException { + return openOrCreate(suppliedRoot, formatIdentity, engine, baseBlockNumber, baseBlockHash, + maxFrozenGenerations, maxBlocks, maxEncodedBytes, + Math.max(1, maxFrozenGenerations / 2), maxFrozenGenerations, + NativeDbConfig.large(), faultHook); + } + + private static StateArchiveHotStore openOrCreate(Path suppliedRoot, byte[] formatIdentity, + Engine engine, long baseBlockNumber, byte[] baseBlockHash, int maxFrozenGenerations, + long maxBlocks, long maxEncodedBytes, int yellowFrozenGenerations, + int redFrozenGenerations, NativeDbConfig dbSettings, FaultHook faultHook) throws IOException { + if (baseBlockNumber < 0) { + throw new IllegalArgumentException("baseBlockNumber must not be negative"); + } + byte[] baseHash = copyDigest(baseBlockHash, "baseBlockHash"); + Path root = Objects.requireNonNull(suppliedRoot, "root").toAbsolutePath().normalize(); + StateArchiveHotStore store = new StateArchiveHotStore(root, formatIdentity, engine, + maxFrozenGenerations, maxBlocks, maxEncodedBytes, yellowFrozenGenerations, + redFrozenGenerations, dbSettings, faultHook); + store.initialize(baseBlockNumber, baseHash); + return store; + } + + private void initialize(long baseBlockNumber, byte[] baseBlockHash) throws IOException { + Files.createDirectories(generations); + Catalog catalog; + Path currentFile = root.resolve(CURRENT); + if (Files.isRegularFile(currentFile, LinkOption.NOFOLLOW_LINKS)) { + catalog = loadCatalog(currentFile); + catalog.require(formatIdentity, engine); + } else { + List existing = generationIds(); + if (existing.isEmpty()) { + createGeneration(0, baseBlockNumber, baseBlockHash, ZERO_DIGEST, null); + persistCatalog(0); + catalog = new Catalog(formatIdentity, engine, 0); + } else if (existing.size() == 1 && existing.get(0) == 0) { + GenerationMeta initial = loadGeneration(0); + initial.requireBase(baseBlockNumber, baseBlockHash); + persistCatalog(0); + catalog = new Catalog(formatIdentity, engine, 0); + } else { + throw new ArchivePersistenceException( + "Hot Archive CURRENT is missing with ambiguous generations"); + } + } + + GenerationMeta selected = loadGeneration(catalog.currentGeneration); + if (selected.sealed) { + selected = recoverRotation(selected); + } + if (selected.id == 0) { + selected.requireBase(baseBlockNumber, baseBlockHash); + } + loadAndValidateGenerations(selected); + current = selected; + writer = StateArchiveIndexDatabase.openWriter(generationPath(current.id).resolve(DATABASE), + engine, dbSettings); + resumeTruncateIfPresent(); + } + + /** Atomically appends a contiguous group of solidified block diffs to the current Hot DB. */ + public synchronized void appendSolidified(List diffs) throws IOException { + append(diffs, null, null); + } + + /** Materializes one checkpoint target without advancing the reader-visible published head. */ + public synchronized void prepareCheckpoint(byte[] targetDigest, List diffs) + throws IOException { + byte[] target = copyDigest(targetDigest, "targetDigest"); + if (Objects.requireNonNull(diffs, "diffs").isEmpty()) { + throw new IllegalArgumentException("Hot Archive checkpoint must contain blocks"); + } + HotCheckpointStatus status = inspectCheckpoint(target); + if (status != HotCheckpointStatus.NEEDS_MATERIALIZATION) { + return; + } + prepareCheckpoint(target, planCheckpoint(diffs), diffs); + } + + /** Materializes only when the caller's v2 binding equals a fresh no-write plan. */ + public synchronized void prepareCheckpoint(byte[] targetDigest, + StateArchiveHotBatchDescriptor descriptor, List diffs) + throws IOException { + byte[] target = copyDigest(targetDigest, "targetDigest"); + StateArchiveHotBatchDescriptor supplied = Objects.requireNonNull(descriptor, "descriptor"); + if (inspectCheckpoint(target, supplied) != HotCheckpointStatus.NEEDS_MATERIALIZATION) { + return; + } + if (!supplied.equals(planCheckpoint(diffs))) { + throw new ArchivePersistenceException("Hot Archive checkpoint descriptor differs"); + } + append(diffs, target, supplied); + } + + /** Computes the exact logical and encoded identity of one candidate batch without writing. */ + public synchronized StateArchiveHotBatchDescriptor planCheckpoint(List diffs) + throws IOException { + ensureOpen(); + List admitted = new ArrayList<>(Objects.requireNonNull(diffs, "diffs")); + if (admitted.isEmpty()) { + throw new IllegalArgumentException("Hot Archive checkpoint must contain blocks"); + } + if (current.prepared != null || current.publishedBlock != current.endBlock) { + throw new ArchivePersistenceException( + "Hot Archive cannot plan across an unpublished checkpoint"); + } + long previousBlock = current.publishedBlock; + byte[] previousHash = current.publishedHash; + byte[] resultContentDigest = current.publishedContentDigest; + long encodedBytes = 0; + Hasher orderedRecords = Hashing.sha256().newHasher(); + Hasher mutationViews = Hashing.sha256().newHasher(); + List blocks = new ArrayList<>(); + for (BlockReverseDiff diff : admitted) { + BlockReverseDiff block = Objects.requireNonNull(diff, "diff"); + BlockSnapshotMeta meta = block.getMeta(); + byte[] viewDigest = block.getMutationViewDigest(); + if (meta.getEpoch() != meta.getBlockNumber() + || meta.getBlockNumber() != previousBlock + 1 + || !Arrays.equals(meta.getParentHash(), previousHash) + || viewDigest == null) { + throw new IllegalArgumentException( + "Hot Archive checkpoint identity is not contiguous"); + } + byte[] record = encodeRecord(block); + byte[] recordDigest = Hashing.sha256().hashBytes(record).asBytes(); + encodedBytes = Math.addExact(encodedBytes, record.length); + resultContentDigest = nextContentDigest(resultContentDigest, meta, record); + orderedRecords.putLong(meta.getBlockNumber()).putBytes(recordDigest); + mutationViews.putLong(meta.getBlockNumber()).putBytes(viewDigest); + blocks.add(new StateArchiveHotBatchDescriptor.BlockDigest(meta, viewDigest, recordDigest)); + previousBlock = meta.getBlockNumber(); + previousHash = meta.getBlockHash(); + } + return new StateArchiveHotBatchDescriptor(engine, current.publishedBlock, + current.publishedHash, admitted.get(0).getMeta(), + admitted.get(admitted.size() - 1).getMeta(), encodedBytes, + current.publishedContentDigest, resultContentDigest, + orderedRecords.hash().asBytes(), mutationViews.hash().asBytes(), blocks); + } + + private void append(List diffs, byte[] preparedTarget, + StateArchiveHotBatchDescriptor preparedDescriptor) throws IOException { + ensureOpen(); + Objects.requireNonNull(diffs, "diffs"); + if (diffs.isEmpty()) { + return; + } + if (current.sealed) { + throw new ArchivePersistenceException("Hot Archive current generation is sealed"); + } + if (current.prepared != null) { + throw new ArchivePersistenceException( + "Hot Archive cannot append across a prepared checkpoint target"); + } + + List mutations = new ArrayList<>(); + Set newKeys = new HashSet<>(); + long previousBlock = current.endBlock; + byte[] previousHash = current.headHash; + long startBlock = current.startBlock; + long blockCount = current.blockCount; + long encodedBytes = current.encodedBytes; + byte[] contentDigest = current.contentDigest; + for (BlockReverseDiff diff : diffs) { + Objects.requireNonNull(diff, "diff"); + BlockSnapshotMeta meta = diff.getMeta(); + if (meta.getEpoch() != meta.getBlockNumber()) { + throw new IllegalArgumentException("Hot Archive requires block-boundary epochs"); + } + if (meta.getBlockNumber() != previousBlock + 1 + || !Arrays.equals(meta.getParentHash(), previousHash)) { + throw new IllegalArgumentException("Hot Archive block sequence is not contiguous"); + } + byte[] bodyKey = bodyKey(meta.getBlockNumber()); + requireNewKey(bodyKey, newKeys); + byte[] record = encodeRecord(diff); + mutations.add(StateArchiveIndexDatabase.put(bodyKey, record)); + for (DbGroup group : diff.getGroups()) { + for (Entry entry : group.getEntries()) { + byte[] indexKey = indexKey(group.getDbName(), entry.getKey(), meta.getBlockNumber()); + requireNewKey(indexKey, newKeys); + mutations.add(StateArchiveIndexDatabase.put(indexKey, longBytes(meta.getBlockNumber()))); + } + } + if (startBlock < 0) { + startBlock = meta.getBlockNumber(); + } + previousBlock = meta.getBlockNumber(); + previousHash = meta.getBlockHash(); + blockCount++; + encodedBytes = Math.addExact(encodedBytes, record.length); + contentDigest = nextContentDigest(contentDigest, meta, record); + } + mutations.add(StateArchiveIndexDatabase.put(META_START_BLOCK, longBytes(startBlock))); + mutations.add(StateArchiveIndexDatabase.put(META_END_BLOCK, longBytes(previousBlock))); + mutations.add(StateArchiveIndexDatabase.put(META_HEAD_HASH, previousHash)); + mutations.add(StateArchiveIndexDatabase.put(META_BLOCK_COUNT, longBytes(blockCount))); + mutations.add(StateArchiveIndexDatabase.put(META_ENCODED_BYTES, longBytes(encodedBytes))); + mutations.add(StateArchiveIndexDatabase.put(META_CONTENT_DIGEST, contentDigest)); + PreparedTarget prepared = null; + long publishedBlock = previousBlock; + byte[] publishedHash = previousHash; + byte[] publishedContentDigest = contentDigest; + byte[] publishedTarget = current.publishedTarget; + StateArchiveHotBatchDescriptor publishedDescriptor = current.publishedDescriptor; + if (preparedTarget != null) { + prepared = new PreparedTarget(preparedTarget, + Objects.requireNonNull(preparedDescriptor, "preparedDescriptor")); + publishedBlock = current.publishedBlock; + publishedHash = current.publishedHash; + publishedContentDigest = current.publishedContentDigest; + mutations.add(StateArchiveIndexDatabase.put(META_PREPARED_TARGET, prepared.targetDigest)); + mutations.add(StateArchiveIndexDatabase.put(META_PREPARED_DESCRIPTOR, + descriptorCodec.encode(prepared.descriptor))); + } else { + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_BLOCK, + longBytes(publishedBlock))); + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_HASH, publishedHash)); + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_CONTENT_DIGEST, + publishedContentDigest)); + } + if (prepared != null + && (blockCount >= maxBlocks || encodedBytes >= maxEncodedBytes) + && frozen.size() >= maxFrozenGenerations) { + throw new ArchivePersistenceException( + "Hot Archive cannot prepare a rotation with a full frozen backlog"); + } + writer.write(mutations, true); + current = new GenerationMeta(current.id, current.baseBlock, current.baseHash, startBlock, + previousBlock, previousHash, blockCount, encodedBytes, contentDigest, false, + publishedBlock, publishedHash, publishedContentDigest, publishedTarget, + publishedDescriptor, prepared); + if (prepared != null) { + faultHook.after(Stage.AFTER_PREPARE); + } + } + + /** Atomically publishes an already materialized exact checkpoint target. */ + public synchronized void publishCheckpoint(byte[] targetDigest) throws IOException { + ensureOpen(); + byte[] target = copyDigest(targetDigest, "targetDigest"); + HotCheckpointStatus status = inspectCheckpoint(target); + if (status == HotCheckpointStatus.PUBLISHED) { + rotatePublishedCurrentIfDue(); + return; + } + if (status != HotCheckpointStatus.MATERIALIZED || current.prepared == null) { + throw new ArchivePersistenceException( + "Hot Archive checkpoint target is not materialized"); + } + List mutations = Arrays.asList( + StateArchiveIndexDatabase.put(META_PUBLISHED_BLOCK, longBytes(current.endBlock)), + StateArchiveIndexDatabase.put(META_PUBLISHED_HASH, current.headHash), + StateArchiveIndexDatabase.put(META_PUBLISHED_CONTENT_DIGEST, current.contentDigest), + StateArchiveIndexDatabase.put(META_PUBLISHED_TARGET, target), + StateArchiveIndexDatabase.put(META_PUBLISHED_DESCRIPTOR, + descriptorCodec.encode(current.prepared.descriptor)), + StateArchiveIndexDatabase.delete(META_PREPARED_TARGET), + StateArchiveIndexDatabase.delete(META_PREPARED_DESCRIPTOR)); + writer.write(mutations, true); + current = current.published(target, current.prepared.descriptor); + faultHook.after(Stage.AFTER_PUBLISH); + rotatePublishedCurrentIfDue(); + } + + synchronized void requireFormatIdentity(byte[] expected) throws IOException { + ensureOpen(); + if (!Arrays.equals(formatIdentity, copyDigest(expected, "formatIdentity"))) { + throw new ArchivePersistenceException("Hot Archive checkpoint format differs"); + } + } + + private void rotatePublishedCurrentIfDue() throws IOException { + if (shouldSealCurrent()) { + sealCurrent(); + } + } + + public synchronized HotCheckpointStatus inspectCheckpoint(byte[] targetDigest) + throws IOException { + ensureOpen(); + byte[] target = copyDigest(targetDigest, "targetDigest"); + if (Arrays.equals(current.publishedTarget, target)) { + return HotCheckpointStatus.PUBLISHED; + } + if (current.prepared != null) { + if (Arrays.equals(current.prepared.targetDigest, target)) { + return HotCheckpointStatus.MATERIALIZED; + } + throw new ArchivePersistenceException( + "Hot Archive prepared checkpoint target differs"); + } + return HotCheckpointStatus.NEEDS_MATERIALIZATION; + } + + public synchronized HotCheckpointStatus inspectCheckpoint(byte[] targetDigest, + StateArchiveHotBatchDescriptor descriptor) throws IOException { + StateArchiveHotBatchDescriptor expected = Objects.requireNonNull(descriptor, "descriptor"); + HotCheckpointStatus status = inspectCheckpoint(targetDigest); + if (status == HotCheckpointStatus.MATERIALIZED + && !expected.equals(current.prepared.descriptor)) { + throw new ArchivePersistenceException("Hot Archive prepared descriptor differs"); + } + if (status == HotCheckpointStatus.PUBLISHED + && !expected.equals(current.publishedDescriptor)) { + throw new ArchivePersistenceException("Hot Archive published descriptor differs"); + } + return status; + } + + public synchronized void publishCheckpoint(byte[] targetDigest, + StateArchiveHotBatchDescriptor descriptor) throws IOException { + HotCheckpointStatus status = inspectCheckpoint(targetDigest, descriptor); + if (status == HotCheckpointStatus.NEEDS_MATERIALIZATION) { + throw new ArchivePersistenceException("Hot Archive checkpoint target is not materialized"); + } + publishCheckpoint(targetDigest); + } + + /** Seals current and atomically publishes a new writable generation. */ + public synchronized long sealCurrent() throws IOException { + ensureOpen(); + if (current.blockCount == 0) { + throw new IllegalStateException("Hot Archive cannot seal an empty generation"); + } + if (current.prepared != null || current.publishedBlock != current.endBlock) { + throw new ArchivePersistenceException( + "Hot Archive cannot seal an unpublished current generation"); + } + if (frozen.size() >= maxFrozenGenerations) { + throw new ArchivePersistenceException("Hot Archive frozen backlog reached its limit"); + } + writer.write(Collections.singletonList( + StateArchiveIndexDatabase.put(META_SEALED, new byte[]{1})), true); + current = current.sealed(); + writer.close(); + writer = null; + faultHook.after(Stage.AFTER_SEAL); + + long frozenId = current.id; + long nextId = Math.addExact(frozenId, 1); + GenerationMeta next = createOrValidateNext(current, nextId); + faultHook.after(Stage.AFTER_NEW_GENERATION); + persistCatalog(nextId); + faultHook.after(Stage.AFTER_CURRENT); + frozen.add(current); + current = next; + writer = StateArchiveIndexDatabase.openWriter(generationPath(nextId).resolve(DATABASE), + engine, dbSettings); + return frozenId; + } + + public synchronized boolean shouldSealCurrent() { + ensureOpenUnchecked(); + return current.blockCount >= maxBlocks || current.encodedBytes >= maxEncodedBytes; + } + + /** Finds the first changed block after {@code targetBlock} across frozen and current Hot DBs. */ + public synchronized Optional findOldValueAfter(String dbName, byte[] rawKey, + long targetBlock) throws IOException { + ensureOpen(); + Objects.requireNonNull(dbName, "dbName"); + Objects.requireNonNull(rawKey, "rawKey"); + if (targetBlock < -1) { + throw new IllegalArgumentException("targetBlock must not be less than -1"); + } + if (targetBlock == Long.MAX_VALUE) { + return Optional.empty(); + } + byte[] prefix = indexPrefix(dbName, rawKey); + byte[] seek = ByteBuffer.allocate(prefix.length + Long.BYTES).put(prefix) + .putLong(targetBlock + 1).array(); + long candidate = Long.MAX_VALUE; + for (GenerationMeta generation : allGenerations()) { + if (generation.blockCount == 0 || generation.publishedBlock <= targetBlock) { + continue; + } + try (StateArchiveIndexDatabase.Reader reader = StateArchiveIndexDatabase.openReader( + generationPath(generation.id).resolve(DATABASE), engine, dbSettings)) { + StateArchiveIndexDatabase.KeyValue found = reader.seek(seek); + if (found != null && isIndexCandidate(found.getKey(), prefix)) { + long block = ByteBuffer.wrap(found.getKey(), prefix.length, Long.BYTES).getLong(); + if (!Arrays.equals(found.getValue(), longBytes(block))) { + throw new ArchivePersistenceException("Hot Archive index locator is corrupt"); + } + if (block <= generation.publishedBlock) { + candidate = Math.min(candidate, block); + } + } + } + } + if (candidate == Long.MAX_VALUE) { + return Optional.empty(); + } + BlockReverseDiff diff = loadBlock(candidate); + return Optional.of(new HotLookup(candidate, findExactOldValue(diff, dbName, rawKey))); + } + + public synchronized BlockReverseDiff loadBlock(long blockNumber) throws IOException { + ensureOpen(); + for (GenerationMeta generation : allGenerations()) { + if (generation.blockCount == 0 || blockNumber < generation.startBlock + || blockNumber > generation.publishedBlock) { + continue; + } + try (StateArchiveIndexDatabase.Reader reader = StateArchiveIndexDatabase.openReader( + generationPath(generation.id).resolve(DATABASE), engine, dbSettings)) { + byte[] encoded = reader.get(bodyKey(blockNumber)); + if (encoded == null) { + throw new ArchivePersistenceException("Hot Archive body is missing for indexed block"); + } + return decodeRecord(encoded); + } + } + throw new ArchivePersistenceException("Hot Archive block is outside committed coverage"); + } + + public synchronized long getCurrentGenerationId() { + ensureOpenUnchecked(); + return current.id; + } + + public synchronized List getFrozenGenerationIds() { + ensureOpenUnchecked(); + return frozen.stream().map(generation -> generation.id).collect(Collectors.toList()); + } + + public synchronized long getCommittedHead() { + ensureOpenUnchecked(); + return current.publishedBlock; + } + + public synchronized byte[] getCommittedHeadHash() { + ensureOpenUnchecked(); + return Arrays.copyOf(current.publishedHash, current.publishedHash.length); + } + + public synchronized Optional getPublishedTargetDigest() { + ensureOpenUnchecked(); + return current.publishedTarget == null ? Optional.empty() + : Optional.of(Arrays.copyOf(current.publishedTarget, current.publishedTarget.length)); + } + + public synchronized long getMaterializedHead() { + ensureOpenUnchecked(); + return current.endBlock; + } + + /** + * Removes only the writable generation suffix above an externally recovered authority. + * Frozen generations are never changed, and equal-height identity drift fails closed. + */ + public synchronized long reconcilePreparedTail(BlockSnapshotMeta authority) throws IOException { + ensureOpen(); + BlockSnapshotMeta admitted = Objects.requireNonNull(authority, "authority"); + long ceiling = admitted.getBlockNumber(); + if (ceiling < current.baseBlock) { + throw new ArchivePersistenceException( + "Hot Archive recovery ceiling precedes the current generation base"); + } + if (!frozen.isEmpty() && frozen.get(frozen.size() - 1).endBlock > ceiling) { + throw new ArchivePersistenceException( + "Hot Archive recovery cannot truncate a frozen generation"); + } + if (current.publishedBlock > ceiling) { + throw new ArchivePersistenceException( + "Hot Archive recovery cannot truncate published history"); + } + if (current.endBlock < ceiling) { + return 0; + } + requireCurrentIdentity(ceiling, admitted.getBlockHash()); + if (current.endBlock == ceiling) { + return 0; + } + if (current.prepared == null || current.publishedBlock != ceiling) { + throw new ArchivePersistenceException( + "Hot Archive recovery ceiling does not bound one prepared tail"); + } + writer.write(Arrays.asList( + StateArchiveIndexDatabase.put(META_TRUNCATE_BLOCK, longBytes(ceiling)), + StateArchiveIndexDatabase.put(META_TRUNCATE_HASH, admitted.getBlockHash())), true); + faultHook.after(Stage.AFTER_TRUNCATE_INTENT); + return completeTruncate(new RecoveryCeiling(ceiling, admitted.getBlockHash())); + } + + /** Returns a self-consistent Hot DB capacity and frozen-backlog snapshot. */ + public synchronized Statistics getStatistics() { + ensureOpenUnchecked(); + long frozenBlocks = 0; + long frozenBytes = 0; + for (GenerationMeta generation : frozen) { + frozenBlocks = Math.addExact(frozenBlocks, generation.blockCount); + frozenBytes = Math.addExact(frozenBytes, generation.encodedBytes); + } + int frozenCount = frozen.size(); + BacklogLevel level = frozenCount >= redFrozenGenerations ? BacklogLevel.RED + : frozenCount >= yellowFrozenGenerations ? BacklogLevel.YELLOW : BacklogLevel.GREEN; + return new Statistics(current.id, current.startBlock, current.endBlock, current.blockCount, + current.encodedBytes, frozenCount, frozenBlocks, frozenBytes, + maxFrozenGenerations, yellowFrozenGenerations, redFrozenGenerations, level, + shouldSealCurrent(), maxBlocks, maxEncodedBytes, current.publishedBlock, + current.prepared != null); + } + + @Override + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + if (writer != null) { + writer.close(); + writer = null; + } + } + } + + private GenerationMeta recoverRotation(GenerationMeta sealed) throws IOException { + if (sealed.blockCount == 0) { + throw new ArchivePersistenceException("Hot Archive sealed generation is empty"); + } + long nextId = Math.addExact(sealed.id, 1); + GenerationMeta next = createOrValidateNext(sealed, nextId); + persistCatalog(nextId); + return next; + } + + private void resumeTruncateIfPresent() throws IOException { + byte[] block = writer.get(META_TRUNCATE_BLOCK); + byte[] hash = writer.get(META_TRUNCATE_HASH); + if (block == null && hash == null) { + return; + } + if (block == null || block.length != Long.BYTES || hash == null + || hash.length != HASH_LENGTH) { + throw new ArchivePersistenceException("Hot Archive truncate intent is corrupt"); + } + RecoveryCeiling ceiling = new RecoveryCeiling(ByteBuffer.wrap(block).getLong(), hash); + if (ceiling.blockNumber < current.baseBlock || ceiling.blockNumber >= current.endBlock) { + throw new ArchivePersistenceException("Hot Archive truncate intent range is invalid"); + } + if (current.prepared == null || current.publishedBlock != ceiling.blockNumber) { + throw new ArchivePersistenceException( + "Hot Archive truncate intent does not bound one prepared tail"); + } + requireCurrentIdentity(ceiling.blockNumber, ceiling.blockHash); + completeTruncate(ceiling); + } + + private long completeTruncate(RecoveryCeiling ceiling) throws IOException { + GenerationMeta retained = rebuildCurrentThrough(ceiling.blockNumber) + .published(current.publishedTarget, current.publishedDescriptor); + long removed = current.endBlock - ceiling.blockNumber; + for (long block = current.endBlock; block > ceiling.blockNumber; block--) { + byte[] body = writer.get(bodyKey(block)); + if (body == null) { + continue; + } + BlockReverseDiff diff = decodeRecord(body); + if (diff.getMeta().getBlockNumber() != block) { + throw new ArchivePersistenceException( + "Hot Archive truncate body identity differs"); + } + List deletes = new ArrayList<>(); + for (DbGroup group : diff.getGroups()) { + for (Entry entry : group.getEntries()) { + deletes.add(StateArchiveIndexDatabase.delete( + indexKey(group.getDbName(), entry.getKey(), block))); + } + } + deletes.add(StateArchiveIndexDatabase.delete(bodyKey(block))); + writer.write(deletes, true); + faultHook.after(Stage.AFTER_TRUNCATE_DELETE_BATCH); + } + List publish = retained.mutableMetadata(); + publish.add(StateArchiveIndexDatabase.delete(META_TRUNCATE_BLOCK)); + publish.add(StateArchiveIndexDatabase.delete(META_TRUNCATE_HASH)); + publish.add(StateArchiveIndexDatabase.delete(META_PREPARED_TARGET)); + publish.add(StateArchiveIndexDatabase.delete(META_PREPARED_DESCRIPTOR)); + writer.write(publish, true); + current = retained; + faultHook.after(Stage.AFTER_TRUNCATE_METADATA); + return removed; + } + + private GenerationMeta rebuildCurrentThrough(long ceiling) throws IOException { + GenerationMeta retained = GenerationMeta.empty(current.id, current.baseBlock, + current.baseHash); + for (long block = current.baseBlock + 1; block <= ceiling; block++) { + byte[] record = writer.get(bodyKey(block)); + if (record == null) { + throw new ArchivePersistenceException( + "Hot Archive retained prefix body is missing"); + } + BlockReverseDiff diff = decodeRecord(record); + BlockSnapshotMeta meta = diff.getMeta(); + if (meta.getBlockNumber() != block + || !Arrays.equals(meta.getParentHash(), retained.headHash)) { + throw new ArchivePersistenceException( + "Hot Archive retained prefix identity differs"); + } + retained = retained.appended(meta, record); + } + return retained; + } + + private void requireCurrentIdentity(long blockNumber, byte[] expectedHash) throws IOException { + byte[] actual; + if (blockNumber == current.baseBlock) { + actual = current.baseHash; + } else { + byte[] record = writer.get(bodyKey(blockNumber)); + if (record == null) { + throw new ArchivePersistenceException( + "Hot Archive recovery ceiling body is missing"); + } + actual = decodeRecord(record).getMeta().getBlockHash(); + } + if (!Arrays.equals(actual, expectedHash)) { + throw new ArchivePersistenceException( + "Hot Archive recovery ceiling hash differs"); + } + } + + private GenerationMeta createOrValidateNext(GenerationMeta parent, long nextId) + throws IOException { + Path path = generationPath(nextId); + if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + GenerationMeta existing = loadGeneration(nextId); + existing.requireBase(parent.endBlock, parent.headHash); + existing.requirePublication(parent.publishedTarget, parent.publishedDescriptor); + if (existing.sealed || existing.blockCount != 0) { + throw new ArchivePersistenceException("Hot Archive recovery child is not empty current"); + } + return existing; + } + return createGeneration(nextId, parent.endBlock, parent.headHash, parent.publishedTarget, + parent.publishedDescriptor); + } + + private GenerationMeta createGeneration(long id, long baseBlock, byte[] baseHash, + byte[] publishedTarget, StateArchiveHotBatchDescriptor publishedDescriptor) + throws IOException { + Path path = generationPath(id); + Files.createDirectory(path); + StateArchiveIndexEngineManifest.openOrCreate(path, engine); + GenerationMeta meta = GenerationMeta.empty(id, baseBlock, baseHash) + .published(publishedTarget, publishedDescriptor); + try (StateArchiveIndexDatabase.Writer created = StateArchiveIndexDatabase.openWriter( + path.resolve(DATABASE), engine, dbSettings)) { + created.write(meta.createMutations(formatIdentity), true); + } + HistorySegmentStore.syncDirectory(generations); + return meta; + } + + private void loadAndValidateGenerations(GenerationMeta selected) throws IOException { + frozen.clear(); + List ids = generationIds(); + GenerationMeta previous = null; + boolean foundCurrent = false; + for (long id : ids) { + if (id > selected.id) { + throw new ArchivePersistenceException("Hot Archive has unpublished future generation"); + } + GenerationMeta meta = id == selected.id ? selected : loadGeneration(id); + if (previous != null) { + meta.requireBase(previous.endBlock, previous.headHash); + } + if (id == selected.id) { + if (meta.sealed) { + throw new ArchivePersistenceException("Hot Archive CURRENT points to sealed generation"); + } + foundCurrent = true; + } else { + if (!meta.sealed || meta.blockCount == 0) { + throw new ArchivePersistenceException("Hot Archive frozen generation is not sealed"); + } + frozen.add(meta); + } + previous = meta; + } + if (!foundCurrent || frozen.size() > maxFrozenGenerations) { + throw new ArchivePersistenceException("Hot Archive generation catalog is inconsistent"); + } + } + + private GenerationMeta loadGeneration(long id) throws IOException { + Path path = generationPath(id); + StateArchiveIndexEngineManifest.require(path, engine); + try (StateArchiveIndexDatabase.Reader reader = StateArchiveIndexDatabase.openReader( + path.resolve(DATABASE), engine, dbSettings)) { + byte[] storedFormat = required(reader, META_FORMAT, HASH_LENGTH); + if (!Arrays.equals(formatIdentity, storedFormat)) { + throw new ArchivePersistenceException("Hot Archive generation format differs"); + } + long storedId = readLong(reader, META_ID); + if (storedId != id) { + throw new ArchivePersistenceException("Hot Archive generation id differs"); + } + long baseBlock = readLong(reader, META_BASE_BLOCK); + byte[] baseHash = required(reader, META_BASE_HASH, HASH_LENGTH); + long startBlock = readLong(reader, META_START_BLOCK); + long endBlock = readLong(reader, META_END_BLOCK); + byte[] headHash = required(reader, META_HEAD_HASH, HASH_LENGTH); + long blockCount = readLong(reader, META_BLOCK_COUNT); + long encodedBytes = readLong(reader, META_ENCODED_BYTES); + byte[] contentDigest = required(reader, META_CONTENT_DIGEST, HASH_LENGTH); + byte[] sealed = required(reader, META_SEALED, 1); + long publishedBlock = readLong(reader, META_PUBLISHED_BLOCK); + byte[] publishedHash = required(reader, META_PUBLISHED_HASH, HASH_LENGTH); + byte[] publishedContentDigest = required(reader, META_PUBLISHED_CONTENT_DIGEST, + HASH_LENGTH); + byte[] publishedTarget = required(reader, META_PUBLISHED_TARGET, HASH_LENGTH); + byte[] encodedPublishedDescriptor = reader.get(META_PUBLISHED_DESCRIPTOR); + StateArchiveHotBatchDescriptor publishedDescriptor = encodedPublishedDescriptor == null + ? null : descriptorCodec.decode(encodedPublishedDescriptor); + byte[] preparedTarget = reader.get(META_PREPARED_TARGET); + byte[] encodedPreparedDescriptor = reader.get(META_PREPARED_DESCRIPTOR); + if ((preparedTarget == null) != (encodedPreparedDescriptor == null) + || preparedTarget != null && preparedTarget.length != HASH_LENGTH) { + throw new ArchivePersistenceException("Hot Archive prepared target is corrupt"); + } + PreparedTarget prepared = preparedTarget == null ? null + : new PreparedTarget(preparedTarget, + descriptorCodec.decode(encodedPreparedDescriptor)); + if (sealed[0] != 0 && sealed[0] != 1) { + throw new ArchivePersistenceException("Hot Archive sealed flag is invalid"); + } + GenerationMeta meta = new GenerationMeta(id, baseBlock, baseHash, startBlock, endBlock, + headHash, blockCount, encodedBytes, contentDigest, sealed[0] == 1, + publishedBlock, publishedHash, publishedContentDigest, publishedTarget, + publishedDescriptor, prepared); + meta.validate(); + if (publishedDescriptor != null && publishedDescriptor.getEngine() != engine) { + throw new ArchivePersistenceException("Hot Archive published descriptor engine differs"); + } + if (publishedDescriptor != null && blockCount > 0 + && publishedDescriptor.getFirstBlock().getBlockNumber() >= startBlock) { + requireExactDescriptor(reader, publishedDescriptor); + } + boolean truncateIntent = reader.get(META_TRUNCATE_BLOCK) != null + || reader.get(META_TRUNCATE_HASH) != null; + if (prepared != null && !truncateIntent) { + requireExactDescriptor(reader, prepared.descriptor); + } + return meta; + } + } + + private void requireExactDescriptor(StateArchiveIndexDatabase.Reader reader, + StateArchiveHotBatchDescriptor descriptor) throws IOException { + if (descriptor.getEngine() != engine) { + throw new ArchivePersistenceException("Hot Archive descriptor engine differs"); + } + long encodedBytes = 0; + byte[] contentDigest = descriptor.getParentContentDigest(); + for (StateArchiveHotBatchDescriptor.BlockDigest block : descriptor.getBlocks()) { + byte[] record = reader.get(bodyKey(block.getMeta().getBlockNumber())); + if (record == null) { + throw new ArchivePersistenceException("Hot Archive descriptor body is missing"); + } + BlockReverseDiff diff = decodeRecord(record); + if (!diff.getMeta().equals(block.getMeta()) + || diff.getMutationViewDigest() == null + || !Arrays.equals(diff.getMutationViewDigest(), block.getMutationViewDigest()) + || !Arrays.equals(Hashing.sha256().hashBytes(record).asBytes(), + block.getArchiveRecordDigest())) { + throw new ArchivePersistenceException("Hot Archive descriptor body identity differs"); + } + encodedBytes = Math.addExact(encodedBytes, record.length); + contentDigest = nextContentDigest(contentDigest, diff.getMeta(), record); + } + if (encodedBytes != descriptor.getEncodedBytes() + || !Arrays.equals(contentDigest, descriptor.getResultContentDigest())) { + throw new ArchivePersistenceException("Hot Archive descriptor batch result differs"); + } + } + + private List generationIds() throws IOException { + try (Stream paths = Files.list(generations)) { + List ids = new ArrayList<>(); + for (Path path : paths.collect(Collectors.toList())) { + if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + throw new ArchivePersistenceException("Hot Archive generations contains a non-directory"); + } + String name = path.getFileName().toString(); + if (!name.matches("[0-9]{20}")) { + throw new ArchivePersistenceException("Hot Archive generation name is invalid"); + } + try { + ids.add(Long.parseLong(name)); + } catch (NumberFormatException invalid) { + throw new ArchivePersistenceException("Hot Archive generation id overflows", invalid); + } + } + ids.sort(Comparator.naturalOrder()); + return ids; + } + } + + private List allGenerations() { + List result = new ArrayList<>(frozen); + result.add(current); + return result; + } + + private Path generationPath(long id) { + return generations.resolve(String.format("%020d", id)); + } + + private void persistCatalog(long currentId) throws IOException { + byte[] encoded = new Catalog(formatIdentity, engine, currentId).encode(); + Path temporary = root.resolve(CURRENT_TEMP); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(encoded); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + try { + Files.move(temporary, root.resolve(CURRENT), StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new ArchivePersistenceException( + "Hot Archive filesystem does not support atomic CURRENT publication", unsupported); + } + HistorySegmentStore.syncDirectory(root); + } + + private static Catalog loadCatalog(Path path) throws IOException { + byte[] encoded = Files.readAllBytes(path); + if (encoded.length != CATALOG_LENGTH) { + throw new ArchivePersistenceException("Hot Archive CURRENT length is invalid"); + } + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(encoded, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new ArchivePersistenceException("Hot Archive CURRENT checksum differs"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload))) { + if (input.readInt() != CATALOG_MAGIC || input.readShort() != CATALOG_VERSION) { + throw new ArchivePersistenceException("Hot Archive CURRENT format is unsupported"); + } + Engine engine = engine(input.readUnsignedShort()); + long generation = input.readLong(); + byte[] format = new byte[HASH_LENGTH]; + input.readFully(format); + if (generation < 0 || input.available() != 0) { + throw new ArchivePersistenceException("Hot Archive CURRENT payload is invalid"); + } + return new Catalog(format, engine, generation); + } catch (EOFException truncated) { + throw new ArchivePersistenceException("Hot Archive CURRENT is truncated", truncated); + } + } + + private byte[] encodeRecord(BlockReverseDiff diff) { + try { + byte[] history = historyCodec.encode(diff); + byte[] viewDigest = diff.getMutationViewDigest(); + short flags = viewDigest == null ? 0 : RECORD_VIEW_DIGEST; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(history.length + 48); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(RECORD_MAGIC); + output.writeShort(RECORD_VERSION); + output.writeShort(flags); + if (viewDigest != null) { + output.write(viewDigest); + } + output.writeInt(history.length); + output.write(history); + output.flush(); + byte[] payload = bytes.toByteArray(); + output.writeInt(Hashing.crc32c().hashBytes(payload).asInt()); + output.flush(); + return bytes.toByteArray(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected Hot Archive record encoding failure", impossible); + } + } + + private BlockReverseDiff decodeRecord(byte[] encoded) throws IOException { + if (encoded.length < 16) { + throw new ArchivePersistenceException("Hot Archive record is truncated"); + } + byte[] payload = Arrays.copyOf(encoded, encoded.length - Integer.BYTES); + int checksum = ByteBuffer.wrap(encoded, payload.length, Integer.BYTES).getInt(); + if (checksum != Hashing.crc32c().hashBytes(payload).asInt()) { + throw new ArchivePersistenceException("Hot Archive record checksum differs"); + } + try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload))) { + if (input.readInt() != RECORD_MAGIC || input.readShort() != RECORD_VERSION) { + throw new ArchivePersistenceException("Hot Archive record format is unsupported"); + } + short flags = input.readShort(); + if ((flags & ~RECORD_VIEW_DIGEST) != 0) { + throw new ArchivePersistenceException("Hot Archive record flags are unsupported"); + } + byte[] viewDigest = null; + if ((flags & RECORD_VIEW_DIGEST) != 0) { + viewDigest = new byte[HASH_LENGTH]; + input.readFully(viewDigest); + } + int historyLength = input.readInt(); + if (historyLength <= 0 || historyLength != input.available()) { + throw new ArchivePersistenceException("Hot Archive history length is invalid"); + } + byte[] history = new byte[historyLength]; + input.readFully(history); + BlockReverseDiff decoded; + try { + decoded = historyCodec.decode(history); + } catch (IllegalArgumentException invalid) { + throw new ArchivePersistenceException("Hot Archive history is corrupt", invalid); + } + return new BlockReverseDiff(decoded.getMeta(), decoded.getGroups(), viewDigest); + } catch (EOFException truncated) { + throw new ArchivePersistenceException("Hot Archive record is truncated", truncated); + } + } + + private void requireNewKey(byte[] key, Set newKeys) throws IOException { + if (!newKeys.add(new ByteArrayKey(key)) || writer.get(key) != null) { + throw new ArchivePersistenceException("Hot Archive append would overwrite existing data"); + } + } + + private static OldValue findExactOldValue(BlockReverseDiff diff, String dbName, byte[] rawKey) + throws IOException { + for (DbGroup group : diff.getGroups()) { + if (!group.getDbName().equals(dbName)) { + continue; + } + for (Entry entry : group.getEntries()) { + if (Arrays.equals(entry.getKey(), rawKey)) { + return entry.getOldValue(); + } + } + } + throw new ArchivePersistenceException("Hot Archive index does not match authoritative body"); + } + + private static boolean isIndexCandidate(byte[] key, byte[] prefix) { + return key.length == prefix.length + Long.BYTES + && Arrays.equals(prefix, Arrays.copyOf(key, prefix.length)); + } + + private static byte[] bodyKey(long blockNumber) { + return ByteBuffer.allocate(BODY_PREFIX.length + Long.BYTES).put(BODY_PREFIX) + .putLong(blockNumber).array(); + } + + private static byte[] indexKey(String dbName, byte[] rawKey, long blockNumber) { + byte[] prefix = indexPrefix(dbName, rawKey); + return ByteBuffer.allocate(prefix.length + Long.BYTES).put(prefix).putLong(blockNumber).array(); + } + + private static byte[] indexPrefix(String dbName, byte[] rawKey) { + byte[] name = dbName.getBytes(StandardCharsets.UTF_8); + if (name.length == 0 || name.length > 0xffff) { + throw new IllegalArgumentException("Hot Archive dbName length is invalid"); + } + return ByteBuffer.allocate(INDEX_PREFIX.length + Short.BYTES + name.length + + Integer.BYTES + rawKey.length).put(INDEX_PREFIX).putShort((short) name.length) + .put(name).putInt(rawKey.length).put(rawKey).array(); + } + + private static byte[] nextContentDigest(byte[] previous, BlockSnapshotMeta meta, byte[] record) { + return Hashing.sha256().newHasher().putBytes(previous).putLong(meta.getBlockNumber()) + .putBytes(meta.getBlockHash()).putBytes(Hashing.sha256().hashBytes(record).asBytes()) + .hash().asBytes(); + } + + private static byte[] required(StateArchiveIndexDatabase.Reader reader, byte[] key, int length) + throws IOException { + byte[] value = reader.get(key); + if (value == null || value.length != length) { + throw new ArchivePersistenceException("Hot Archive generation metadata is missing"); + } + return value; + } + + private static long readLong(StateArchiveIndexDatabase.Reader reader, byte[] key) + throws IOException { + return ByteBuffer.wrap(required(reader, key, Long.BYTES)).getLong(); + } + + private static byte[] longBytes(long value) { + return ByteBuffer.allocate(Long.BYTES).putLong(value).array(); + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.US_ASCII); + } + + private static int positive(int value, String name) { + if (value <= 0) { + throw new IllegalArgumentException(name + " must be positive"); + } + return value; + } + + private static long positive(long value, String name) { + if (value <= 0) { + throw new IllegalArgumentException(name + " must be positive"); + } + return value; + } + + private static byte[] copyDigest(byte[] value, String name) { + Objects.requireNonNull(value, name); + if (value.length != HASH_LENGTH) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return Arrays.copyOf(value, value.length); + } + + private void ensureOpen() throws IOException { + if (closed || writer == null) { + throw new IOException("Hot Archive store is closed or requires recovery"); + } + } + + private void ensureOpenUnchecked() { + if (closed || writer == null) { + throw new IllegalStateException("Hot Archive store is closed or requires recovery"); + } + } + + public static final class HotLookup { + private final long blockNumber; + private final OldValue oldValue; + + private HotLookup(long blockNumber, OldValue oldValue) { + this.blockNumber = blockNumber; + this.oldValue = oldValue; + } + + public long getBlockNumber() { + return blockNumber; + } + + public OldValue getOldValue() { + return oldValue; + } + } + + public enum BacklogLevel { + GREEN, + YELLOW, + RED + } + + public enum HotCheckpointStatus { + NEEDS_MATERIALIZATION, + MATERIALIZED, + PUBLISHED + } + + /** Immutable values suitable for logging, gauges, or a future management API. */ + public static final class Statistics { + private final long currentGenerationId; + private final long currentStartBlock; + private final long currentEndBlock; + private final long currentBlocks; + private final long currentEncodedBytes; + private final int frozenGenerations; + private final long frozenBlocks; + private final long frozenEncodedBytes; + private final int maxFrozenGenerations; + private final int yellowFrozenGenerations; + private final int redFrozenGenerations; + private final BacklogLevel backlogLevel; + private final boolean rotationDue; + private final long maxBlocks; + private final long maxEncodedBytes; + private final long publishedBlock; + private final boolean prepared; + + private Statistics(long currentGenerationId, long currentStartBlock, long currentEndBlock, + long currentBlocks, long currentEncodedBytes, int frozenGenerations, long frozenBlocks, + long frozenEncodedBytes, int maxFrozenGenerations, int yellowFrozenGenerations, + int redFrozenGenerations, BacklogLevel backlogLevel, boolean rotationDue, long maxBlocks, + long maxEncodedBytes, long publishedBlock, boolean prepared) { + this.currentGenerationId = currentGenerationId; + this.currentStartBlock = currentStartBlock; + this.currentEndBlock = currentEndBlock; + this.currentBlocks = currentBlocks; + this.currentEncodedBytes = currentEncodedBytes; + this.frozenGenerations = frozenGenerations; + this.frozenBlocks = frozenBlocks; + this.frozenEncodedBytes = frozenEncodedBytes; + this.maxFrozenGenerations = maxFrozenGenerations; + this.yellowFrozenGenerations = yellowFrozenGenerations; + this.redFrozenGenerations = redFrozenGenerations; + this.backlogLevel = backlogLevel; + this.rotationDue = rotationDue; + this.maxBlocks = maxBlocks; + this.maxEncodedBytes = maxEncodedBytes; + this.publishedBlock = publishedBlock; + this.prepared = prepared; + } + + public long getCurrentGenerationId() { + return currentGenerationId; + } + + public long getCurrentStartBlock() { + return currentStartBlock; + } + + public long getCurrentEndBlock() { + return currentEndBlock; + } + + public long getCurrentBlocks() { + return currentBlocks; + } + + public long getCurrentEncodedBytes() { + return currentEncodedBytes; + } + + public int getFrozenGenerations() { + return frozenGenerations; + } + + public long getFrozenBlocks() { + return frozenBlocks; + } + + public long getFrozenEncodedBytes() { + return frozenEncodedBytes; + } + + public int getMaxFrozenGenerations() { + return maxFrozenGenerations; + } + + public int getYellowFrozenGenerations() { + return yellowFrozenGenerations; + } + + public int getRedFrozenGenerations() { + return redFrozenGenerations; + } + + public BacklogLevel getBacklogLevel() { + return backlogLevel; + } + + public boolean isRotationDue() { + return rotationDue; + } + + public long getMaxBlocks() { + return maxBlocks; + } + + public long getMaxEncodedBytes() { + return maxEncodedBytes; + } + + public long getPublishedBlock() { + return publishedBlock; + } + + public boolean hasPreparedCheckpoint() { + return prepared; + } + } + + enum Stage { + AFTER_SEAL, + AFTER_NEW_GENERATION, + AFTER_CURRENT, + AFTER_PREPARE, + AFTER_PUBLISH, + AFTER_TRUNCATE_INTENT, + AFTER_TRUNCATE_DELETE_BATCH, + AFTER_TRUNCATE_METADATA + } + + @FunctionalInterface + interface FaultHook { + void after(Stage stage) throws IOException; + } + + private static final class RecoveryCeiling { + private final long blockNumber; + private final byte[] blockHash; + + private RecoveryCeiling(long blockNumber, byte[] blockHash) { + this.blockNumber = blockNumber; + this.blockHash = copyDigest(blockHash, "blockHash"); + } + } + + private static final class PreparedTarget { + private final byte[] targetDigest; + private final StateArchiveHotBatchDescriptor descriptor; + + private PreparedTarget(byte[] targetDigest, StateArchiveHotBatchDescriptor descriptor) { + this.targetDigest = copyDigest(targetDigest, "targetDigest"); + this.descriptor = Objects.requireNonNull(descriptor, "descriptor"); + } + } + + private static final class Catalog { + private final byte[] formatIdentity; + private final Engine engine; + private final long currentGeneration; + + private Catalog(byte[] formatIdentity, Engine engine, long currentGeneration) { + this.formatIdentity = copyDigest(formatIdentity, "formatIdentity"); + this.engine = Objects.requireNonNull(engine, "engine"); + this.currentGeneration = currentGeneration; + } + + private void require(byte[] expectedFormat, Engine expectedEngine) throws IOException { + if (!Arrays.equals(formatIdentity, expectedFormat) || engine != expectedEngine) { + throw new ArchivePersistenceException("Hot Archive CURRENT identity differs"); + } + } + + private byte[] encode() { + ByteBuffer payload = ByteBuffer.allocate(CATALOG_LENGTH - Integer.BYTES) + .putInt(CATALOG_MAGIC).putShort(CATALOG_VERSION).putShort((short) engineTag(engine)) + .putLong(currentGeneration).put(formatIdentity); + byte[] bytes = payload.array(); + return ByteBuffer.allocate(CATALOG_LENGTH).put(bytes) + .putInt(Hashing.crc32c().hashBytes(bytes).asInt()).array(); + } + } + + private static final class GenerationMeta { + private final long id; + private final long baseBlock; + private final byte[] baseHash; + private final long startBlock; + private final long endBlock; + private final byte[] headHash; + private final long blockCount; + private final long encodedBytes; + private final byte[] contentDigest; + private final boolean sealed; + private final long publishedBlock; + private final byte[] publishedHash; + private final byte[] publishedContentDigest; + private final byte[] publishedTarget; + private final StateArchiveHotBatchDescriptor publishedDescriptor; + private final PreparedTarget prepared; + + private GenerationMeta(long id, long baseBlock, byte[] baseHash, long startBlock, + long endBlock, byte[] headHash, long blockCount, long encodedBytes, + byte[] contentDigest, boolean sealed, long publishedBlock, byte[] publishedHash, + byte[] publishedContentDigest, byte[] publishedTarget, + StateArchiveHotBatchDescriptor publishedDescriptor, PreparedTarget prepared) { + this.id = id; + this.baseBlock = baseBlock; + this.baseHash = copyDigest(baseHash, "baseHash"); + this.startBlock = startBlock; + this.endBlock = endBlock; + this.headHash = copyDigest(headHash, "headHash"); + this.blockCount = blockCount; + this.encodedBytes = encodedBytes; + this.contentDigest = copyDigest(contentDigest, "contentDigest"); + this.sealed = sealed; + this.publishedBlock = publishedBlock; + this.publishedHash = copyDigest(publishedHash, "publishedHash"); + this.publishedContentDigest = copyDigest(publishedContentDigest, + "publishedContentDigest"); + this.publishedTarget = copyDigest(publishedTarget, "publishedTarget"); + this.publishedDescriptor = publishedDescriptor; + this.prepared = prepared; + } + + private static GenerationMeta empty(long id, long baseBlock, byte[] baseHash) { + return new GenerationMeta(id, baseBlock, baseHash, -1, baseBlock, baseHash, 0, 0, + ZERO_DIGEST, false, baseBlock, baseHash, ZERO_DIGEST, ZERO_DIGEST, null, null); + } + + private GenerationMeta sealed() { + return new GenerationMeta(id, baseBlock, baseHash, startBlock, endBlock, headHash, + blockCount, encodedBytes, contentDigest, true, publishedBlock, publishedHash, + publishedContentDigest, publishedTarget, publishedDescriptor, prepared); + } + + private GenerationMeta appended(BlockSnapshotMeta meta, byte[] record) { + long first = blockCount == 0 ? meta.getBlockNumber() : startBlock; + return new GenerationMeta(id, baseBlock, baseHash, first, meta.getBlockNumber(), + meta.getBlockHash(), Math.addExact(blockCount, 1), + Math.addExact(encodedBytes, record.length), + nextContentDigest(contentDigest, meta, record), false, meta.getBlockNumber(), + meta.getBlockHash(), nextContentDigest(contentDigest, meta, record), publishedTarget, + publishedDescriptor, null); + } + + private GenerationMeta published(byte[] targetDigest, + StateArchiveHotBatchDescriptor descriptor) { + return new GenerationMeta(id, baseBlock, baseHash, startBlock, endBlock, headHash, + blockCount, encodedBytes, contentDigest, sealed, endBlock, headHash, contentDigest, + targetDigest, descriptor, null); + } + + private List mutableMetadata() { + List mutations = new ArrayList<>(); + mutations.add(StateArchiveIndexDatabase.put(META_START_BLOCK, longBytes(startBlock))); + mutations.add(StateArchiveIndexDatabase.put(META_END_BLOCK, longBytes(endBlock))); + mutations.add(StateArchiveIndexDatabase.put(META_HEAD_HASH, headHash)); + mutations.add(StateArchiveIndexDatabase.put(META_BLOCK_COUNT, longBytes(blockCount))); + mutations.add(StateArchiveIndexDatabase.put(META_ENCODED_BYTES, longBytes(encodedBytes))); + mutations.add(StateArchiveIndexDatabase.put(META_CONTENT_DIGEST, contentDigest)); + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_BLOCK, + longBytes(publishedBlock))); + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_HASH, publishedHash)); + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_CONTENT_DIGEST, + publishedContentDigest)); + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_TARGET, publishedTarget)); + mutations.add(publishedDescriptor == null + ? StateArchiveIndexDatabase.delete(META_PUBLISHED_DESCRIPTOR) + : StateArchiveIndexDatabase.put(META_PUBLISHED_DESCRIPTOR, + new StateArchiveHotBatchDescriptorCodec().encode(publishedDescriptor))); + return mutations; + } + + private List createMutations(byte[] formatIdentity) { + List mutations = new ArrayList<>(); + mutations.add(StateArchiveIndexDatabase.put(META_FORMAT, formatIdentity)); + mutations.add(StateArchiveIndexDatabase.put(META_ID, longBytes(id))); + mutations.add(StateArchiveIndexDatabase.put(META_BASE_BLOCK, longBytes(baseBlock))); + mutations.add(StateArchiveIndexDatabase.put(META_BASE_HASH, baseHash)); + mutations.add(StateArchiveIndexDatabase.put(META_START_BLOCK, longBytes(startBlock))); + mutations.add(StateArchiveIndexDatabase.put(META_END_BLOCK, longBytes(endBlock))); + mutations.add(StateArchiveIndexDatabase.put(META_HEAD_HASH, headHash)); + mutations.add(StateArchiveIndexDatabase.put(META_BLOCK_COUNT, longBytes(blockCount))); + mutations.add(StateArchiveIndexDatabase.put(META_ENCODED_BYTES, longBytes(encodedBytes))); + mutations.add(StateArchiveIndexDatabase.put(META_CONTENT_DIGEST, contentDigest)); + mutations.add(StateArchiveIndexDatabase.put(META_SEALED, new byte[]{0})); + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_BLOCK, + longBytes(publishedBlock))); + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_HASH, publishedHash)); + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_CONTENT_DIGEST, + publishedContentDigest)); + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_TARGET, publishedTarget)); + if (publishedDescriptor != null) { + mutations.add(StateArchiveIndexDatabase.put(META_PUBLISHED_DESCRIPTOR, + new StateArchiveHotBatchDescriptorCodec().encode(publishedDescriptor))); + } + return mutations; + } + + private void validate() throws IOException { + if (id < 0 || baseBlock < 0 || blockCount < 0 || encodedBytes < 0 + || endBlock < baseBlock || publishedBlock < baseBlock || publishedBlock > endBlock) { + throw new ArchivePersistenceException("Hot Archive generation metadata is invalid"); + } + if (blockCount == 0) { + if (startBlock != -1 || endBlock != baseBlock || !Arrays.equals(headHash, baseHash) + || encodedBytes != 0 || !Arrays.equals(contentDigest, ZERO_DIGEST) || sealed) { + throw new ArchivePersistenceException("Hot Archive empty generation is inconsistent"); + } + } else if (startBlock != baseBlock + 1 || endBlock - startBlock + 1 != blockCount) { + throw new ArchivePersistenceException("Hot Archive generation coverage is inconsistent"); + } + if (publishedBlock == baseBlock + && (!Arrays.equals(publishedHash, baseHash) + || !Arrays.equals(publishedContentDigest, ZERO_DIGEST))) { + throw new ArchivePersistenceException("Hot Archive published base is inconsistent"); + } + if (publishedBlock == endBlock + && (!Arrays.equals(publishedHash, headHash) + || !Arrays.equals(publishedContentDigest, contentDigest))) { + throw new ArchivePersistenceException("Hot Archive published head is inconsistent"); + } + if (prepared == null && publishedBlock != endBlock) { + throw new ArchivePersistenceException("Hot Archive unpublished tail has no target"); + } + if (prepared != null + && (sealed + || prepared.descriptor.getParentPublishedBlock() != publishedBlock + || !Arrays.equals(prepared.descriptor.getParentPublishedHash(), publishedHash) + || !Arrays.equals(prepared.descriptor.getParentContentDigest(), + publishedContentDigest) + || prepared.descriptor.getFirstBlock().getBlockNumber() != publishedBlock + 1 + || prepared.descriptor.getLastBlock().getBlockNumber() != endBlock + || !Arrays.equals(prepared.descriptor.getLastBlock().getBlockHash(), headHash) + || !Arrays.equals(prepared.descriptor.getResultContentDigest(), contentDigest))) { + throw new ArchivePersistenceException("Hot Archive prepared target is inconsistent"); + } + boolean initialPublication = Arrays.equals(publishedTarget, ZERO_DIGEST); + if (initialPublication != (publishedDescriptor == null)) { + throw new ArchivePersistenceException("Hot Archive published descriptor is missing"); + } + if (publishedDescriptor != null + && (publishedDescriptor.getLastBlock().getBlockNumber() != publishedBlock + || !Arrays.equals(publishedDescriptor.getLastBlock().getBlockHash(), publishedHash))) { + throw new ArchivePersistenceException("Hot Archive published descriptor is inconsistent"); + } + if (publishedDescriptor != null && blockCount > 0 + && publishedDescriptor.getFirstBlock().getBlockNumber() >= startBlock + && !Arrays.equals(publishedDescriptor.getResultContentDigest(), + publishedContentDigest)) { + throw new ArchivePersistenceException( + "Hot Archive published descriptor content differs"); + } + if (sealed && publishedBlock != endBlock) { + throw new ArchivePersistenceException("Hot Archive sealed generation is unpublished"); + } + } + + private void requireBase(long expectedBlock, byte[] expectedHash) throws IOException { + if (baseBlock != expectedBlock || !Arrays.equals(baseHash, expectedHash)) { + throw new ArchivePersistenceException("Hot Archive generation parent differs"); + } + } + + private void requirePublication(byte[] expectedTarget, + StateArchiveHotBatchDescriptor expectedDescriptor) throws IOException { + if (!Arrays.equals(publishedTarget, expectedTarget) + || !Objects.equals(publishedDescriptor, expectedDescriptor)) { + throw new ArchivePersistenceException( + "Hot Archive generation publication differs"); + } + } + + } + + private static final class ByteArrayKey { + private final byte[] value; + + private ByteArrayKey(byte[] value) { + this.value = Arrays.copyOf(value, value.length); + } + + @Override + public boolean equals(Object object) { + return object instanceof ByteArrayKey + && Arrays.equals(value, ((ByteArrayKey) object).value); + } + + @Override + public int hashCode() { + return Arrays.hashCode(value); + } + } + + private static int engineTag(Engine engine) { + return engine == Engine.LEVELDB ? 1 : 2; + } + + private static Engine engine(int tag) throws IOException { + if (tag == 1) { + return Engine.LEVELDB; + } + if (tag == 2) { + return Engine.ROCKSDB; + } + throw new ArchivePersistenceException("Hot Archive engine tag is unsupported"); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java index 201d2d510d2..ab02bdd347f 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java @@ -24,7 +24,7 @@ import org.tron.core.config.args.StorageConfig.NativeDbConfig; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; -/** Engine-neutral native store for Archive serving indexes. */ +/** Engine-neutral native store for Archive indexes and independent hot-history generations. */ final class StateArchiveIndexDatabase { private static final Logger logger = LoggerFactory.getLogger("DB"); @@ -35,15 +35,25 @@ private StateArchiveIndexDatabase() { } static Reader openReader(Path directory, Engine engine) throws IOException { + return openReader(directory, engine, configuredOptions()); + } + + static Reader openReader(Path directory, Engine engine, NativeDbConfig suppliedConfig) + throws IOException { Path path = normalize(directory); - NativeDbConfig config = configuredOptions(); + NativeDbConfig config = Objects.requireNonNull(suppliedConfig, "suppliedConfig"); return engine == Engine.LEVELDB ? new LevelReader(acquireLevel(path, false, config)) : new RocksReader(acquireRocks(path, false, config)); } static Writer openWriter(Path directory, Engine engine) throws IOException { + return openWriter(directory, engine, configuredOptions()); + } + + static Writer openWriter(Path directory, Engine engine, NativeDbConfig suppliedConfig) + throws IOException { Path path = normalize(directory); - NativeDbConfig config = configuredOptions(); + NativeDbConfig config = Objects.requireNonNull(suppliedConfig, "suppliedConfig"); return engine == Engine.LEVELDB ? new LevelWriter(acquireLevel(path, true, config)) : new RocksWriter(acquireRocks(path, true, config)); } @@ -112,6 +122,10 @@ static Mutation put(byte[] key, byte[] value) { return new Mutation(key, value); } + static Mutation delete(byte[] key) { + return new Mutation(key, null); + } + private static Path normalize(Path directory) { return Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); } @@ -139,7 +153,7 @@ private static synchronized SharedLevelDatabase acquireLevel(Path directory, boo throw failure; } LEVEL_DATABASES.put(directory, shared); - logger.info("Archive serving index opened: directory={}, engine=LEVELDB, blockBytes={}, " + logger.info("Archive native database opened: directory={}, engine=LEVELDB, blockBytes={}, " + "writeBufferBytes={}, cacheBytes={}, maxOpenFiles={}", directory, config.getBlockSize(), config.getWriteBufferSize(), config.getCacheSize(), config.getMaxOpenFiles()); @@ -161,7 +175,7 @@ private static synchronized SharedRocksDatabase acquireRocks(Path directory, boo throw new IOException("Failed to open RocksDB Archive serving index", failure); } ROCKS_DATABASES.put(directory, shared); - logger.info("Archive serving index opened: directory={}, engine=ROCKSDB, blockBytes={}, " + logger.info("Archive native database opened: directory={}, engine=ROCKSDB, blockBytes={}, " + "writeBufferBytes={}, cacheBytes={}, maxOpenFiles={}", directory, config.getBlockSize(), config.getWriteBufferSize(), config.getCacheSize(), config.getMaxOpenFiles()); @@ -238,7 +252,7 @@ static final class Mutation { private Mutation(byte[] key, byte[] value) { this.key = Arrays.copyOf(Objects.requireNonNull(key, "key"), key.length); - this.value = Arrays.copyOf(Objects.requireNonNull(value, "value"), value.length); + this.value = value == null ? null : Arrays.copyOf(value, value.length); } } @@ -414,7 +428,11 @@ public void write(List mutations) throws IOException { public void write(List mutations, boolean sync) throws IOException { try (org.iq80.leveldb.WriteBatch batch = shared.database.createWriteBatch()) { for (Mutation mutation : mutations) { - batch.put(mutation.key, mutation.value); + if (mutation.value == null) { + batch.delete(mutation.key); + } else { + batch.put(mutation.key, mutation.value); + } } shared.database.write(batch, new org.iq80.leveldb.WriteOptions().sync(sync)); } @@ -555,7 +573,11 @@ public void write(List mutations) throws IOException { public void write(List mutations, boolean sync) throws IOException { try (org.rocksdb.WriteBatch batch = new org.rocksdb.WriteBatch()) { for (Mutation mutation : mutations) { - batch.put(mutation.key, mutation.value); + if (mutation.value == null) { + batch.delete(mutation.key); + } else { + batch.put(mutation.key, mutation.value); + } } try (org.rocksdb.WriteOptions selected = new org.rocksdb.WriteOptions().setSync(sync)) { shared.database.write(selected, batch); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointCapture.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointCapture.java new file mode 100644 index 00000000000..980940466a4 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointCapture.java @@ -0,0 +1,60 @@ +package org.tron.core.db2.core; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.StateArchiveHotBatchDescriptor; + +/** V2 coordination payload plus transient Archive bodies that are never encoded into its WAL. */ +public final class CommonCheckpointCapture { + + private final CommonCheckpointPayload payload; + private final List archiveDiffs; + private final StateArchiveHotBatchDescriptor archiveBinding; + + CommonCheckpointCapture(CommonCheckpointPayload payload, List archiveDiffs, + StateArchiveHotBatchDescriptor archiveBinding) { + this.payload = Objects.requireNonNull(payload, "payload"); + if (payload.getVersion() != CommonCheckpointPayload.COORDINATION_FORMAT_VERSION) { + throw new IllegalArgumentException("common checkpoint capture requires payload v2"); + } + this.archiveDiffs = Collections.unmodifiableList(new ArrayList<>( + Objects.requireNonNull(archiveDiffs, "archiveDiffs"))); + this.archiveBinding = Objects.requireNonNull(archiveBinding, "archiveBinding"); + if (this.archiveDiffs.size() != archiveBinding.getBlockCount() + || !archiveBinding.equals(payload.getArchiveBinding())) { + throw new IllegalArgumentException("common checkpoint capture binding differs"); + } + for (int index = 0; index < this.archiveDiffs.size(); index++) { + BlockReverseDiff diff = Objects.requireNonNull(this.archiveDiffs.get(index), + "archiveDiff"); + StateArchiveHotBatchDescriptor.BlockDigest block = archiveBinding.getBlocks().get(index); + if (!diff.getMeta().equals(block.getMeta()) + || diff.getMutationViewDigest() == null + || !Arrays.equals(diff.getMutationViewDigest(), + block.getMutationViewDigest())) { + throw new IllegalArgumentException("common checkpoint transient Archive diff differs"); + } + } + } + + public static CommonCheckpointCapture create(CommonCheckpointPayload payload, + List archiveDiffs, StateArchiveHotBatchDescriptor archiveBinding) { + return new CommonCheckpointCapture(payload, archiveDiffs, archiveBinding); + } + + public CommonCheckpointPayload getPayload() { + return payload; + } + + public List getArchiveDiffs() { + return archiveDiffs; + } + + public StateArchiveHotBatchDescriptor getArchiveBinding() { + return archiveBinding; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointHotRecovery.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointHotRecovery.java new file mode 100644 index 00000000000..c07822339e8 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointHotRecovery.java @@ -0,0 +1,181 @@ +package org.tron.core.db2.core; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; +import org.tron.core.db2.archive.BlockSnapshotMeta; + +/** Selects one externally verified Hot DB recovery ceiling before common-checkpoint redo. */ +public final class CommonCheckpointHotRecovery { + + private static final byte[] DYNAMIC_BLOCK_NUMBER = + "latest_block_header_number".getBytes(StandardCharsets.UTF_8); + private static final byte[] DYNAMIC_BLOCK_HASH = + "latest_block_header_hash".getBytes(StandardCharsets.UTF_8); + private static final String DYNAMIC_STORE = "properties"; + private static final int HASH_LENGTH = 32; + + private final CommonCheckpointFile checkpointFile; + private final PersistentDynamicHeadSource dynamicHeadSource; + private final BlockMetaSource blockMetaSource; + private final HotTailReconciler reconciler; + + public CommonCheckpointHotRecovery(CommonCheckpointFile checkpointFile, + PersistentDynamicHeadSource dynamicHeadSource, BlockMetaSource blockMetaSource, + HotTailReconciler reconciler) { + this.checkpointFile = Objects.requireNonNull(checkpointFile, "checkpointFile"); + this.dynamicHeadSource = Objects.requireNonNull(dynamicHeadSource, "dynamicHeadSource"); + this.blockMetaSource = Objects.requireNonNull(blockMetaSource, "blockMetaSource"); + this.reconciler = Objects.requireNonNull(reconciler, "reconciler"); + } + + /** + * Reconciles Hot PREPARED data before common redo. A WAL target is authoritative even when its + * Block Store write has not happened yet; without a WAL, the persisted dynamic head must already + * match an exact Block Store record. + */ + public Result reconcileBeforeCommonRedo() throws IOException { + CommonCheckpointPayload payload = checkpointFile.loadIfPresent(); + BlockSnapshotMeta authority; + Source source; + if (payload != null) { + if (payload.getVersion() != CommonCheckpointPayload.COORDINATION_FORMAT_VERSION) { + throw new IOException("Hot Archive recovery requires common checkpoint payload v2"); + } + authority = CommonCheckpointTarget.from(payload).getLastBlock(); + requireWalDynamicIdentity(payload, authority); + BlockSnapshotMeta stored = blockMetaSource.loadIfPresent(authority.getBlockNumber()); + if (stored != null && !authority.equals(stored)) { + throw new IOException("common checkpoint WAL and Block Store identity differ"); + } + source = Source.COMMON_WAL; + } else { + PersistentDynamicHead dynamic = Objects.requireNonNull(dynamicHeadSource.load(), + "persistent dynamic head source returned null"); + BlockSnapshotMeta stored = blockMetaSource.loadIfPresent(dynamic.getBlockNumber()); + if (stored == null) { + throw new IOException("persisted dynamic head is missing from Block Store"); + } + requireNumberAndHash(dynamic.getBlockNumber(), dynamic.getBlockHash(), stored, + "persisted dynamic head and Block Store identity differ"); + authority = stored; + source = Source.PERSISTED_DYNAMIC; + } + return new Result(source, authority, reconciler.reconcile(authority)); + } + + static void requireWalDynamicIdentity(CommonCheckpointPayload payload, + BlockSnapshotMeta target) throws IOException { + CommonCheckpointPayload.StoreMutations dynamic = null; + for (CommonCheckpointPayload.StoreMutations store : payload.getChainbaseStores()) { + if (DYNAMIC_STORE.equals(store.getDbName())) { + dynamic = store; + break; + } + } + if (dynamic == null) { + throw new IOException("common checkpoint WAL has no dynamic Store mutations"); + } + byte[] encodedNumber = null; + byte[] encodedHash = null; + for (CommonCheckpointPayload.Mutation mutation : dynamic.getMutations()) { + if (Arrays.equals(DYNAMIC_BLOCK_NUMBER, mutation.getKey())) { + encodedNumber = mutation.getValue(); + } else if (Arrays.equals(DYNAMIC_BLOCK_HASH, mutation.getKey())) { + encodedHash = mutation.getValue(); + } + } + if (encodedNumber == null || encodedNumber.length != Long.BYTES + || encodedHash == null || encodedHash.length != HASH_LENGTH) { + throw new IOException("common checkpoint WAL dynamic head is incomplete"); + } + long number = ByteBuffer.wrap(encodedNumber).getLong(); + requireNumberAndHash(number, encodedHash, target, + "common checkpoint WAL dynamic head and target differ"); + } + + private static void requireNumberAndHash(long number, byte[] hash, BlockSnapshotMeta meta, + String message) throws IOException { + if (number < 0 || number != meta.getBlockNumber() + || !Arrays.equals(hash, meta.getBlockHash())) { + throw new IOException(message); + } + } + + public enum Source { + COMMON_WAL, + PERSISTED_DYNAMIC + } + + public static final class PersistentDynamicHead { + + private final long blockNumber; + private final byte[] blockHash; + + public PersistentDynamicHead(long blockNumber, byte[] blockHash) { + if (blockNumber < 0) { + throw new IllegalArgumentException("persistent dynamic block number must not be negative"); + } + this.blockNumber = blockNumber; + this.blockHash = copyHash(blockHash); + } + + public long getBlockNumber() { + return blockNumber; + } + + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + } + + public static final class Result { + + private final Source source; + private final BlockSnapshotMeta authority; + private final long removedBlocks; + + private Result(Source source, BlockSnapshotMeta authority, long removedBlocks) { + this.source = source; + this.authority = authority; + this.removedBlocks = removedBlocks; + } + + public Source getSource() { + return source; + } + + public BlockSnapshotMeta getAuthority() { + return authority; + } + + public long getRemovedBlocks() { + return removedBlocks; + } + } + + @FunctionalInterface + public interface PersistentDynamicHeadSource { + PersistentDynamicHead load() throws IOException; + } + + @FunctionalInterface + public interface BlockMetaSource { + BlockSnapshotMeta loadIfPresent(long blockNumber) throws IOException; + } + + @FunctionalInterface + public interface HotTailReconciler { + long reconcile(BlockSnapshotMeta authority) throws IOException; + } + + private static byte[] copyHash(byte[] value) { + byte[] copy = Arrays.copyOf(Objects.requireNonNull(value, "blockHash"), value.length); + if (copy.length != HASH_LENGTH) { + throw new IllegalArgumentException("blockHash must contain exactly 32 bytes"); + } + return copy; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java index 79133112479..51a22a30fc8 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java @@ -8,17 +8,20 @@ import java.util.Objects; import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.StateArchiveHotBatchDescriptor; import org.tron.core.db2.stateroot.PathStateFlushTarget; import org.tron.core.db2.stateroot.PathStateSnapshotDelta; -/** Complete immutable redo input for one future cross-authority checkpoint. */ +/** Versioned immutable redo or coordination input for one cross-authority checkpoint. */ public final class CommonCheckpointPayload { public static final int FORMAT_VERSION = 1; + public static final int COORDINATION_FORMAT_VERSION = 2; private static final int DIGEST_LENGTH = 32; private static final Comparator MUTATION_ORDER = (left, right) -> compareUnsigned(left.key, right.key); + private final int version; private final byte[] formatIdentity; private final List blocks; private final byte[] parentStateRoot; @@ -26,10 +29,16 @@ public final class CommonCheckpointPayload { private final List chainbaseStores; private final List pathStores; private final List superNodeMutations; + private final StateArchiveHotBatchDescriptor archiveBinding; - private CommonCheckpointPayload(byte[] formatIdentity, List blocks, + private CommonCheckpointPayload(int version, byte[] formatIdentity, List blocks, byte[] parentStateRoot, byte[] stateRoot, List chainbaseStores, - List pathStores, List superNodeMutations) { + List pathStores, List superNodeMutations, + StateArchiveHotBatchDescriptor archiveBinding) { + if (version != FORMAT_VERSION && version != COORDINATION_FORMAT_VERSION) { + throw new IllegalArgumentException("unsupported common checkpoint payload version"); + } + this.version = version; this.formatIdentity = digest(formatIdentity, "formatIdentity"); if (blocks.isEmpty()) { throw new IllegalArgumentException("common checkpoint must contain at least one block"); @@ -37,11 +46,13 @@ private CommonCheckpointPayload(byte[] formatIdentity, List blocks this.parentStateRoot = digest(parentStateRoot, "parentStateRoot"); this.stateRoot = digest(stateRoot, "stateRoot"); List admittedBlocks = new ArrayList<>(blocks); - validateBlocks(admittedBlocks, this.parentStateRoot, this.stateRoot); + validateBlocks(version, admittedBlocks, this.parentStateRoot, this.stateRoot, + archiveBinding); this.blocks = Collections.unmodifiableList(admittedBlocks); this.chainbaseStores = immutableStores(chainbaseStores); this.pathStores = immutablePathStores(pathStores); this.superNodeMutations = immutableMutations(superNodeMutations); + this.archiveBinding = archiveBinding; } public static CommonCheckpointPayload create(byte[] formatIdentity, @@ -74,16 +85,93 @@ public static CommonCheckpointPayload create(byte[] formatIdentity, store.getStoreRoot(), mutations(store.getFlatMutations()), mutations(store.getNodeMutations()))); } - return new CommonCheckpointPayload(formatIdentity, blocks, path.getParentStateRoot(), + return new CommonCheckpointPayload(FORMAT_VERSION, formatIdentity, blocks, + path.getParentStateRoot(), path.getStateRoot(), chainbaseStores, pathStores, - mutations(path.getSuperNodeMutations())); + mutations(path.getSuperNodeMutations()), null); + } + + /** Builds a v2 coordination payload whose Archive section contains only Hot DB digests. */ + public static CommonCheckpointPayload createV2(byte[] formatIdentity, + PathStateFlushTarget pathState, StateArchiveHotBatchDescriptor archiveBinding, + List chainbaseStores) { + PathStateFlushTarget path = Objects.requireNonNull(pathState, "pathState"); + StateArchiveHotBatchDescriptor archive = Objects.requireNonNull(archiveBinding, + "archiveBinding"); + if (path.getBlocks().size() != archive.getBlocks().size()) { + throw new IllegalArgumentException("common checkpoint block payload count differs"); + } + List blocks = new ArrayList<>(); + for (int index = 0; index < path.getBlocks().size(); index++) { + PathStateFlushTarget.BlockBinding binding = path.getBlocks().get(index); + StateArchiveHotBatchDescriptor.BlockDigest digest = archive.getBlocks().get(index); + if (!binding.getMeta().equals(digest.getMeta()) + || !Arrays.equals(binding.getMutationViewDigest(), digest.getMutationViewDigest())) { + throw new IllegalArgumentException( + "common checkpoint Hot Archive and PathState block identity differs"); + } + blocks.add(BlockPayload.coordination(binding.getMeta(), binding.getParentStateRoot(), + binding.getStateRoot(), binding.getTransitionPayloadDigest(), + binding.getMutationViewDigest(), digest.getArchiveRecordDigest())); + } + List pathStores = new ArrayList<>(); + for (PathStateFlushTarget.StoreTarget store : path.getStores()) { + pathStores.add(new PathStoreTarget(store.getStoreId(), store.getDbName(), + store.getStoreRoot(), mutations(store.getFlatMutations()), + mutations(store.getNodeMutations()))); + } + return new CommonCheckpointPayload(COORDINATION_FORMAT_VERSION, formatIdentity, blocks, + path.getParentStateRoot(), path.getStateRoot(), chainbaseStores, pathStores, + mutations(path.getSuperNodeMutations()), archive); + } + + static CommonCheckpointPayload coordinateV2(CommonCheckpointPayload capturedV1, + StateArchiveHotBatchDescriptor archiveBinding) { + CommonCheckpointPayload source = Objects.requireNonNull(capturedV1, "capturedV1"); + if (source.version != FORMAT_VERSION) { + throw new IllegalArgumentException("coordination conversion requires a v1 capture"); + } + StateArchiveHotBatchDescriptor archive = Objects.requireNonNull(archiveBinding, + "archiveBinding"); + if (source.blocks.size() != archive.getBlocks().size()) { + throw new IllegalArgumentException("common checkpoint block payload count differs"); + } + List blocks = new ArrayList<>(); + for (int index = 0; index < source.blocks.size(); index++) { + BlockPayload block = source.blocks.get(index); + StateArchiveHotBatchDescriptor.BlockDigest digest = archive.getBlocks().get(index); + if (!block.meta.equals(digest.getMeta()) + || !Arrays.equals(block.mutationViewDigest, digest.getMutationViewDigest())) { + throw new IllegalArgumentException( + "common checkpoint Hot Archive and captured block identity differs"); + } + blocks.add(BlockPayload.coordination(block.meta, block.parentStateRoot, block.stateRoot, + block.transitionPayloadDigest, block.mutationViewDigest, + digest.getArchiveRecordDigest())); + } + return new CommonCheckpointPayload(COORDINATION_FORMAT_VERSION, source.formatIdentity, + blocks, source.parentStateRoot, source.stateRoot, source.chainbaseStores, + source.pathStores, source.superNodeMutations, archive); } static CommonCheckpointPayload restore(byte[] formatIdentity, List blocks, byte[] parentStateRoot, byte[] stateRoot, List chainbaseStores, List pathStores, List superNodeMutations) { - return new CommonCheckpointPayload(formatIdentity, blocks, parentStateRoot, stateRoot, - chainbaseStores, pathStores, superNodeMutations); + return new CommonCheckpointPayload(FORMAT_VERSION, formatIdentity, blocks, parentStateRoot, + stateRoot, chainbaseStores, pathStores, superNodeMutations, null); + } + + static CommonCheckpointPayload restoreV2(byte[] formatIdentity, List blocks, + byte[] parentStateRoot, byte[] stateRoot, List chainbaseStores, + List pathStores, List superNodeMutations, + StateArchiveHotBatchDescriptor archiveBinding) { + return new CommonCheckpointPayload(COORDINATION_FORMAT_VERSION, formatIdentity, blocks, + parentStateRoot, stateRoot, chainbaseStores, pathStores, superNodeMutations, + archiveBinding); + } + + public int getVersion() { + return version; } public byte[] getFormatIdentity() { @@ -114,6 +202,13 @@ public List getSuperNodeMutations() { return superNodeMutations; } + public StateArchiveHotBatchDescriptor getArchiveBinding() { + if (archiveBinding == null) { + throw new IllegalStateException("v1 common checkpoint has no Hot Archive binding"); + } + return archiveBinding; + } + private static List immutableStores(List supplied) { List stores = new ArrayList<>(Objects.requireNonNull(supplied, "chainbaseStores")); @@ -122,14 +217,19 @@ private static List immutableStores(List supplie return Collections.unmodifiableList(stores); } - private static void validateBlocks(List blocks, byte[] parentStateRoot, - byte[] stateRoot) { + private static void validateBlocks(int version, List blocks, + byte[] parentStateRoot, byte[] stateRoot, + StateArchiveHotBatchDescriptor archiveBinding) { BlockPayload previous = null; for (BlockPayload block : blocks) { BlockPayload current = Objects.requireNonNull(block, "block"); - byte[] archiveView = current.archiveDiff.getMutationViewDigest(); - if (archiveView == null || !Arrays.equals(archiveView, current.mutationViewDigest)) { - throw new IllegalArgumentException("checkpoint block mutation-view identity differs"); + if (version == FORMAT_VERSION) { + byte[] archiveView = current.requireArchiveDiff().getMutationViewDigest(); + if (archiveView == null || !Arrays.equals(archiveView, current.mutationViewDigest)) { + throw new IllegalArgumentException("checkpoint block mutation-view identity differs"); + } + } else if (current.archiveDiff != null || current.archiveRecordDigest == null) { + throw new IllegalArgumentException("coordination checkpoint contains Archive body"); } if (previous != null && (current.meta.getEpoch() != previous.meta.getEpoch() + 1 @@ -144,6 +244,28 @@ private static void validateBlocks(List blocks, byte[] parentState || !Arrays.equals(blocks.get(blocks.size() - 1).stateRoot, stateRoot)) { throw new IllegalArgumentException("common checkpoint target root range differs"); } + if (version == COORDINATION_FORMAT_VERSION + && (archiveBinding == null + || !blocks.get(0).meta.equals(archiveBinding.getFirstBlock()) + || !blocks.get(blocks.size() - 1).meta.equals(archiveBinding.getLastBlock()))) { + throw new IllegalArgumentException("common checkpoint Archive binding range differs"); + } + if (version == COORDINATION_FORMAT_VERSION) { + List archiveBlocks = + archiveBinding.getBlocks(); + if (blocks.size() != archiveBlocks.size()) { + throw new IllegalArgumentException("common checkpoint Archive binding count differs"); + } + for (int index = 0; index < blocks.size(); index++) { + BlockPayload block = blocks.get(index); + StateArchiveHotBatchDescriptor.BlockDigest archive = archiveBlocks.get(index); + if (!block.meta.equals(archive.getMeta()) + || !Arrays.equals(block.mutationViewDigest, archive.getMutationViewDigest()) + || !Arrays.equals(block.archiveRecordDigest, archive.getArchiveRecordDigest())) { + throw new IllegalArgumentException("common checkpoint Archive block binding differs"); + } + } + } } private static List immutablePathStores(List supplied) { @@ -215,6 +337,7 @@ public static final class BlockPayload { private final byte[] transitionPayloadDigest; private final byte[] mutationViewDigest; private final BlockReverseDiff archiveDiff; + private final byte[] archiveRecordDigest; BlockPayload(BlockSnapshotMeta meta, byte[] parentStateRoot, byte[] stateRoot, byte[] transitionPayloadDigest, byte[] mutationViewDigest, @@ -226,11 +349,32 @@ public static final class BlockPayload { "transitionPayloadDigest"); this.mutationViewDigest = digest(mutationViewDigest, "mutationViewDigest"); this.archiveDiff = Objects.requireNonNull(archiveDiff, "archiveDiff"); + this.archiveRecordDigest = null; if (!meta.equals(archiveDiff.getMeta())) { throw new IllegalArgumentException("checkpoint Archive block metadata differs"); } } + private BlockPayload(BlockSnapshotMeta meta, byte[] parentStateRoot, byte[] stateRoot, + byte[] transitionPayloadDigest, byte[] mutationViewDigest, + byte[] archiveRecordDigest) { + this.meta = Objects.requireNonNull(meta, "meta"); + this.parentStateRoot = digest(parentStateRoot, "parentStateRoot"); + this.stateRoot = digest(stateRoot, "stateRoot"); + this.transitionPayloadDigest = digest(transitionPayloadDigest, + "transitionPayloadDigest"); + this.mutationViewDigest = digest(mutationViewDigest, "mutationViewDigest"); + this.archiveDiff = null; + this.archiveRecordDigest = digest(archiveRecordDigest, "archiveRecordDigest"); + } + + static BlockPayload coordination(BlockSnapshotMeta meta, byte[] parentStateRoot, + byte[] stateRoot, byte[] transitionPayloadDigest, byte[] mutationViewDigest, + byte[] archiveRecordDigest) { + return new BlockPayload(meta, parentStateRoot, stateRoot, transitionPayloadDigest, + mutationViewDigest, archiveRecordDigest); + } + public BlockSnapshotMeta getMeta() { return meta; } @@ -252,6 +396,20 @@ public byte[] getMutationViewDigest() { } public BlockReverseDiff getArchiveDiff() { + return requireArchiveDiff(); + } + + public byte[] getArchiveRecordDigest() { + if (archiveRecordDigest == null) { + throw new IllegalStateException("v1 common checkpoint has no Archive record digest"); + } + return copy(archiveRecordDigest); + } + + private BlockReverseDiff requireArchiveDiff() { + if (archiveDiff == null) { + throw new IllegalStateException("v2 coordination checkpoint has no Archive body"); + } return archiveDiff; } } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java index 91b8f595c03..cc139369633 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java @@ -14,16 +14,20 @@ import org.tron.core.db2.archive.BlockHistoryCodec; import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.StateArchiveHotBatchDescriptor; +import org.tron.core.db2.archive.StateArchiveHotBatchDescriptor.BlockDigest; import org.tron.core.db2.core.CommonCheckpointPayload.BlockPayload; import org.tron.core.db2.core.CommonCheckpointPayload.Mutation; import org.tron.core.db2.core.CommonCheckpointPayload.PathStoreTarget; import org.tron.core.db2.core.CommonCheckpointPayload.StoreMutations; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; -/** Deterministic, bounded and checksummed codec for a complete common-checkpoint redo payload. */ +/** Deterministic, bounded codec for v1 redo bodies and v2 digest-only Archive coordination. */ public final class CommonCheckpointPayloadCodec { public static final int MAGIC = 0x54434350; // TCCP public static final short VERSION = 1; + public static final short COORDINATION_VERSION = 2; public static final int HEADER_LENGTH = 44; public static final int DEFAULT_MAX_ENCODED_LENGTH = 256 * 1024 * 1024; private static final int DIGEST_LENGTH = 32; @@ -54,7 +58,7 @@ public byte[] encode(CommonCheckpointPayload payload) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(HEADER_LENGTH + body.length); DataOutputStream output = new DataOutputStream(bytes); output.writeInt(MAGIC); - output.writeShort(VERSION); + output.writeShort(payload.getVersion()); output.writeShort(0); output.writeInt(body.length); output.write(Hashing.sha256().hashBytes(body).asBytes()); @@ -76,7 +80,8 @@ public CommonCheckpointPayload decode(byte[] encoded) { if (input.readInt() != MAGIC) { throw new IllegalArgumentException("invalid common checkpoint magic"); } - if (input.readShort() != VERSION) { + short version = input.readShort(); + if (version != VERSION && version != COORDINATION_VERSION) { throw new IllegalArgumentException("unsupported common checkpoint version"); } if (input.readShort() != 0) { @@ -91,7 +96,7 @@ public CommonCheckpointPayload decode(byte[] encoded) { if (!Arrays.equals(expectedDigest, Hashing.sha256().hashBytes(body).asBytes())) { throw new IllegalArgumentException("common checkpoint payload checksum mismatch"); } - return decodeBody(body); + return version == VERSION ? decodeBodyV1(body) : decodeBodyV2(body); } catch (EOFException truncated) { throw new IllegalArgumentException("common checkpoint payload is truncated", truncated); } catch (IOException invalid) { @@ -117,7 +122,14 @@ private byte[] encodeBody(CommonCheckpointPayload payload) throws IOException { output.write(block.getStateRoot()); output.write(block.getTransitionPayloadDigest()); output.write(block.getMutationViewDigest()); - writeBytes(output, historyCodec.encode(block.getArchiveDiff())); + if (admitted.getVersion() == CommonCheckpointPayload.FORMAT_VERSION) { + writeBytes(output, historyCodec.encode(block.getArchiveDiff())); + } else { + output.write(block.getArchiveRecordDigest()); + } + } + if (admitted.getVersion() == CommonCheckpointPayload.COORDINATION_FORMAT_VERSION) { + writeArchiveBinding(output, admitted.getArchiveBinding()); } writeStores(output, admitted.getChainbaseStores()); output.writeInt(admitted.getPathStores().size()); @@ -133,7 +145,7 @@ private byte[] encodeBody(CommonCheckpointPayload payload) throws IOException { return bytes.toByteArray(); } - private CommonCheckpointPayload decodeBody(byte[] body) throws IOException { + private CommonCheckpointPayload decodeBodyV1(byte[] body) throws IOException { DataInputStream input = new DataInputStream(new ByteArrayInputStream(body)); byte[] formatIdentity = readExact(input, DIGEST_LENGTH); byte[] parentStateRoot = readExact(input, DIGEST_LENGTH); @@ -170,6 +182,97 @@ private CommonCheckpointPayload decodeBody(byte[] body) throws IOException { chainbase, pathStores, superNodes); } + private CommonCheckpointPayload decodeBodyV2(byte[] body) throws IOException { + DataInputStream input = new DataInputStream(new ByteArrayInputStream(body)); + byte[] formatIdentity = readExact(input, DIGEST_LENGTH); + byte[] parentStateRoot = readExact(input, DIGEST_LENGTH); + byte[] stateRoot = readExact(input, DIGEST_LENGTH); + int blockCount = readCount(input, MAX_BLOCKS, "block"); + List blocks = new ArrayList<>(blockCount); + List archiveBlocks = new ArrayList<>(blockCount); + for (int index = 0; index < blockCount; index++) { + BlockSnapshotMeta meta = readMeta(input); + byte[] parentRoot = readExact(input, DIGEST_LENGTH); + byte[] blockRoot = readExact(input, DIGEST_LENGTH); + byte[] transitionDigest = readExact(input, DIGEST_LENGTH); + byte[] viewDigest = readExact(input, DIGEST_LENGTH); + byte[] recordDigest = readExact(input, DIGEST_LENGTH); + blocks.add(BlockPayload.coordination(meta, parentRoot, blockRoot, transitionDigest, + viewDigest, recordDigest)); + archiveBlocks.add(BlockDigest.restore(meta, viewDigest, recordDigest)); + } + StateArchiveHotBatchDescriptor archiveBinding = readArchiveBinding(input, archiveBlocks); + List chainbase = readStores(input); + int pathStoreCount = readCount(input, MAX_STORES, "path-state Store"); + List pathStores = new ArrayList<>(pathStoreCount); + for (int index = 0; index < pathStoreCount; index++) { + int storeId = input.readInt(); + String dbName = readName(input); + byte[] storeRoot = readExact(input, DIGEST_LENGTH); + pathStores.add(new PathStoreTarget(storeId, dbName, storeRoot, + readMutations(input), readMutations(input))); + } + List superNodes = readMutations(input); + if (input.available() != 0) { + throw new IllegalArgumentException("common checkpoint payload has trailing bytes"); + } + return CommonCheckpointPayload.restoreV2(formatIdentity, blocks, parentStateRoot, stateRoot, + chainbase, pathStores, superNodes, archiveBinding); + } + + private static void writeArchiveBinding(DataOutputStream output, + StateArchiveHotBatchDescriptor binding) throws IOException { + output.writeShort(StateArchiveHotBatchDescriptor.HOT_FORMAT_VERSION); + output.writeShort(engineTag(binding.getEngine())); + output.writeLong(binding.getParentPublishedBlock()); + output.write(binding.getParentPublishedHash()); + writeMeta(output, binding.getFirstBlock()); + writeMeta(output, binding.getLastBlock()); + output.writeLong(binding.getBlockCount()); + output.writeLong(binding.getEncodedBytes()); + output.write(binding.getParentContentDigest()); + output.write(binding.getResultContentDigest()); + output.write(binding.getOrderedRecordDigest()); + output.write(binding.getMutationViewRangeDigest()); + } + + private static StateArchiveHotBatchDescriptor readArchiveBinding(DataInputStream input, + List blocks) throws IOException { + if (input.readUnsignedShort() != StateArchiveHotBatchDescriptor.HOT_FORMAT_VERSION) { + throw new IllegalArgumentException("unsupported Hot Archive binding version"); + } + Engine engine = engine(input.readUnsignedShort()); + long parentBlock = input.readLong(); + byte[] parentHash = readExact(input, DIGEST_LENGTH); + BlockSnapshotMeta first = readMeta(input); + BlockSnapshotMeta last = readMeta(input); + long blockCount = input.readLong(); + long encodedBytes = input.readLong(); + byte[] parentContent = readExact(input, DIGEST_LENGTH); + byte[] resultContent = readExact(input, DIGEST_LENGTH); + byte[] orderedRecords = readExact(input, DIGEST_LENGTH); + byte[] mutationViews = readExact(input, DIGEST_LENGTH); + if (blockCount != blocks.size()) { + throw new IllegalArgumentException("Hot Archive binding block count differs"); + } + return StateArchiveHotBatchDescriptor.restore(engine, parentBlock, parentHash, first, last, + encodedBytes, parentContent, resultContent, orderedRecords, mutationViews, blocks); + } + + private static int engineTag(Engine engine) { + return engine == Engine.LEVELDB ? 1 : 2; + } + + private static Engine engine(int tag) { + if (tag == 1) { + return Engine.LEVELDB; + } + if (tag == 2) { + return Engine.ROCKSDB; + } + throw new IllegalArgumentException("unsupported Hot Archive engine tag"); + } + private void writeStores(DataOutputStream output, List stores) throws IOException { output.writeInt(stores.size()); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java index 750a35b521b..fd1dc20598c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java @@ -1,5 +1,6 @@ package org.tron.core.db2.core; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; @@ -9,6 +10,8 @@ import org.tron.core.db2.archive.ArchiveStoreScope; import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.StateArchiveHotBatchDescriptor; +import org.tron.core.db2.archive.StateArchiveHotCheckpointMaterializer; import org.tron.core.db2.common.Key; import org.tron.core.db2.common.Value; import org.tron.core.db2.common.WrappedByteArray; @@ -18,6 +21,22 @@ /** Builds one immutable common-checkpoint redo payload without querying durable databases. */ public final class CommonCheckpointPayloadFactory { + /** Captures v2 coordination data while retaining Archive bodies only in transient memory. */ + public CommonCheckpointCapture captureV2(byte[] formatIdentity, List databases, + int flushCount, StateArchiveHotCheckpointMaterializer hotMaterializer) throws IOException { + CommonCheckpointPayload captured = capture(formatIdentity, databases, flushCount); + List archiveDiffs = new ArrayList<>(); + for (CommonCheckpointPayload.BlockPayload block : captured.getBlocks()) { + archiveDiffs.add(block.getArchiveDiff()); + } + StateArchiveHotBatchDescriptor binding = Objects.requireNonNull(hotMaterializer, + "hotMaterializer") + .planCheckpoint(archiveDiffs); + CommonCheckpointPayload coordination = CommonCheckpointPayload.coordinateV2(captured, + binding); + return new CommonCheckpointCapture(coordination, archiveDiffs, binding); + } + /** Captures the oldest {@code flushCount} Snapshot layers from every registered Store. */ public CommonCheckpointPayload capture(byte[] formatIdentity, List databases, int flushCount) { diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRecoveryStateAdapter.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRecoveryStateAdapter.java new file mode 100644 index 00000000000..f896394590d --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRecoveryStateAdapter.java @@ -0,0 +1,66 @@ +package org.tron.core.db2.core; + +import java.io.IOException; +import java.util.Objects; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.ChainBaseManager; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.exception.BadItemException; +import org.tron.core.exception.ItemNotFoundException; +import org.tron.core.store.DynamicPropertiesStore; + +/** Read-only recovery identity adapter over checkpoint-redone persistent Chainbase stores. */ +public final class CommonCheckpointRecoveryStateAdapter + implements CommonCheckpointHotRecovery.PersistentDynamicHeadSource, + CommonCheckpointHotRecovery.BlockMetaSource { + + private final DynamicPropertiesStore dynamicPropertiesStore; + private final ChainBaseManager chainBaseManager; + + public CommonCheckpointRecoveryStateAdapter(DynamicPropertiesStore dynamicPropertiesStore, + ChainBaseManager chainBaseManager) { + this.dynamicPropertiesStore = Objects.requireNonNull(dynamicPropertiesStore, + "dynamicPropertiesStore"); + this.chainBaseManager = Objects.requireNonNull(chainBaseManager, "chainBaseManager"); + } + + /** Reads only the persistent root; reversible Snapshot values are not recovery authority. */ + @Override + public CommonCheckpointHotRecovery.PersistentDynamicHead load() throws IOException { + final long blockNumber; + final Sha256Hash blockHash; + try { + blockNumber = dynamicPropertiesStore.getLatestBlockHeaderNumberFromDB(); + blockHash = dynamicPropertiesStore.getLatestBlockHeaderHashFromDB(); + } catch (RuntimeException failure) { + throw new IOException("persistent dynamic block identity cannot be read", failure); + } + if (blockNumber < 0 || blockHash == null) { + throw new IOException("persistent dynamic block identity is unavailable"); + } + return new CommonCheckpointHotRecovery.PersistentDynamicHead(blockNumber, + blockHash.getBytes()); + } + + /** Loads the full canonical block metadata needed to validate an exact recovery ceiling. */ + @Override + public BlockSnapshotMeta loadIfPresent(long blockNumber) throws IOException { + if (blockNumber < 0) { + throw new IllegalArgumentException("blockNumber must not be negative"); + } + final BlockCapsule block; + try { + block = chainBaseManager.getBlockByNum(blockNumber); + } catch (ItemNotFoundException missing) { + return null; + } catch (BadItemException corrupt) { + throw new IOException("Block Store recovery metadata is corrupt", corrupt); + } + if (block.getNum() != blockNumber) { + throw new IOException("Block Store returned a different recovery height"); + } + return BlockSnapshotMeta.forBlock(blockNumber, block.getBlockId().getBytes(), + block.getParentHash().getBytes(), block.getTimeStamp()); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java index 45f81ade009..b13740c86ea 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java @@ -85,6 +85,15 @@ public synchronized RecoveryAction recover() throws IOException { return action; } + synchronized void requireMaterializer(Authority authority, + CommonCheckpointMaterializer expected) { + if (materializers.get(Objects.requireNonNull(authority, "authority")) + != Objects.requireNonNull(expected, "expected")) { + throw new IllegalArgumentException( + "common checkpoint runtime materializer identity differs: " + authority); + } + } + /** Closes runtime-owned authority resources in reverse publication order. */ @Override public synchronized void close() throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java index 437eaddc350..36c441e8254 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java @@ -9,6 +9,7 @@ import java.util.function.LongSupplier; import org.tron.core.db2.archive.StateArchiveCheckpointMaterializer; import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; +import org.tron.core.db2.archive.StateArchiveHotCheckpointMaterializer; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +26,8 @@ public final class CommonCheckpointRuntime implements AutoCloseable { private final Engine engine; private final StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory; private final CommonCheckpointMemoryRebaser memoryRebaser; + private final CommonCheckpointHotRecovery hotRecovery; + private final StateArchiveHotCheckpointMaterializer hotMaterializer; private final LongSupplier nanoTime; private final TimingSink timingSink; private final CommonCheckpointPayloadFactory payloadFactory = new CommonCheckpointPayloadFactory(); @@ -36,7 +39,20 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, + Path archiveDirectory, byte[] formatIdentity, Engine engine, + StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory, + CommonCheckpointMemoryRebaser memoryRebaser, + StateArchiveHotCheckpointMaterializer hotMaterializer, + CommonCheckpointHotRecovery hotRecovery) { + this(owner, databases, archiveDirectory, formatIdentity, engine, latestFactory, + memoryRebaser, Objects.requireNonNull(hotMaterializer, "hotMaterializer"), + Objects.requireNonNull(hotRecovery, "hotRecovery"), System::nanoTime, + CommonCheckpointRuntime::logTiming); } CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, @@ -44,6 +60,16 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, + Path archiveDirectory, byte[] formatIdentity, Engine engine, + StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory, + CommonCheckpointMemoryRebaser memoryRebaser, + StateArchiveHotCheckpointMaterializer hotMaterializer, + CommonCheckpointHotRecovery hotRecovery, LongSupplier nanoTime, TimingSink timingSink) { this.owner = Objects.requireNonNull(owner, "owner"); this.databases = new ArrayList<>(Objects.requireNonNull(databases, "databases")); if (this.databases.isEmpty() || this.databases.contains(null)) { @@ -54,6 +80,15 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List { long chainbasePrepareStart = nanoTime.getAsLong(); @@ -117,6 +167,9 @@ public synchronized CommonCheckpointTarget checkpointAndRebase(int flushCount) /** Pins one point-only historical request under the same publication gate. */ public synchronized StateArchiveCheckpointReadSnapshot pinPoint(long targetBlock) throws IOException { + if (hotMaterializer != null) { + throw new IOException("Hot Archive runtime point reads are not integrated"); + } CommonCheckpointTarget target = publishedTarget; if (target == null) { throw new IOException("State Archive has no published common-checkpoint target"); @@ -157,9 +210,11 @@ private void emitTiming(Timing timing) { private static void logTiming(Timing timing) { logger.info("Common checkpoint runtime stages: head={}, blocks={}, payloadCaptureUs={}, " - + "ownerApplyUs={}, chainbaseRebasePrepareUs={}, pathStateRebasePrepareUs={}, " + + "hotPrepareUs={}, ownerApplyUs={}, chainbaseRebasePrepareUs={}, " + + "pathStateRebasePrepareUs={}, " + "chainbaseRebaseApplyUs={}, pathStateRebaseApplyUs={}, totalUs={}", - timing.head, timing.blocks, timing.payloadCaptureUs, timing.ownerApplyUs, + timing.head, timing.blocks, timing.payloadCaptureUs, timing.hotPrepareUs, + timing.ownerApplyUs, timing.chainbaseRebasePrepareUs, timing.pathStateRebasePrepareUs, timing.chainbaseRebaseApplyUs, timing.pathStateRebaseApplyUs, timing.totalUs); } @@ -169,6 +224,7 @@ static final class Timing { private long head; private final int blocks; private long payloadCaptureUs; + private long hotPrepareUs; private long ownerApplyUs; private long chainbaseRebasePrepareUs; private long pathStateRebasePrepareUs; @@ -192,6 +248,10 @@ long getPayloadCaptureUs() { return payloadCaptureUs; } + long getHotPrepareUs() { + return hotPrepareUs; + } + long getOwnerApplyUs() { return ownerApplyUs; } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java index 649262823c2..bb549784299 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java @@ -16,6 +16,11 @@ public CommonCheckpointRuntimeOwner(CommonCheckpointRedoCoordinator coordinator) this.coordinator = Objects.requireNonNull(coordinator, "coordinator"); } + void requireMaterializer(CommonCheckpointMaterializer materializer) { + CommonCheckpointMaterializer admitted = Objects.requireNonNull(materializer, "materializer"); + coordinator.requireMaterializer(admitted.authority(), admitted); + } + /** Completes any durable redo before allowing the first read lease. */ public CommonCheckpointRedoCoordinator.RecoveryAction recoverBeforeServing() throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointTarget.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointTarget.java index 4867fcffb0c..e4029631258 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointTarget.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointTarget.java @@ -3,6 +3,7 @@ import java.util.Arrays; import java.util.Objects; import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.StateArchiveHotBatchDescriptor; /** Immutable identity every authority must reach for one common-checkpoint payload. */ public final class CommonCheckpointTarget { @@ -13,16 +14,18 @@ public final class CommonCheckpointTarget { private final BlockSnapshotMeta lastBlock; private final byte[] parentStateRoot; private final byte[] stateRoot; + private final StateArchiveHotBatchDescriptor archiveBinding; private CommonCheckpointTarget(byte[] formatIdentity, byte[] payloadDigest, BlockSnapshotMeta firstBlock, BlockSnapshotMeta lastBlock, byte[] parentStateRoot, - byte[] stateRoot) { + byte[] stateRoot, StateArchiveHotBatchDescriptor archiveBinding) { this.formatIdentity = copy(formatIdentity); this.payloadDigest = copy(payloadDigest); this.firstBlock = Objects.requireNonNull(firstBlock, "firstBlock"); this.lastBlock = Objects.requireNonNull(lastBlock, "lastBlock"); this.parentStateRoot = copy(parentStateRoot); this.stateRoot = copy(stateRoot); + this.archiveBinding = archiveBinding; } public static CommonCheckpointTarget from(CommonCheckpointPayload payload) { @@ -31,7 +34,9 @@ public static CommonCheckpointTarget from(CommonCheckpointPayload payload) { new CommonCheckpointPayloadCodec().digest(admitted), admitted.getBlocks().get(0).getMeta(), admitted.getBlocks().get(admitted.getBlocks().size() - 1).getMeta(), - admitted.getParentStateRoot(), admitted.getStateRoot()); + admitted.getParentStateRoot(), admitted.getStateRoot(), + admitted.getVersion() == CommonCheckpointPayload.COORDINATION_FORMAT_VERSION + ? admitted.getArchiveBinding() : null); } /** Reconstructs a target identity from a checksummed authority publication record. */ @@ -46,7 +51,8 @@ public static CommonCheckpointTarget restore(byte[] formatIdentity, byte[] paylo } return new CommonCheckpointTarget(requireDigest(formatIdentity, "formatIdentity"), requireDigest(payloadDigest, "payloadDigest"), first, last, - requireDigest(parentStateRoot, "parentStateRoot"), requireDigest(stateRoot, "stateRoot")); + requireDigest(parentStateRoot, "parentStateRoot"), requireDigest(stateRoot, "stateRoot"), + null); } public byte[] getFormatIdentity() { @@ -73,6 +79,13 @@ public byte[] getStateRoot() { return copy(stateRoot); } + public StateArchiveHotBatchDescriptor getArchiveBinding() { + if (archiveBinding == null) { + throw new IllegalStateException("checkpoint target has no Hot Archive binding"); + } + return archiveBinding; + } + @Override public boolean equals(Object object) { if (this == object) { diff --git a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index ef1ee26ad84..875c6b84077 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -2246,6 +2246,21 @@ public long getLatestBlockHeaderNumberFromDB() { return -1; } + /** + * Gets the latest block hash from the persistent Snapshot root, excluding reversible layers. + */ + public Sha256Hash getLatestBlockHeaderHashFromDB() { + try { + byte[] blockHash = Optional.ofNullable(getFromRoot(LATEST_BLOCK_HEADER_HASH)) + .map(BytesCapsule::getData) + .orElseThrow(() -> new IllegalArgumentException("not found block hash")); + return Sha256Hash.wrap(blockHash); + } catch (ItemNotFoundException | BadItemException | IllegalArgumentException e) { + logger.error("Get header hash from DB, {}.", e.getMessage()); + } + return null; + } + public int getStateFlag() { return Optional.ofNullable(getUnchecked(STATE_FLAG)) .map(BytesCapsule::getData) diff --git a/common/src/main/java/org/tron/core/config/args/Storage.java b/common/src/main/java/org/tron/core/config/args/Storage.java index 04a595c2686..c1c5c1e0943 100644 --- a/common/src/main/java/org/tron/core/config/args/Storage.java +++ b/common/src/main/java/org/tron/core/config/args/Storage.java @@ -105,6 +105,10 @@ public class Storage { @Setter private StorageConfig.NativeDbConfig stateArchiveServingIndexDbSettings; + @Getter + @Setter + private StorageConfig.StateArchiveHotStoreConfig stateArchiveHotStoreSettings; + @Getter @Setter private boolean commonCheckpointEnabled; diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index 76078315d95..e9e996dca2f 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -153,6 +153,7 @@ public static class StateArchiveConfig { private long maxSegmentSize = 1073741824L; private int queueCapacity = 256; private NativeDbConfig servingIndex = NativeDbConfig.large(); + private StateArchiveHotStoreConfig hotStore = new StateArchiveHotStoreConfig(); void postProcess() { if (directory == null || directory.trim().isEmpty()) { @@ -167,6 +168,39 @@ void postProcess() { "stateArchive.queueCapacity must be in [1, 65536]"); } servingIndex.validate("storage.stateArchive.servingIndex"); + hotStore.postProcess(); + } + } + + /** Independent, default-off Hot DB limits and native database options. */ + @Getter + @Setter + public static class StateArchiveHotStoreConfig { + + private boolean enabled = false; + private long maxBlocks = 10000L; + private long maxEncodedBytes = 2147483648L; + private int maxFrozenGenerations = 8; + private int yellowFrozenGenerations = 4; + private int redFrozenGenerations = 7; + private NativeDbConfig dbSettings = NativeDbConfig.large(); + + void postProcess() { + validate(); + } + + public void validate() { + if (maxBlocks <= 0 || maxEncodedBytes <= 0) { + throw new IllegalArgumentException( + "stateArchive.hotStore rotation limits must be positive"); + } + if (maxFrozenGenerations <= 0 || yellowFrozenGenerations <= 0 + || redFrozenGenerations <= yellowFrozenGenerations + || redFrozenGenerations > maxFrozenGenerations) { + throw new IllegalArgumentException( + "stateArchive.hotStore frozen watermarks must satisfy 0 < yellow < red <= max"); + } + dbSettings.validate("storage.stateArchive.hotStore.dbSettings"); } } @@ -362,6 +396,10 @@ public static StorageConfig fromConfig(Config config) { throw new IllegalArgumentException( "commonCheckpoint.enabled requires stateArchive.enabled and pathStateRoot.enabled"); } + if (sc.stateArchive.hotStore.enabled && !sc.commonCheckpoint.enabled) { + throw new IllegalArgumentException( + "stateArchive.hotStore.enabled requires commonCheckpoint.enabled"); + } if (sc.commonCheckpoint.enabled && (sc.pathStateRoot.volatileSnapshotBenchmark || sc.pathStateRoot.asyncPrepareBenchmark)) { diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 2ac8bfc9b64..a83b9b416f5 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -154,6 +154,32 @@ storage { backgroundFlushes = 1 backgroundCompactions = 1 } + # Independent Hot DB candidate. It has no production caller while disabled. + stateArchive.hotStore { + enabled = false + maxBlocks = 10000 + maxEncodedBytes = 2147483648 # 2 GiB + maxFrozenGenerations = 8 + yellowFrozenGenerations = 4 + redFrozenGenerations = 7 + dbSettings { + blockSize = 4096 + writeBufferSize = 67108864 + cacheSize = 33554432 + maxOpenFiles = 100 + targetFileSizeBase = 67108864 + maxBytesForLevelBase = 268435456 + bloomBitsPerKey = 10 + maxWriteBufferNumber = 2 + levelNumber = 7 + maxBytesForLevelMultiplier = 10 + level0FileNumCompactionTrigger = 4 + level0SlowdownWritesTrigger = 20 + level0StopWritesTrigger = 36 + backgroundFlushes = 1 + backgroundCompactions = 1 + } + } # Three-authority checkpoint. Admits a verified format-v1 PathState baseline. commonCheckpoint.enabled = false commonCheckpoint.directory = "common-checkpoint" diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java index 7e90e6370fc..405c088e319 100644 --- a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -63,6 +64,13 @@ public void testStateArchiveDefaultsAndOverrides() { assertEquals("state-archive", defaults.getStateArchive().getDirectory()); assertEquals(1073741824L, defaults.getStateArchive().getMaxSegmentSize()); assertEquals(256, defaults.getStateArchive().getQueueCapacity()); + assertFalse(defaults.getStateArchive().getHotStore().isEnabled()); + assertEquals(10000L, defaults.getStateArchive().getHotStore().getMaxBlocks()); + assertEquals(2147483648L, + defaults.getStateArchive().getHotStore().getMaxEncodedBytes()); + assertEquals(8, defaults.getStateArchive().getHotStore().getMaxFrozenGenerations()); + assertEquals(4, defaults.getStateArchive().getHotStore().getYellowFrozenGenerations()); + assertEquals(7, defaults.getStateArchive().getHotStore().getRedFrozenGenerations()); StorageConfig configured = StorageConfig.fromConfig(withRef( "storage.stateArchive { enabled = true, directory = archive-test, " @@ -80,6 +88,8 @@ public void testArchiveNativeDatabaseProfileDefaultsAndOverrides() { defaults.getStateArchive().getServingIndex().getWriteBufferSize()); assertEquals(33554432L, defaults.getStateArchive().getServingIndex().getCacheSize()); + assertNotSame(defaults.getStateArchive().getServingIndex(), + defaults.getStateArchive().getHotStore().getDbSettings()); assertEquals(16777216, defaults.getPathStateRoot().getDbSettings().getSmall().getWriteBufferSize()); assertEquals(67108864, @@ -90,13 +100,25 @@ public void testArchiveNativeDatabaseProfileDefaultsAndOverrides() { StorageConfig configured = StorageConfig.fromConfig(withRef( "storage.pathStateRoot.dbSettings.small.cacheSize = 1048576\n" + "storage.pathStateRoot.dbSettings.giant.maxOpenFiles = 321\n" - + "storage.stateArchive.servingIndex.writeBufferSize = 8388608")); + + "storage.stateArchive.servingIndex.writeBufferSize = 8388608\n" + + "storage.stateArchive.hotStore.dbSettings.writeBufferSize = 4194304")); assertEquals(1048576L, configured.getPathStateRoot().getDbSettings().getSmall().getCacheSize()); assertEquals(321, configured.getPathStateRoot().getDbSettings().getGiant().getMaxOpenFiles()); assertEquals(8388608, configured.getStateArchive().getServingIndex().getWriteBufferSize()); + assertEquals(4194304, + configured.getStateArchive().getHotStore().getDbSettings().getWriteBufferSize()); + assertEquals(67108864, + defaults.getStateArchive().getHotStore().getDbSettings().getWriteBufferSize()); + } + + @Test(expected = IllegalArgumentException.class) + public void testHotStoreRejectsInvalidFrozenWatermarks() { + StorageConfig.fromConfig(withRef( + "storage.stateArchive.hotStore.yellowFrozenGenerations = 7\n" + + "storage.stateArchive.hotStore.redFrozenGenerations = 7")); } @Test(expected = IllegalArgumentException.class) @@ -129,6 +151,23 @@ public void testCommonCheckpointRequiresBothAuthorities() { StorageConfig.fromConfig(withRef("storage.commonCheckpoint.enabled = true")); } + @Test(expected = IllegalArgumentException.class) + public void testHotStoreRequiresCommonCheckpoint() { + StorageConfig.fromConfig(withRef( + "storage.stateArchive.enabled = true\n" + + "storage.stateArchive.hotStore.enabled = true")); + } + + @Test + public void testHotStoreAdmitsOnlyWithCommonCheckpointAuthorities() { + StorageConfig configured = StorageConfig.fromConfig(withRef( + "storage.stateArchive.enabled = true\n" + + "storage.stateArchive.hotStore.enabled = true\n" + + "storage.pathStateRoot.enabled = true\n" + + "storage.commonCheckpoint.enabled = true")); + assertTrue(configured.getStateArchive().getHotStore().isEnabled()); + } + @Test(expected = IllegalArgumentException.class) public void testCommonCheckpointRejectsBenchmarkMode() { StorageConfig.fromConfig(withRef( diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index c8487bc362e..f64f03e181d 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -223,6 +223,7 @@ private static void applyStorageConfig(StorageConfig sc) { PARAMETER.storage.setStateArchiveQueueCapacity(sc.getStateArchive().getQueueCapacity()); PARAMETER.storage.setStateArchiveServingIndexDbSettings( sc.getStateArchive().getServingIndex()); + PARAMETER.storage.setStateArchiveHotStoreSettings(sc.getStateArchive().getHotStore()); PARAMETER.storage.setCommonCheckpointEnabled(sc.getCommonCheckpoint().isEnabled()); PARAMETER.storage.setCommonCheckpointDirectory(sc.getCommonCheckpoint().getDirectory()); PARAMETER.storage.setPathStateRootEnabled(sc.getPathStateRoot().isEnabled()); diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index e3e67dee0a1..f8d2a69a9fa 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -135,6 +135,8 @@ import org.tron.core.db2.archive.SnapshotPathStateTransitionCollector; import org.tron.core.db2.archive.StateArchiveCheckpointMaterializer; import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; +import org.tron.core.db2.archive.StateArchiveHotCheckpointMaterializer; +import org.tron.core.db2.archive.StateArchiveHotStore; import org.tron.core.db2.archive.StateArchiveRuntimeOwner; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.ChainbaseCheckpointMaterializer; @@ -142,6 +144,8 @@ import org.tron.core.db2.core.CommonCheckpointBaselineFile; import org.tron.core.db2.core.CommonCheckpointFile; import org.tron.core.db2.core.CommonCheckpointFormat; +import org.tron.core.db2.core.CommonCheckpointHotRecovery; +import org.tron.core.db2.core.CommonCheckpointRecoveryStateAdapter; import org.tron.core.db2.core.CommonCheckpointRedoCoordinator; import org.tron.core.db2.core.CommonCheckpointRuntime; import org.tron.core.db2.core.CommonCheckpointRuntimeAttachment; @@ -775,6 +779,7 @@ private void initCommonCheckpoint() { byte[] formatIdentity = CommonCheckpointFormat.identity(); PathStatePhysicalOverlayHead pathOwner = null; CommonCheckpointRuntimeAttachment attachment = null; + StateArchiveHotStore hotStore = null; try { PathStateStoreManifest.Engine engine = PathStateStoreManifest.Engine.valueOf( storage.getDbEngine()); @@ -862,18 +867,54 @@ private void initCommonCheckpoint() { snapshots, supplementalStores); PathStateCheckpointMaterializer pathMaterializer = pathOwner.checkpointMaterializer( formatIdentity, baseline); + org.tron.core.config.args.StorageConfig.StateArchiveHotStoreConfig hotConfig = + storage.getStateArchiveHotStoreSettings(); + boolean hotEnabled = hotConfig != null && hotConfig.isEnabled(); + Path hotDirectory = archiveDirectory.resolve("hot"); + if (hotEnabled && !Files.exists(hotDirectory, LinkOption.NOFOLLOW_LINKS)) { + requireEmptyOrMissing(archiveDirectory, "State Archive v2"); + if (!baseline.getHead().equals(canonical)) { + throw new IllegalStateException( + "State Archive Hot DB requires a fresh common checkpoint baseline"); + } + } + CommonCheckpointFile checkpointFile = new CommonCheckpointFile(checkpointDirectory); + StateArchiveHotCheckpointMaterializer hotMaterializer = null; + org.tron.core.db2.core.CommonCheckpointMaterializer archiveMaterializer; + if (hotEnabled) { + hotStore = StateArchiveHotStore.openOrCreate(hotDirectory, formatIdentity, engine, + baseline.getHead().getBlockNumber(), baseline.getHead().getBlockHash(), hotConfig); + hotMaterializer = new StateArchiveHotCheckpointMaterializer(hotStore); + archiveMaterializer = hotMaterializer; + } else { + archiveMaterializer = new StateArchiveCheckpointMaterializer(archiveDirectory, + formatIdentity, baseline, engine); + } CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( - new CommonCheckpointFile(checkpointDirectory), + checkpointFile, new ChainbaseCheckpointMaterializer(checkpointDirectory, formatIdentity, snapshots.getDbs(), baseline), - pathMaterializer, - new StateArchiveCheckpointMaterializer(archiveDirectory, formatIdentity, baseline, - engine)); + pathMaterializer, archiveMaterializer); PathStatePhysicalOverlayHead admittedOwner = pathOwner; + StateArchiveHotCheckpointMaterializer admittedHotMaterializer = hotMaterializer; + StateArchiveHotStore admittedHotStore = hotStore; attachment = CommonCheckpointRuntimeAttachment.open(true, - () -> new CommonCheckpointRuntime(new CommonCheckpointRuntimeOwner(coordinator), - snapshots.getDbs(), archiveDirectory, formatIdentity, engine, latest::pin, - admittedOwner::prepareCommonCheckpointRebase)); + () -> { + CommonCheckpointRuntimeOwner owner = new CommonCheckpointRuntimeOwner(coordinator); + if (admittedHotMaterializer == null) { + return new CommonCheckpointRuntime(owner, snapshots.getDbs(), archiveDirectory, + formatIdentity, engine, latest::pin, + admittedOwner::prepareCommonCheckpointRebase); + } + CommonCheckpointRecoveryStateAdapter recoveryState = + new CommonCheckpointRecoveryStateAdapter(getDynamicPropertiesStore(), + chainBaseManager); + CommonCheckpointHotRecovery recovery = new CommonCheckpointHotRecovery(checkpointFile, + recoveryState, recoveryState, admittedHotMaterializer::reconcilePreparedTail); + return new CommonCheckpointRuntime(owner, snapshots.getDbs(), archiveDirectory, + formatIdentity, engine, latest::pin, + admittedOwner::prepareCommonCheckpointRebase, admittedHotMaterializer, recovery); + }); canonical = currentCanonicalBlockMeta(); if (Files.isRegularFile(pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), @@ -885,8 +926,13 @@ private void initCommonCheckpoint() { } if (Files.isRegularFile(pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), LinkOption.NOFOLLOW_LINKS)) { - requireCommonPublishedAuthorities(checkpointDirectory, archiveDirectory, pathDirectory, - formatIdentity, engine); + if (admittedHotStore == null) { + requireCommonPublishedAuthorities(checkpointDirectory, archiveDirectory, pathDirectory, + formatIdentity, engine); + } else { + requireHotCommonPublishedAuthorities(checkpointDirectory, pathDirectory, + formatIdentity, admittedHotStore); + } } PathStateRootMetadata recovered = admittedOwner.getHead(); if (recovered.getBlockNumber() != canonical.getBlockNumber() @@ -902,6 +948,7 @@ private void initCommonCheckpoint() { commonCheckpointRuntime = attachment; pathOwner = null; attachment = null; + hotStore = null; logger.info("Common checkpoint runtime attached: checkpoint={}, archive={}, path={}, " + "head={}, format={}", checkpointDirectory, archiveDirectory, pathDirectory, canonical.getBlockNumber(), CommonCheckpointFormat.ID); @@ -925,6 +972,14 @@ private void initCommonCheckpoint() { } if (attachment != null) { attachment.close(); + hotStore = null; + } + if (hotStore != null) { + try { + hotStore.close(); + } catch (java.io.IOException closeFailure) { + failure.addSuppressed(closeFailure); + } } if (pathOwner != null) { try { @@ -978,6 +1033,27 @@ private static void requireCommonPublishedAuthorities(Path checkpointDirectory, } } + private static void requireHotCommonPublishedAuthorities(Path checkpointDirectory, + Path pathDirectory, byte[] formatIdentity, StateArchiveHotStore hotStore) + throws java.io.IOException { + ChainbaseCheckpointMaterializer.PublishedHead chain = + ChainbaseCheckpointMaterializer.loadPublishedHead(checkpointDirectory, formatIdentity); + PathStateCheckpointMaterializer.PublishedHead path = + PathStateCheckpointMaterializer.loadPublishedHead(pathDirectory, formatIdentity); + Optional hotTarget = hotStore.getPublishedTargetDigest(); + if (!hotTarget.isPresent() + || chain.getEpoch() != path.getEpoch() + || chain.getBlockNumber() != path.getBlockNumber() + || chain.getBlockNumber() != hotStore.getCommittedHead() + || !Arrays.equals(chain.getBlockHash(), path.getBlockHash()) + || !Arrays.equals(chain.getBlockHash(), hotStore.getCommittedHeadHash()) + || !Arrays.equals(chain.getPayloadDigest(), path.getPayloadDigest()) + || !Arrays.equals(chain.getPayloadDigest(), hotTarget.get()) + || !Arrays.equals(chain.getStateRoot(), path.getStateRoot())) { + throw new java.io.IOException("Hot common checkpoint published authorities differ"); + } + } + private static void requireEmptyOrMissing(Path directory, String label) throws java.io.IOException { if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { diff --git a/framework/src/test/java/org/tron/core/config/args/StorageTest.java b/framework/src/test/java/org/tron/core/config/args/StorageTest.java index 8adfc00f749..c4059bcd122 100644 --- a/framework/src/test/java/org/tron/core/config/args/StorageTest.java +++ b/framework/src/test/java/org/tron/core/config/args/StorageTest.java @@ -81,6 +81,9 @@ public void archiveDatabaseProfilesAreBridgedFromConfiguration() { Assert.assertNotNull(storage.getStateArchiveServingIndexDbSettings()); Assert.assertEquals(64 * 1024 * 1024, storage.getStateArchiveServingIndexDbSettings().getWriteBufferSize()); + Assert.assertNotNull(storage.getStateArchiveHotStoreSettings()); + Assert.assertFalse(storage.getStateArchiveHotStoreSettings().isEnabled()); + Assert.assertEquals(10_000L, storage.getStateArchiveHotStoreSettings().getMaxBlocks()); } @Test diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializerTest.java new file mode 100644 index 00000000000..158df64cc0a --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializerTest.java @@ -0,0 +1,177 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.file.Path; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.core.CommonCheckpointCapture; +import org.tron.core.db2.core.CommonCheckpointFile; +import org.tron.core.db2.core.CommonCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Status; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointRedoCoordinator; +import org.tron.core.db2.core.CommonCheckpointRedoCoordinator.RecoveryAction; +import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateFlushTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class StateArchiveHotCheckpointMaterializerTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void commonParticipantOnlyVerifiesPreparedHotBodiesThenPublishes() throws Exception { + Path root = temporaryFolder.newFolder("hot-materializer").toPath(); + byte[] format = hash(7); + StateArchiveHotStore store = open(root, format); + BlockReverseDiff diff = diff(); + StateArchiveHotBatchDescriptor descriptor = store.planCheckpoint( + Collections.singletonList(diff)); + CommonCheckpointPayload payload = payload(format, descriptor); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + CommonCheckpointCapture capture = CommonCheckpointCapture.create(payload, + Collections.singletonList(diff), descriptor); + StateArchiveHotCheckpointMaterializer materializer = + new StateArchiveHotCheckpointMaterializer(store); + try { + assertEquals(Status.NEEDS_MATERIALIZATION, materializer.inspect(target)); + CommonCheckpointTarget foreignFormat = CommonCheckpointTarget.from( + payload(hash(8), descriptor)); + assertThrows(ArchivePersistenceException.class, + () -> materializer.inspect(foreignFormat)); + assertThrows(IllegalArgumentException.class, + () -> materializer.prepare(target, Collections.singletonList( + new BlockReverseDiff(BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 6_000L), + Collections.emptyList(), hash(62))))); + assertThrows(ArchivePersistenceException.class, () -> store.loadBlock(1)); + assertThrows(java.io.IOException.class, + () -> materializer.materialize(payload, target)); + assertEquals(0, store.getMaterializedHead()); + + assertEquals(target, materializer.prepare(capture)); + assertEquals(target, materializer.prepare(capture)); + assertEquals(Status.MATERIALIZED, materializer.inspect(target)); + assertEquals(1, store.getMaterializedHead()); + assertEquals(0, store.getCommittedHead()); + assertFalse(store.findOldValueAfter("code", new byte[]{1}, 0).isPresent()); + materializer.materialize(payload, target); + materializer.publish(target); + assertEquals(Status.PUBLISHED, materializer.inspect(target)); + assertEquals(1, store.loadBlock(1).getMeta().getBlockNumber()); + } finally { + materializer.close(); + } + + StateArchiveHotStore reopenedStore = open(root, format); + StateArchiveHotCheckpointMaterializer reopened = + new StateArchiveHotCheckpointMaterializer(reopenedStore); + try { + assertEquals(Status.PUBLISHED, reopened.inspect(target)); + reopened.materialize(payload, target); + reopened.publish(target); + assertEquals(1, reopenedStore.getCommittedHead()); + } finally { + reopened.close(); + } + } + + @Test + public void prepreparedHotCaptureCrossesExistingCoordinatorWithoutWalBody() throws Exception { + Path root = temporaryFolder.newFolder("hot-v2-coordinator").toPath(); + byte[] format = hash(9); + StateArchiveHotStore store = open(root.resolve("hot"), format); + BlockReverseDiff diff = diff(); + StateArchiveHotBatchDescriptor descriptor = store.planCheckpoint( + Collections.singletonList(diff)); + CommonCheckpointPayload payload = payload(format, descriptor); + CommonCheckpointCapture capture = CommonCheckpointCapture.create(payload, + Collections.singletonList(diff), descriptor); + StateArchiveHotCheckpointMaterializer archive = + new StateArchiveHotCheckpointMaterializer(store); + CommonCheckpointTarget target = archive.prepare(capture); + try (CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( + new CommonCheckpointFile(root.resolve("wal")), new FakeMaterializer(Authority.CHAINBASE), + new FakeMaterializer(Authority.PATH_STATE), archive)) { + assertEquals(RecoveryAction.COMPLETED_REDO, coordinator.apply(payload)); + assertEquals(Status.PUBLISHED, archive.inspect(target)); + assertEquals(1, store.getCommittedHead()); + assertEquals(RecoveryAction.NO_CHECKPOINT, coordinator.recover()); + } + } + + private StateArchiveHotStore open(Path root, byte[] format) throws java.io.IOException { + return StateArchiveHotStore.openOrCreate(root, format, Engine.LEVELDB, + 0, hash(0), 3, 10, 1024 * 1024); + } + + private static CommonCheckpointPayload payload(byte[] format, + StateArchiveHotBatchDescriptor descriptor) { + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); + byte[] viewDigest = hash(61); + PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); + when(binding.getMeta()).thenReturn(meta); + when(binding.getParentStateRoot()).thenReturn(hash(5)); + when(binding.getStateRoot()).thenReturn(hash(6)); + when(binding.getTransitionPayloadDigest()).thenReturn(hash(71)); + when(binding.getMutationViewDigest()).thenReturn(viewDigest); + PathStateFlushTarget pathState = mock(PathStateFlushTarget.class); + when(pathState.getBlocks()).thenReturn(Collections.singletonList(binding)); + when(pathState.getParentStateRoot()).thenReturn(hash(5)); + when(pathState.getStateRoot()).thenReturn(hash(6)); + when(pathState.getStores()).thenReturn(Collections.emptyList()); + when(pathState.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + return CommonCheckpointPayload.createV2(format, pathState, descriptor, + Collections.emptyList()); + } + + private static BlockReverseDiff diff() { + return new BlockReverseDiff(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L), + Collections.singletonList(new BlockReverseDiff.DbGroup("code", + Collections.singletonList(new BlockReverseDiff.Entry(new byte[]{1}, + OldValue.absent())))), hash(61)); + } + + private static byte[] hash(int marker) { + byte[] hash = new byte[32]; + hash[31] = (byte) marker; + return hash; + } + + private static final class FakeMaterializer implements CommonCheckpointMaterializer { + private final Authority authority; + private Status status = Status.NEEDS_MATERIALIZATION; + + private FakeMaterializer(Authority authority) { + this.authority = authority; + } + + @Override + public Authority authority() { + return authority; + } + + @Override + public Status inspect(CommonCheckpointTarget target) { + return status; + } + + @Override + public void materialize(CommonCheckpointPayload payload, CommonCheckpointTarget target) { + status = Status.MATERIALIZED; + } + + @Override + public void publish(CommonCheckpointTarget target) { + status = Status.PUBLISHED; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java new file mode 100644 index 00000000000..bba0cc77f5c --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java @@ -0,0 +1,129 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.core.CommonCheckpointFile; +import org.tron.core.db2.core.CommonCheckpointHotRecovery; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Verifies recovery after an actual JVM halt, not only an injected in-process exception. */ +public class StateArchiveHotProcessRecoveryTest { + + private static final int HALT_CODE = 91; + private static final byte[] FORMAT = hash(70); + private static final byte[] BASE_HASH = hash(0); + private static final byte[] TARGET = hash(80); + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void jvmHaltAfterHotPrepareBeforeCommonWalTruncatesOrphanOnRestart() + throws Exception { + Path root = temporaryFolder.newFolder("hot-process-recovery").toPath(); + Process child = new ProcessBuilder(javaExecutable(), "-cp", runtimeClasspath(), + StateArchiveHotProcessRecoveryTest.class.getName(), "halt-after-prepare", + root.toString()).redirectErrorStream(true) + .redirectOutput(root.resolve("halt-after-prepare.log").toFile()).start(); + assertTrue("child process timed out", child.waitFor(30, TimeUnit.SECONDS)); + assertEquals(HALT_CODE, child.exitValue()); + + try (StateArchiveHotStore store = open(root.resolve("hot"))) { + assertEquals(StateArchiveHotStore.HotCheckpointStatus.MATERIALIZED, + store.inspectCheckpoint(TARGET)); + assertEquals(0L, store.getCommittedHead()); + assertEquals(1L, store.getMaterializedHead()); + CommonCheckpointHotRecovery recovery = new CommonCheckpointHotRecovery( + new CommonCheckpointFile(root.resolve("common")), + () -> new CommonCheckpointHotRecovery.PersistentDynamicHead(0, BASE_HASH), + ignored -> meta(0, 0), store::reconcilePreparedTail); + CommonCheckpointHotRecovery.Result result = recovery.reconcileBeforeCommonRedo(); + assertEquals(CommonCheckpointHotRecovery.Source.PERSISTED_DYNAMIC, result.getSource()); + assertEquals(1L, result.getRemovedBlocks()); + assertEquals(0L, store.getMaterializedHead()); + assertEquals(StateArchiveHotStore.HotCheckpointStatus.NEEDS_MATERIALIZATION, + store.inspectCheckpoint(TARGET)); + assertThrows(ArchivePersistenceException.class, () -> store.loadBlock(1)); + } + + try (StateArchiveHotStore reopened = open(root.resolve("hot"))) { + assertEquals(0L, reopened.getCommittedHead()); + assertEquals(0L, reopened.getMaterializedHead()); + assertEquals(StateArchiveHotStore.HotCheckpointStatus.NEEDS_MATERIALIZATION, + reopened.inspectCheckpoint(TARGET)); + } + } + + /** Child-process entry point used to leave only the native-sync Hot PREPARED authority. */ + public static void main(String[] args) throws Exception { + if (args.length != 2 || !"halt-after-prepare".equals(args[0])) { + throw new IllegalArgumentException("unknown process recovery mode"); + } + StateArchiveHotStore store = open(Paths.get(args[1]).resolve("hot")); + store.prepareCheckpoint(TARGET, Collections.singletonList(diff(1, 0))); + Runtime.getRuntime().halt(HALT_CODE); + } + + private static StateArchiveHotStore open(Path root) throws Exception { + return StateArchiveHotStore.openOrCreate(root, FORMAT, Engine.LEVELDB, 0, BASE_HASH, + 3, 10, 1024 * 1024); + } + + private static BlockReverseDiff diff(long block, int parent) { + return new BlockReverseDiff(meta(block, parent), Collections.singletonList( + new DbGroup("code", Collections.singletonList( + new Entry(new byte[]{1}, OldValue.absent())))), hash(60 + (int) block)); + } + + private static BlockSnapshotMeta meta(long block, int parent) { + return BlockSnapshotMeta.forBlock(block, hash((int) block), hash(parent), block * 3_000L); + } + + private static String javaExecutable() { + return Paths.get(System.getProperty("java.home"), "bin", "java").toString(); + } + + private static String runtimeClasspath() { + Set entries = new LinkedHashSet<>(); + String configured = System.getProperty("java.class.path", ""); + Collections.addAll(entries, configured.split(java.util.regex.Pattern.quote( + File.pathSeparator))); + for (ClassLoader loader = StateArchiveHotProcessRecoveryTest.class.getClassLoader(); + loader != null; loader = loader.getParent()) { + if (loader instanceof URLClassLoader) { + for (URL url : ((URLClassLoader) loader).getURLs()) { + if ("file".equals(url.getProtocol())) { + try { + entries.add(Paths.get(url.toURI()).toString()); + } catch (java.net.URISyntaxException invalid) { + throw new IllegalStateException("invalid test runtime classpath", invalid); + } + } + } + } + } + return String.join(File.pathSeparator, entries); + } + + private static byte[] hash(int marker) { + byte[] hash = new byte[32]; + hash[31] = (byte) marker; + return hash; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java new file mode 100644 index 00000000000..89073bf7cd0 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java @@ -0,0 +1,415 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.config.args.StorageConfig.NativeDbConfig; +import org.tron.core.config.args.StorageConfig.StateArchiveHotStoreConfig; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class StateArchiveHotStoreTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void appendsSealsQueriesAndReopensAcrossBothEngines() throws Exception { + for (Engine engine : Engine.values()) { + Path root = temporaryFolder.newFolder("hot-" + engine.name()).toPath(); + byte[] format = hash(90); + try (StateArchiveHotStore store = open(root, format, engine, 0, hash(0), 3, 2)) { + store.appendSolidified(Arrays.asList( + diff(1, 0, "code", new byte[]{1}, OldValue.absent()), + diff(2, 1, "code", new byte[]{1}, OldValue.present(new byte[0])))); + assertTrue(store.shouldSealCurrent()); + assertLookup(store.findOldValueAfter("code", new byte[]{1}, 0), 1, + OldValue.absent()); + assertLookup(store.findOldValueAfter("code", new byte[]{1}, 1), 2, + OldValue.present(new byte[0])); + assertEquals(0, store.sealCurrent()); + assertEquals(Collections.singletonList(0L), store.getFrozenGenerationIds()); + assertEquals(1, store.getCurrentGenerationId()); + store.appendSolidified(Collections.singletonList( + diff(3, 2, "account", new byte[]{3}, OldValue.present(new byte[]{33})))); + } + + try (StateArchiveHotStore reopened = open(root, format, engine, 0, hash(0), 3, 2)) { + assertEquals(3, reopened.getCommittedHead()); + assertEquals(1, reopened.getCurrentGenerationId()); + assertEquals(Collections.singletonList(0L), reopened.getFrozenGenerationIds()); + assertEquals(1, reopened.loadBlock(1).getMeta().getBlockNumber()); + assertArrayEquals(hash(61), reopened.loadBlock(1).getMutationViewDigest()); + assertLookup(reopened.findOldValueAfter("account", new byte[]{3}, 2), 3, + OldValue.present(new byte[]{33})); + assertFalse(reopened.findOldValueAfter("missing", new byte[]{1}, 0).isPresent()); + } + + assertTrue(Files.isRegularFile(root.resolve(StateArchiveHotStore.CURRENT))); + assertTrue(Files.isDirectory(root.resolve(StateArchiveHotStore.GENERATIONS) + .resolve("00000000000000000000").resolve(StateArchiveHotStore.DATABASE))); + } + } + + @Test + public void recoversEveryRotationPublicationBoundary() throws Exception { + for (StateArchiveHotStore.Stage failedStage : Arrays.asList( + StateArchiveHotStore.Stage.AFTER_SEAL, + StateArchiveHotStore.Stage.AFTER_NEW_GENERATION, + StateArchiveHotStore.Stage.AFTER_CURRENT)) { + Path root = temporaryFolder.newFolder("fault-" + failedStage).toPath(); + byte[] format = hash(91); + StateArchiveHotStore failed = StateArchiveHotStore.openOrCreate(root, format, + Engine.LEVELDB, 0, hash(0), 2, 1, 1024 * 1024, + stage -> { + if (stage == failedStage) { + throw new IOException("injected " + stage); + } + }); + failed.appendSolidified(Collections.singletonList( + diff(1, 0, "code", new byte[]{1}, OldValue.present(new byte[]{9})))); + assertThrows(IOException.class, failed::sealCurrent); + failed.close(); + + try (StateArchiveHotStore recovered = open(root, format, Engine.LEVELDB, 0, hash(0), + 2, 1)) { + assertEquals(1, recovered.getCurrentGenerationId()); + assertEquals(Collections.singletonList(0L), recovered.getFrozenGenerationIds()); + assertLookup(recovered.findOldValueAfter("code", new byte[]{1}, 0), 1, + OldValue.present(new byte[]{9})); + } + } + } + + @Test + public void reconcilesOnlyCurrentPreparedTailAcrossBothEngines() throws Exception { + for (Engine engine : Engine.values()) { + Path root = temporaryFolder.newFolder("reconcile-" + engine.name()).toPath(); + byte[] format = hash(94); + try (StateArchiveHotStore store = open(root, format, engine, 0, hash(0), 3, 10)) { + store.appendSolidified(Arrays.asList( + diff(1, 0, "code", new byte[]{1}, OldValue.absent()), + diff(2, 1, "code", new byte[]{2}, OldValue.absent()))); + store.prepareCheckpoint(hash(110), Collections.singletonList( + diff(3, 2, "code", new byte[]{3}, OldValue.absent()))); + assertEquals(2, store.getCommittedHead()); + assertEquals(3, store.getMaterializedHead()); + assertThrows(ArchivePersistenceException.class, + () -> store.reconcilePreparedTail(meta(2, 1, 99))); + assertEquals(1, store.reconcilePreparedTail(meta(2, 1, 2))); + assertEquals(2, store.getCommittedHead()); + assertThrows(ArchivePersistenceException.class, () -> store.loadBlock(3)); + assertFalse(store.findOldValueAfter("code", new byte[]{3}, 0).isPresent()); + } + try (StateArchiveHotStore reopened = open(root, format, engine, 0, hash(0), 3, 10)) { + assertEquals(2, reopened.getCommittedHead()); + assertEquals(2, reopened.loadBlock(2).getMeta().getBlockNumber()); + assertThrows(ArchivePersistenceException.class, () -> reopened.loadBlock(3)); + } + } + } + + @Test + public void resumesEveryPreparedTailTruncateBoundary() throws Exception { + for (StateArchiveHotStore.Stage failedStage : Arrays.asList( + StateArchiveHotStore.Stage.AFTER_TRUNCATE_INTENT, + StateArchiveHotStore.Stage.AFTER_TRUNCATE_DELETE_BATCH, + StateArchiveHotStore.Stage.AFTER_TRUNCATE_METADATA)) { + Path root = temporaryFolder.newFolder("truncate-" + failedStage).toPath(); + byte[] format = hash(95); + StateArchiveHotStore failed = StateArchiveHotStore.openOrCreate(root, format, + Engine.LEVELDB, 0, hash(0), 3, 10, 1024 * 1024, + stage -> { + if (stage == failedStage) { + throw new IOException("injected " + stage); + } + }); + failed.appendSolidified(Arrays.asList( + diff(1, 0, "code", new byte[]{1}, OldValue.absent()))); + failed.prepareCheckpoint(hash(111), Collections.singletonList( + diff(2, 1, "code", new byte[]{2}, OldValue.absent()))); + assertThrows(IOException.class, () -> failed.reconcilePreparedTail(meta(1, 0, 1))); + failed.close(); + + try (StateArchiveHotStore recovered = open(root, format, Engine.LEVELDB, 0, hash(0), + 3, 10)) { + assertEquals(1, recovered.getCommittedHead()); + assertEquals(1, recovered.loadBlock(1).getMeta().getBlockNumber()); + assertThrows(ArchivePersistenceException.class, () -> recovered.loadBlock(2)); + } + } + } + + @Test + public void preparesPublishesAndReopensAcrossBothEngines() throws Exception { + for (Engine engine : Engine.values()) { + Path root = temporaryFolder.newFolder("publication-" + engine.name()).toPath(); + byte[] format = hash(97); + byte[] target = hash(112); + BlockReverseDiff block = diff(1, 0, "code", new byte[]{1}, OldValue.absent()); + StateArchiveHotBatchDescriptor descriptor; + try (StateArchiveHotStore store = open(root, format, engine, 0, hash(0), 3, 10)) { + descriptor = store.planCheckpoint(Collections.singletonList(block)); + assertEquals(StateArchiveHotStore.HotCheckpointStatus.NEEDS_MATERIALIZATION, + store.inspectCheckpoint(target)); + store.prepareCheckpoint(target, Collections.singletonList(block)); + assertEquals(StateArchiveHotStore.HotCheckpointStatus.MATERIALIZED, + store.inspectCheckpoint(target, descriptor)); + assertEquals(0, store.getCommittedHead()); + assertEquals(1, store.getMaterializedHead()); + assertEquals(0, store.getStatistics().getPublishedBlock()); + assertTrue(store.getStatistics().hasPreparedCheckpoint()); + assertThrows(ArchivePersistenceException.class, () -> store.loadBlock(1)); + assertFalse(store.findOldValueAfter("code", new byte[]{1}, 0).isPresent()); + assertThrows(ArchivePersistenceException.class, store::sealCurrent); + } + + try (StateArchiveHotStore store = open(root, format, engine, 0, hash(0), 3, 10)) { + assertEquals(StateArchiveHotStore.HotCheckpointStatus.MATERIALIZED, + store.inspectCheckpoint(target, descriptor)); + assertEquals(0, store.getCommittedHead()); + assertThrows(ArchivePersistenceException.class, () -> store.loadBlock(1)); + store.prepareCheckpoint(target, descriptor, Collections.singletonList(block)); + store.publishCheckpoint(target, descriptor); + store.publishCheckpoint(target, descriptor); + assertEquals(StateArchiveHotStore.HotCheckpointStatus.PUBLISHED, + store.inspectCheckpoint(target, descriptor)); + assertEquals(1, store.getCommittedHead()); + assertEquals(1, store.loadBlock(1).getMeta().getBlockNumber()); + assertFalse(store.getStatistics().hasPreparedCheckpoint()); + store.sealCurrent(); + } + + try (StateArchiveHotStore store = open(root, format, engine, 0, hash(0), 3, 10)) { + assertEquals(StateArchiveHotStore.HotCheckpointStatus.PUBLISHED, + store.inspectCheckpoint(target, descriptor)); + assertEquals(1, store.getCommittedHead()); + } + } + } + + @Test + public void recoversPrepareAndPublishNativeBoundaries() throws Exception { + Path preparedRoot = temporaryFolder.newFolder("fault-prepare").toPath(); + byte[] format = hash(98); + byte[] preparedTarget = hash(113); + StateArchiveHotStore failedPrepare = StateArchiveHotStore.openOrCreate(preparedRoot, format, + Engine.LEVELDB, 0, hash(0), 3, 10, 1024 * 1024, + stage -> { + if (stage == StateArchiveHotStore.Stage.AFTER_PREPARE) { + throw new IOException("injected prepare failure"); + } + }); + assertThrows(IOException.class, () -> failedPrepare.prepareCheckpoint(preparedTarget, + Collections.singletonList(diff(1, 0, "code", new byte[]{1}, OldValue.absent())))); + failedPrepare.close(); + try (StateArchiveHotStore recovered = open(preparedRoot, format, Engine.LEVELDB, + 0, hash(0), 3, 10)) { + assertEquals(StateArchiveHotStore.HotCheckpointStatus.MATERIALIZED, + recovered.inspectCheckpoint(preparedTarget)); + assertEquals(0, recovered.getCommittedHead()); + } + + Path publishedRoot = temporaryFolder.newFolder("fault-publish").toPath(); + byte[] publishedTarget = hash(114); + StateArchiveHotStore failedPublish = StateArchiveHotStore.openOrCreate(publishedRoot, format, + Engine.LEVELDB, 0, hash(0), 3, 1, 1024 * 1024, + stage -> { + if (stage == StateArchiveHotStore.Stage.AFTER_PUBLISH) { + throw new IOException("injected publish failure"); + } + }); + failedPublish.prepareCheckpoint(publishedTarget, Collections.singletonList( + diff(1, 0, "code", new byte[]{1}, OldValue.absent()))); + assertThrows(IOException.class, () -> failedPublish.publishCheckpoint(publishedTarget)); + failedPublish.close(); + try (StateArchiveHotStore recovered = open(publishedRoot, format, Engine.LEVELDB, + 0, hash(0), 3, 1)) { + assertEquals(StateArchiveHotStore.HotCheckpointStatus.PUBLISHED, + recovered.inspectCheckpoint(publishedTarget)); + assertEquals(1, recovered.getCommittedHead()); + assertEquals(1, recovered.loadBlock(1).getMeta().getBlockNumber()); + recovered.publishCheckpoint(publishedTarget); + assertEquals(1, recovered.getCurrentGenerationId()); + assertEquals(Collections.singletonList(0L), recovered.getFrozenGenerationIds()); + } + } + + @Test + public void rejectsPreparedRotationBeforeWritingWhenFrozenBacklogIsFull() throws Exception { + Path root = temporaryFolder.newFolder("prepare-full-backlog").toPath(); + try (StateArchiveHotStore store = open(root, hash(99), Engine.LEVELDB, + 0, hash(0), 1, 1)) { + store.appendSolidified(Collections.singletonList( + diff(1, 0, "code", new byte[]{1}, OldValue.absent()))); + store.sealCurrent(); + assertThrows(ArchivePersistenceException.class, () -> store.prepareCheckpoint(hash(115), + Collections.singletonList( + diff(2, 1, "code", new byte[]{2}, OldValue.absent())))); + assertEquals(1, store.getMaterializedHead()); + assertEquals(StateArchiveHotStore.HotCheckpointStatus.NEEDS_MATERIALIZATION, + store.inspectCheckpoint(hash(115))); + } + } + + @Test + public void persistsAndRevalidatesExactPreparedDescriptorAcrossBothEngines() + throws Exception { + for (Engine engine : Engine.values()) { + Path root = temporaryFolder.newFolder("descriptor-" + engine.name()).toPath(); + byte[] format = hash(100); + BlockReverseDiff original = diff(1, 0, "code", new byte[]{1}, OldValue.absent()); + StateArchiveHotBatchDescriptor descriptor; + try (StateArchiveHotStore store = open(root, format, engine, 0, hash(0), 3, 10)) { + descriptor = store.planCheckpoint(Collections.singletonList(original)); + store.prepareCheckpoint(hash(116), descriptor, Collections.singletonList(original)); + } + try (StateArchiveHotStore reopened = open(root, format, engine, 0, hash(0), 3, 10)) { + assertEquals(StateArchiveHotStore.HotCheckpointStatus.MATERIALIZED, + reopened.inspectCheckpoint(hash(116), descriptor)); + } + + Path scratch = temporaryFolder.newFolder("descriptor-other-" + engine.name()).toPath(); + StateArchiveHotBatchDescriptor different; + try (StateArchiveHotStore store = open(scratch, format, engine, 0, hash(0), 3, 10)) { + BlockReverseDiff changed = diff(1, 0, "code", new byte[]{1}, + OldValue.present(new byte[]{9})); + different = store.planCheckpoint(Collections.singletonList(changed)); + } + Path database = root.resolve(StateArchiveHotStore.GENERATIONS) + .resolve("00000000000000000000").resolve(StateArchiveHotStore.DATABASE); + try (StateArchiveIndexDatabase.Writer writer = StateArchiveIndexDatabase.openWriter( + database, engine, NativeDbConfig.large())) { + writer.write(Collections.singletonList(StateArchiveIndexDatabase.put( + "meta/prepared-descriptor".getBytes(StandardCharsets.US_ASCII), + new StateArchiveHotBatchDescriptorCodec().encode(different))), true); + } + assertThrows(ArchivePersistenceException.class, + () -> open(root, format, engine, 0, hash(0), 3, 10)); + } + } + + @Test + public void recoveryCeilingNeverTruncatesFrozenHistory() throws Exception { + Path root = temporaryFolder.newFolder("truncate-frozen").toPath(); + try (StateArchiveHotStore store = open(root, hash(96), Engine.LEVELDB, + 0, hash(0), 3, 1)) { + store.appendSolidified(Collections.singletonList( + diff(1, 0, "code", new byte[]{1}, OldValue.absent()))); + store.sealCurrent(); + store.appendSolidified(Collections.singletonList( + diff(2, 1, "code", new byte[]{2}, OldValue.absent()))); + assertThrows(ArchivePersistenceException.class, + () -> store.reconcilePreparedTail(meta(0, 0, 0))); + assertEquals(2, store.getCommittedHead()); + assertEquals(Collections.singletonList(0L), store.getFrozenGenerationIds()); + } + } + + @Test + public void rejectsGapsParentDriftIdentityDriftAndFrozenOverflow() throws Exception { + Path root = temporaryFolder.newFolder("reject").toPath(); + byte[] format = hash(92); + try (StateArchiveHotStore store = open(root, format, Engine.LEVELDB, 0, hash(0), 1, 1)) { + assertThrows(IllegalArgumentException.class, () -> store.appendSolidified( + Collections.singletonList(diff(2, 0, "code", new byte[]{1}, OldValue.absent())))); + store.appendSolidified(Collections.singletonList( + diff(1, 0, "code", new byte[]{1}, OldValue.absent()))); + store.sealCurrent(); + store.appendSolidified(Collections.singletonList( + diff(2, 1, "code", new byte[]{2}, OldValue.present(new byte[]{1})))); + assertThrows(ArchivePersistenceException.class, store::sealCurrent); + } + + assertThrows(ArchivePersistenceException.class, + () -> open(root, hash(99), Engine.LEVELDB, 0, hash(0), 1, 1)); + assertThrows(ArchivePersistenceException.class, + () -> open(root, format, Engine.ROCKSDB, 0, hash(0), 1, 1)); + } + + @Test + public void dedicatedConfigurationControlsRotationAndBacklogStatistics() throws Exception { + Path disabledRoot = temporaryFolder.newFolder("hot-disabled").toPath(); + StateArchiveHotStoreConfig config = new StateArchiveHotStoreConfig(); + assertThrows(IllegalStateException.class, () -> StateArchiveHotStore.openOrCreate( + disabledRoot, hash(93), Engine.LEVELDB, 0, hash(0), config)); + + config.setEnabled(true); + config.setMaxBlocks(1); + config.setMaxEncodedBytes(1024 * 1024); + config.setMaxFrozenGenerations(3); + config.setYellowFrozenGenerations(1); + config.setRedFrozenGenerations(2); + Path root = temporaryFolder.newFolder("hot-configured").toPath(); + try (StateArchiveHotStore store = StateArchiveHotStore.openOrCreate( + root, hash(93), Engine.LEVELDB, 0, hash(0), config)) { + StateArchiveHotStore.Statistics empty = store.getStatistics(); + assertEquals(StateArchiveHotStore.BacklogLevel.GREEN, empty.getBacklogLevel()); + assertEquals(0, empty.getFrozenGenerations()); + assertEquals(10_000L, new StateArchiveHotStoreConfig().getMaxBlocks()); + + store.appendSolidified(Collections.singletonList( + diff(1, 0, "code", new byte[]{1}, OldValue.absent()))); + assertTrue(store.getStatistics().isRotationDue()); + store.sealCurrent(); + StateArchiveHotStore.Statistics yellow = store.getStatistics(); + assertEquals(StateArchiveHotStore.BacklogLevel.YELLOW, yellow.getBacklogLevel()); + assertEquals(1, yellow.getFrozenGenerations()); + assertEquals(1, yellow.getFrozenBlocks()); + assertTrue(yellow.getFrozenEncodedBytes() > 0); + assertEquals(3, yellow.getMaxFrozenGenerations()); + assertEquals(1, yellow.getYellowFrozenGenerations()); + assertEquals(2, yellow.getRedFrozenGenerations()); + + store.appendSolidified(Collections.singletonList( + diff(2, 1, "code", new byte[]{2}, OldValue.absent()))); + store.sealCurrent(); + assertEquals(StateArchiveHotStore.BacklogLevel.RED, + store.getStatistics().getBacklogLevel()); + } + } + + private StateArchiveHotStore open(Path root, byte[] format, Engine engine, long baseBlock, + byte[] baseHash, int maxFrozen, long maxBlocks) throws IOException { + return StateArchiveHotStore.openOrCreate(root, format, engine, baseBlock, baseHash, + maxFrozen, maxBlocks, 1024 * 1024); + } + + private static BlockReverseDiff diff(long block, int parent, String dbName, byte[] key, + OldValue oldValue) { + return new BlockReverseDiff(BlockSnapshotMeta.forBlock(block, hash((int) block), hash(parent), + block * 3_000), Collections.singletonList(new DbGroup(dbName, + Collections.singletonList(new Entry(key, oldValue)))), hash(60 + (int) block)); + } + + private static BlockSnapshotMeta meta(long block, int parent, int hashMarker) { + return BlockSnapshotMeta.forBlock(block, hash(hashMarker), hash(parent), block * 3_000); + } + + private static void assertLookup(Optional found, + long block, OldValue oldValue) { + assertTrue(found.isPresent()); + assertEquals(block, found.get().getBlockNumber()); + assertEquals(oldValue, found.get().getOldValue()); + } + + private static byte[] hash(int marker) { + byte[] hash = new byte[32]; + hash[31] = (byte) marker; + return hash; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java index e1d0adf119c..2fbd555c8f7 100644 --- a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java @@ -11,6 +11,8 @@ import static org.mockito.Mockito.when; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -18,6 +20,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -35,6 +38,8 @@ import org.tron.core.db2.archive.OldValue; import org.tron.core.db2.archive.StateArchiveCheckpointMaterializer; import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; +import org.tron.core.db2.archive.StateArchiveHotCheckpointMaterializer; +import org.tron.core.db2.archive.StateArchiveHotStore; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.Flusher; import org.tron.core.db2.common.WrappedByteArray; @@ -143,7 +148,7 @@ public void rejectsUnknownStoreForeignFormatAndNonParentTarget() throws Exceptio } @Test - public void payloadFactoryCoalescesSnapshotMutationsWithoutDurableReads() { + public void payloadFactoryCoalescesSnapshotMutationsWithoutDurableReads() throws Exception { MemoryDb code = new MemoryDb("code"); MemoryDb storage = new MemoryDb("storage-row"); Chainbase codeChainbase = new Chainbase(new SnapshotRoot(code)); @@ -195,6 +200,23 @@ public void payloadFactoryCoalescesSnapshotMutationsWithoutDurableReads() { assertEquals(true, storageStore.getMutations().get(0).isDelete()); assertEquals(0, code.getCalls); assertEquals(0, storage.getCalls); + + java.nio.file.Path hotRoot = temporaryFolder.newFolder("capture-v2-hot").toPath(); + try (StateArchiveHotStore hotStore = StateArchiveHotStore.openOrCreate(hotRoot, hash(80), + Engine.LEVELDB, 0, hash(0), 3, 10, 1024 * 1024)) { + CommonCheckpointCapture capture = new CommonCheckpointPayloadFactory().captureV2(hash(80), + databases, 2, new StateArchiveHotCheckpointMaterializer(hotStore)); + assertEquals(CommonCheckpointPayload.COORDINATION_FORMAT_VERSION, + capture.getPayload().getVersion()); + assertEquals(2, capture.getArchiveDiffs().size()); + assertEquals(2, capture.getArchiveBinding().getBlockCount()); + assertEquals(0, hotStore.getMaterializedHead()); + assertThrows(IllegalStateException.class, + () -> capture.getPayload().getBlocks().get(0).getArchiveDiff()); + CommonCheckpointPayloadCodec codec = new CommonCheckpointPayloadCodec(); + assertEquals(capture.getArchiveBinding(), + codec.decode(codec.encode(capture.getPayload())).getArchiveBinding()); + } } @Test @@ -375,11 +397,13 @@ public void runtimeComposesStartupCheckpointRebaseAndPointQuery() throws Excepti runtime.recoverBeforeServing()); CommonCheckpointTarget target = runtime.checkpointAndRebase(1); assertEquals(meta, target.getLastBlock()); + assertThrows(IllegalStateException.class, target::getArchiveBinding); assertEquals(1, timings.size()); CommonCheckpointRuntime.Timing timing = timings.get(0); assertEquals(1, timing.getHead()); assertEquals(1, timing.getBlocks()); assertEquals(1, timing.getPayloadCaptureUs()); + assertEquals(0, timing.getHotPrepareUs()); assertTrue(timing.getOwnerApplyUs() > 0); assertEquals(1, timing.getChainbaseRebasePrepareUs()); assertEquals(1, timing.getPathStateRebasePrepareUs()); @@ -404,6 +428,82 @@ public void runtimeComposesStartupCheckpointRebaseAndPointQuery() throws Excepti assertEquals(CommonCheckpointRuntimeOwner.State.CLOSED, runtime.getState()); } + @Test + public void hotRuntimePreparesV2BeforeWalAndCompletesBothBarriers() throws Exception { + java.nio.file.Path root = temporaryFolder.newFolder("hot-runtime-v2").toPath(); + byte[] format = hash(94); + V2Snapshots snapshots = new V2Snapshots(); + StateArchiveHotStore hotStore = StateArchiveHotStore.openOrCreate(root.resolve("hot"), + format, Engine.LEVELDB, 0, hash(0), 3, 10, 1024 * 1024); + List timings = new ArrayList<>(); + CommonCheckpointRuntime runtime = hotRuntime(root, + new CommonCheckpointFile(root.resolve("wal")), snapshots, hotStore, format, timings); + + assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.NO_CHECKPOINT, + runtime.recoverBeforeServing()); + CommonCheckpointTarget target = runtime.checkpointAndRebase(1); + + assertEquals(snapshots.meta, target.getLastBlock()); + assertEquals(1, target.getArchiveBinding().getBlockCount()); + assertEquals(1, hotStore.getMaterializedHead()); + assertEquals(1, hotStore.getCommittedHead()); + assertFalse(java.nio.file.Files.exists(root.resolve("wal") + .resolve(CommonCheckpointFile.FILE_NAME))); + assertSame(snapshots.codeDatabase.getHead().getRoot(), snapshots.codeDatabase.getHead()); + assertSame(snapshots.propertiesDatabase.getHead().getRoot(), + snapshots.propertiesDatabase.getHead()); + assertEquals(1, timings.size()); + assertEquals(1, timings.get(0).getHotPrepareUs()); + assertThrows(IOException.class, () -> runtime.pinPoint(1)); + runtime.close(); + } + + @Test + public void hotRuntimeRetriesAfterPrepareButBeforeWalPublication() throws Exception { + java.nio.file.Path root = temporaryFolder.newFolder("hot-runtime-retry").toPath(); + byte[] format = hash(95); + V2Snapshots snapshots = new V2Snapshots(); + StateArchiveHotStore firstHot = StateArchiveHotStore.openOrCreate(root.resolve("hot"), + format, Engine.LEVELDB, 0, hash(0), 3, 10, 1024 * 1024); + AtomicBoolean failedOnce = new AtomicBoolean(); + CommonCheckpointFile interruptedFile = new CommonCheckpointFile(root.resolve("wal"), + CommonCheckpointPayloadCodec.DEFAULT_MAX_ENCODED_LENGTH, (stage, path) -> { + if (stage == CommonCheckpointFile.Stage.AFTER_TEMPORARY_FORCE + && failedOnce.compareAndSet(false, true)) { + throw new IOException("injected failure after Hot prepare and temporary WAL force"); + } + }); + CommonCheckpointRuntime interrupted = hotRuntime(root, interruptedFile, snapshots, firstHot, + format, new ArrayList<>()); + interrupted.recoverBeforeServing(); + + assertThrows(IOException.class, () -> interrupted.checkpointAndRebase(1)); + assertEquals(CommonCheckpointRuntimeOwner.State.FAILED, interrupted.getState()); + assertFalse(java.nio.file.Files.exists(root.resolve("wal") + .resolve(CommonCheckpointFile.FILE_NAME))); + assertTrue(java.nio.file.Files.exists(root.resolve("wal") + .resolve(CommonCheckpointFile.TEMPORARY_FILE_NAME))); + assertSame(snapshots.codeLayer, snapshots.codeDatabase.getHead()); + + StateArchiveHotStore recoveredHot = StateArchiveHotStore.openOrCreate(root.resolve("hot"), + format, Engine.LEVELDB, 0, hash(0), 3, 10, 1024 * 1024); + assertEquals(1, recoveredHot.getMaterializedHead()); + assertEquals(0, recoveredHot.getCommittedHead()); + CommonCheckpointRuntime recovered = hotRuntime(root, + new CommonCheckpointFile(root.resolve("wal")), snapshots, recoveredHot, format, + new ArrayList<>()); + assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.NO_CHECKPOINT, + recovered.recoverBeforeServing()); + assertEquals(0, recoveredHot.getMaterializedHead()); + + CommonCheckpointTarget target = recovered.checkpointAndRebase(1); + assertEquals(snapshots.meta, target.getLastBlock()); + assertEquals(1, recoveredHot.getCommittedHead()); + assertFalse(java.nio.file.Files.exists(root.resolve("wal") + .resolve(CommonCheckpointFile.TEMPORARY_FILE_NAME))); + recovered.close(); + } + private Fixture fixture(String name, ChainbaseCheckpointMaterializer.Stage failedStage) throws Exception { java.nio.file.Path root = temporaryFolder.newFolder(name).toPath(); @@ -418,6 +518,27 @@ private Fixture fixture(String name, ChainbaseCheckpointMaterializer.Stage faile return new Fixture(root, code, storage, databases, format, payload, materializer); } + private static CommonCheckpointRuntime hotRuntime(java.nio.file.Path root, + CommonCheckpointFile file, V2Snapshots snapshots, StateArchiveHotStore hotStore, + byte[] format, List timings) { + StateArchiveHotCheckpointMaterializer hotMaterializer = + new StateArchiveHotCheckpointMaterializer(hotStore); + BlockSnapshotMeta persisted = BlockSnapshotMeta.forBlock(0, hash(0), hash(-1), 0); + CommonCheckpointHotRecovery recovery = new CommonCheckpointHotRecovery(file, + () -> new CommonCheckpointHotRecovery.PersistentDynamicHead(0, hash(0)), + ignored -> persisted, hotMaterializer::reconcilePreparedTail); + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator(file, + new ChainbaseCheckpointMaterializer(root.resolve("chainbase"), format, + snapshots.databases), + new PublishingMaterializer(Authority.PATH_STATE), hotMaterializer); + AtomicLong clock = new AtomicLong(); + return new CommonCheckpointRuntime(new CommonCheckpointRuntimeOwner(coordinator), + snapshots.databases, root.resolve("legacy-archive"), format, Engine.LEVELDB, + (blockNumber, blockHash) -> new TestLatest(snapshots.code, blockNumber, blockHash), + target -> () -> { }, hotMaterializer, recovery, () -> clock.addAndGet(1_000L), + timings::add); + } + private static ChainbaseCheckpointMaterializer.FaultHook failAt( ChainbaseCheckpointMaterializer.Stage failedStage) { return (stage, dbName) -> { @@ -556,6 +677,32 @@ private Fixture(java.nio.file.Path root, MemoryDb code, MemoryDb storage, } } + private static final class V2Snapshots { + + private final MemoryDb code = new MemoryDb("code"); + private final MemoryDb properties = new MemoryDb("properties"); + private final Chainbase codeDatabase = new Chainbase(new SnapshotRoot(code)); + private final Chainbase propertiesDatabase = new Chainbase(new SnapshotRoot(properties)); + private final List databases = Arrays.asList(codeDatabase, propertiesDatabase); + private final BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), + 3_000L); + private final SnapshotImpl codeLayer; + + private V2Snapshots() { + byte[] view = hash(41); + PathStateSnapshotDelta path = pathDelta(meta, hash(10), hash(11), view); + BlockReverseDiff archive = new BlockReverseDiff(meta, + Collections.singletonList(new DbGroup("code", Collections.singletonList( + new Entry(new byte[]{1}, OldValue.present(new byte[]{0}))))), view); + codeLayer = append(codeDatabase, meta, archive, path); + SnapshotImpl propertiesLayer = append(propertiesDatabase, meta, archive, path); + codeLayer.put(new byte[]{1}, new byte[]{2}); + propertiesLayer.put("latest_block_header_number".getBytes(StandardCharsets.UTF_8), + ByteBuffer.allocate(Long.BYTES).putLong(1).array()); + propertiesLayer.put("latest_block_header_hash".getBytes(StandardCharsets.UTF_8), hash(1)); + } + } + private static final class PublishingMaterializer implements CommonCheckpointMaterializer { private final Authority authority; @@ -674,7 +821,9 @@ public void remove(byte[] key) { @Override public Iterator> iterator() { - throw new UnsupportedOperationException(); + Map copy = new LinkedHashMap<>(); + values.forEach((key, value) -> copy.put(key.getBytes(), value)); + return copy.entrySet().iterator(); } @Override diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointHotRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointHotRecoveryTest.java new file mode 100644 index 00000000000..10d9646755b --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointHotRecoveryTest.java @@ -0,0 +1,224 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.StateArchiveHotBatchDescriptor; +import org.tron.core.db2.archive.StateArchiveHotCheckpointMaterializer; +import org.tron.core.db2.archive.StateArchiveHotStore; +import org.tron.core.db2.core.CommonCheckpointHotRecovery.PersistentDynamicHead; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; +import org.tron.core.db2.stateroot.PathStateFlushTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class CommonCheckpointHotRecoveryTest { + + private static final byte[] NUMBER_KEY = bytes("latest_block_header_number"); + private static final byte[] HASH_KEY = bytes("latest_block_header_hash"); + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void validWalSelectsTargetBeforeChainbaseBlockExists() throws Exception { + Path root = temporaryFolder.newFolder("wal-authority").toPath(); + BlockSnapshotMeta target = meta(1); + CommonCheckpointFile file = new CommonCheckpointFile(root.resolve("wal")); + file.publish(payload(root.resolve("plan"), target, dynamic(target), Engine.LEVELDB)); + AtomicBoolean dynamicRead = new AtomicBoolean(); + AtomicBoolean reconciled = new AtomicBoolean(); + CommonCheckpointHotRecovery recovery = new CommonCheckpointHotRecovery(file, + () -> { + dynamicRead.set(true); + return new PersistentDynamicHead(0, hash(0)); + }, block -> null, authority -> { + assertEquals(target, authority); + reconciled.set(true); + return 2; + }); + + CommonCheckpointHotRecovery.Result result = recovery.reconcileBeforeCommonRedo(); + + assertEquals(CommonCheckpointHotRecovery.Source.COMMON_WAL, result.getSource()); + assertEquals(target, result.getAuthority()); + assertEquals(2, result.getRemovedBlocks()); + assertFalse(dynamicRead.get()); + assertTrue(reconciled.get()); + } + + @Test + public void walRejectsDynamicAndExistingBlockIdentityDrift() throws Exception { + Path root = temporaryFolder.newFolder("wal-drift").toPath(); + BlockSnapshotMeta target = meta(1); + CommonCheckpointFile wrongDynamicFile = new CommonCheckpointFile( + root.resolve("wrong-dynamic")); + wrongDynamicFile.publish(payload(root.resolve("plan-dynamic"), target, + dynamic(target.getBlockNumber(), hash(9)), Engine.LEVELDB)); + AtomicBoolean reconciled = new AtomicBoolean(); + CommonCheckpointHotRecovery wrongDynamic = new CommonCheckpointHotRecovery( + wrongDynamicFile, () -> new PersistentDynamicHead(0, hash(0)), block -> null, + authority -> { + reconciled.set(true); + return 0; + }); + assertThrows(IOException.class, wrongDynamic::reconcileBeforeCommonRedo); + assertFalse(reconciled.get()); + + CommonCheckpointFile wrongBlockFile = new CommonCheckpointFile(root.resolve("wrong-block")); + wrongBlockFile.publish(payload(root.resolve("plan-block"), target, dynamic(target), + Engine.LEVELDB)); + BlockSnapshotMeta driftedMeta = BlockSnapshotMeta.forBlock(1, hash(1), hash(8), 3_000L); + CommonCheckpointHotRecovery wrongBlock = new CommonCheckpointHotRecovery(wrongBlockFile, + () -> new PersistentDynamicHead(0, hash(0)), block -> driftedMeta, authority -> 0); + assertThrows(IOException.class, wrongBlock::reconcileBeforeCommonRedo); + } + + @Test + public void absentWalUsesPersistedDynamicAndTruncatesOrphanIdempotently() throws Exception { + Path root = temporaryFolder.newFolder("dynamic-authority").toPath(); + byte[] format = hash(70); + BlockSnapshotMeta block = meta(1); + BlockReverseDiff diff = new BlockReverseDiff(block, Collections.emptyList(), hash(40)); + try (StateArchiveHotStore hot = StateArchiveHotStore.openOrCreate(root.resolve("hot"), + format, Engine.LEVELDB, 0, hash(0), 3, 10, 1024 * 1024)) { + StateArchiveHotBatchDescriptor descriptor = hot.planCheckpoint( + Collections.singletonList(diff)); + hot.prepareCheckpoint(hash(90), descriptor, Collections.singletonList(diff)); + BlockSnapshotMeta persisted = BlockSnapshotMeta.forBlock(0, hash(0), hash(-1), 0); + CommonCheckpointHotRecovery recovery = new CommonCheckpointHotRecovery( + new CommonCheckpointFile(root.resolve("wal")), + () -> new PersistentDynamicHead(0, hash(0)), ignored -> persisted, + hot::reconcilePreparedTail); + + CommonCheckpointHotRecovery.Result first = recovery.reconcileBeforeCommonRedo(); + CommonCheckpointHotRecovery.Result second = recovery.reconcileBeforeCommonRedo(); + + assertEquals(CommonCheckpointHotRecovery.Source.PERSISTED_DYNAMIC, first.getSource()); + assertEquals(1, first.getRemovedBlocks()); + assertEquals(0, second.getRemovedBlocks()); + assertEquals(0, hot.getCommittedHead()); + assertEquals(0, hot.getMaterializedHead()); + } + } + + @Test + public void runtimeInvokesOptionalHotRecoveryBeforeCommonRedo() throws Exception { + Path root = temporaryFolder.newFolder("runtime").toPath(); + CommonCheckpointFile file = new CommonCheckpointFile(root.resolve("wal")); + AtomicBoolean reconciled = new AtomicBoolean(); + BlockSnapshotMeta persisted = BlockSnapshotMeta.forBlock(0, hash(0), hash(-1), 0); + StateArchiveHotStore hotStore = StateArchiveHotStore.openOrCreate(root.resolve("hot"), + hash(70), Engine.LEVELDB, 0, hash(0), 3, 10, 1024 * 1024); + StateArchiveHotCheckpointMaterializer hotMaterializer = + new StateArchiveHotCheckpointMaterializer(hotStore); + CommonCheckpointHotRecovery hotRecovery = new CommonCheckpointHotRecovery(file, + () -> new PersistentDynamicHead(0, hash(0)), ignored -> persisted, authority -> { + reconciled.set(true); + return hotMaterializer.reconcilePreparedTail(authority); + }); + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator(file, + materializer(Authority.CHAINBASE), materializer(Authority.PATH_STATE), + hotMaterializer); + CommonCheckpointRuntime runtime = new CommonCheckpointRuntime( + new CommonCheckpointRuntimeOwner(coordinator), + Collections.singletonList(mock(Chainbase.class)), root.resolve("archive"), hash(70), + Engine.LEVELDB, (blockNumber, blockHash) -> { + throw new IOException("latest state is intentionally unavailable"); + }, target -> () -> { }, hotMaterializer, hotRecovery); + + assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.NO_CHECKPOINT, + runtime.recoverBeforeServing()); + assertTrue(reconciled.get()); + assertEquals(CommonCheckpointRuntimeOwner.State.READY, runtime.getState()); + runtime.close(); + } + + @Test + public void absentWalRejectsMissingOrDriftedBlockStoreIdentity() throws Exception { + Path root = temporaryFolder.newFolder("dynamic-drift").toPath(); + CommonCheckpointFile file = new CommonCheckpointFile(root.resolve("wal")); + CommonCheckpointHotRecovery missing = new CommonCheckpointHotRecovery(file, + () -> new PersistentDynamicHead(1, hash(1)), ignored -> null, authority -> 0); + assertThrows(IOException.class, missing::reconcileBeforeCommonRedo); + + CommonCheckpointHotRecovery drifted = new CommonCheckpointHotRecovery(file, + () -> new PersistentDynamicHead(1, hash(9)), ignored -> meta(1), authority -> 0); + assertThrows(IOException.class, drifted::reconcileBeforeCommonRedo); + } + + private CommonCheckpointPayload payload(Path hotPath, BlockSnapshotMeta meta, + List stores, Engine engine) throws Exception { + byte[] viewDigest = hash(40); + BlockReverseDiff diff = new BlockReverseDiff(meta, Collections.emptyList(), viewDigest); + try (StateArchiveHotStore hot = StateArchiveHotStore.openOrCreate(hotPath, hash(70), engine, + 0, hash(0), 3, 10, 1024 * 1024)) { + return CommonCheckpointPayload.createV2(hash(70), pathState(meta, viewDigest), + hot.planCheckpoint(Collections.singletonList(diff)), stores); + } + } + + private static List dynamic(BlockSnapshotMeta meta) { + return dynamic(meta.getBlockNumber(), meta.getBlockHash()); + } + + private static List dynamic(long number, byte[] hash) { + return Collections.singletonList(new CommonCheckpointPayload.StoreMutations("properties", + Arrays.asList(new CommonCheckpointPayload.Mutation(NUMBER_KEY, + ByteBuffer.allocate(Long.BYTES).putLong(number).array()), + new CommonCheckpointPayload.Mutation(HASH_KEY, hash)))); + } + + private static PathStateFlushTarget pathState(BlockSnapshotMeta meta, byte[] viewDigest) { + PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); + when(binding.getMeta()).thenReturn(meta); + when(binding.getParentStateRoot()).thenReturn(hash(50)); + when(binding.getStateRoot()).thenReturn(hash(51)); + when(binding.getTransitionPayloadDigest()).thenReturn(hash(52)); + when(binding.getMutationViewDigest()).thenReturn(viewDigest); + PathStateFlushTarget pathState = mock(PathStateFlushTarget.class); + when(pathState.getBlocks()).thenReturn(Collections.singletonList(binding)); + when(pathState.getParentStateRoot()).thenReturn(hash(50)); + when(pathState.getStateRoot()).thenReturn(hash(51)); + when(pathState.getStores()).thenReturn(Collections.emptyList()); + when(pathState.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + return pathState; + } + + private static CommonCheckpointMaterializer materializer(Authority authority) { + CommonCheckpointMaterializer materializer = mock(CommonCheckpointMaterializer.class); + when(materializer.authority()).thenReturn(authority); + return materializer; + } + + private static BlockSnapshotMeta meta(long block) { + return BlockSnapshotMeta.forBlock(block, hash((int) block), hash((int) block - 1), + block * 3_000L); + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] hash(int marker) { + byte[] value = new byte[32]; + value[31] = (byte) marker; + return value; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointPayloadV2Test.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointPayloadV2Test.java new file mode 100644 index 00000000000..6f1279eb996 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointPayloadV2Test.java @@ -0,0 +1,144 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.ArchivePersistenceException; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.OldValue; +import org.tron.core.db2.archive.StateArchiveHotBatchDescriptor; +import org.tron.core.db2.archive.StateArchiveHotStore; +import org.tron.core.db2.stateroot.PathStateFlushTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class CommonCheckpointPayloadV2Test { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void roundTripsDigestOnlyArchiveBindingWithoutOldValueBody() throws Exception { + byte[] format = hash(7); + byte[] oldValueSentinel = new byte[96]; + for (int index = 0; index < oldValueSentinel.length; index++) { + oldValueSentinel[index] = (byte) (0xa0 + index % 31); + } + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); + byte[] viewDigest = hash(61); + BlockReverseDiff diff = new BlockReverseDiff(meta, + Collections.singletonList(new DbGroup("code", Collections.singletonList( + new Entry(new byte[]{1}, OldValue.present(oldValueSentinel))))), viewDigest); + Path root = temporaryFolder.newFolder("payload-v2").toPath(); + try (StateArchiveHotStore hotStore = StateArchiveHotStore.openOrCreate(root, format, + Engine.LEVELDB, 0, hash(0), 3, 10, 1024 * 1024)) { + StateArchiveHotBatchDescriptor descriptor = hotStore.planCheckpoint( + Collections.singletonList(diff)); + CommonCheckpointPayload payload = CommonCheckpointPayload.createV2(format, + pathState(meta, viewDigest), descriptor, Collections.emptyList()); + CommonCheckpointPayloadCodec codec = new CommonCheckpointPayloadCodec(); + byte[] encoded = codec.encode(payload); + + assertEquals(CommonCheckpointPayload.COORDINATION_FORMAT_VERSION, + ByteBuffer.wrap(encoded, Integer.BYTES, Short.BYTES).getShort()); + assertFalse(contains(encoded, oldValueSentinel)); + CommonCheckpointPayload decoded = codec.decode(encoded); + assertEquals(CommonCheckpointPayload.COORDINATION_FORMAT_VERSION, + decoded.getVersion()); + assertEquals(descriptor, decoded.getArchiveBinding()); + assertArrayEquals(descriptor.getBlocks().get(0).getArchiveRecordDigest(), + decoded.getBlocks().get(0).getArchiveRecordDigest()); + assertThrows(IllegalStateException.class, + () -> decoded.getBlocks().get(0).getArchiveDiff()); + assertArrayEquals(codec.digest(payload), codec.digest(decoded)); + CommonCheckpointFile file = new CommonCheckpointFile(root.resolve("wal")); + file.publish(payload); + assertEquals(descriptor, file.loadRequired().getArchiveBinding()); + file.retire(); + + BlockReverseDiff changedBody = new BlockReverseDiff(meta, + Collections.singletonList(new DbGroup("code", Collections.singletonList( + new Entry(new byte[]{1}, OldValue.absent())))), viewDigest); + assertThrows(ArchivePersistenceException.class, + () -> hotStore.prepareCheckpoint(hash(120), descriptor, + Collections.singletonList(changedBody))); + + CommonCheckpointPayload v1 = CommonCheckpointPayload.create(format, + pathState(meta, viewDigest), Collections.singletonList(diff), Collections.emptyList()); + byte[] encodedV1 = codec.encode(v1); + assertEquals(CommonCheckpointPayload.FORMAT_VERSION, + ByteBuffer.wrap(encodedV1, Integer.BYTES, Short.BYTES).getShort()); + assertTrue(codec.decode(encodedV1).getBlocks().get(0).getArchiveDiff() + .getGroups().get(0).getEntries().get(0).getOldValue().isPresent()); + } + } + + @Test + public void roundTripsExplicitRocksEngineIdentity() throws Exception { + byte[] format = hash(8); + BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); + byte[] viewDigest = hash(62); + BlockReverseDiff diff = new BlockReverseDiff(meta, Collections.emptyList(), viewDigest); + Path root = temporaryFolder.newFolder("payload-v2-rocks").toPath(); + try (StateArchiveHotStore hotStore = StateArchiveHotStore.openOrCreate(root, format, + Engine.ROCKSDB, 0, hash(0), 3, 10, 1024 * 1024)) { + StateArchiveHotBatchDescriptor descriptor = hotStore.planCheckpoint( + Collections.singletonList(diff)); + CommonCheckpointPayload decoded = new CommonCheckpointPayloadCodec().decode( + new CommonCheckpointPayloadCodec().encode(CommonCheckpointPayload.createV2(format, + pathState(meta, viewDigest), descriptor, Collections.emptyList()))); + assertEquals(Engine.ROCKSDB, decoded.getArchiveBinding().getEngine()); + } + } + + private static PathStateFlushTarget pathState(BlockSnapshotMeta meta, byte[] viewDigest) { + PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); + when(binding.getMeta()).thenReturn(meta); + when(binding.getParentStateRoot()).thenReturn(hash(5)); + when(binding.getStateRoot()).thenReturn(hash(6)); + when(binding.getTransitionPayloadDigest()).thenReturn(hash(71)); + when(binding.getMutationViewDigest()).thenReturn(viewDigest); + PathStateFlushTarget pathState = mock(PathStateFlushTarget.class); + when(pathState.getBlocks()).thenReturn(Collections.singletonList(binding)); + when(pathState.getParentStateRoot()).thenReturn(hash(5)); + when(pathState.getStateRoot()).thenReturn(hash(6)); + when(pathState.getStores()).thenReturn(Collections.emptyList()); + when(pathState.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + return pathState; + } + + private static boolean contains(byte[] haystack, byte[] needle) { + for (int offset = 0; offset <= haystack.length - needle.length; offset++) { + boolean equal = true; + for (int index = 0; index < needle.length; index++) { + if (haystack[offset + index] != needle[index]) { + equal = false; + break; + } + } + if (equal) { + return true; + } + } + return false; + } + + private static byte[] hash(int marker) { + byte[] hash = new byte[32]; + hash[31] = (byte) marker; + return hash; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRecoveryStateAdapterTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRecoveryStateAdapterTest.java new file mode 100644 index 00000000000..2019afacca6 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRecoveryStateAdapterTest.java @@ -0,0 +1,84 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.ChainBaseManager; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.BlockCapsule.BlockId; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.exception.BadItemException; +import org.tron.core.exception.ItemNotFoundException; +import org.tron.core.store.DynamicPropertiesStore; + +public class CommonCheckpointRecoveryStateAdapterTest { + + @Test + public void usesPersistentDynamicRootAndReturnsFullBlockMeta() throws Exception { + long number = 73L; + BlockId id = new BlockId(Sha256Hash.wrap(hash(7)), number); + Sha256Hash parent = Sha256Hash.wrap(hash(6)); + DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(99L); + when(dynamic.getLatestBlockHeaderHash()).thenReturn( + new BlockId(Sha256Hash.wrap(hash(9)), 99L)); + when(dynamic.getLatestBlockHeaderNumberFromDB()).thenReturn(number); + when(dynamic.getLatestBlockHeaderHashFromDB()).thenReturn(id); + + BlockCapsule block = mock(BlockCapsule.class); + when(block.getNum()).thenReturn(number); + when(block.getBlockId()).thenReturn(id); + when(block.getParentHash()).thenReturn(parent); + when(block.getTimeStamp()).thenReturn(1234L); + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getBlockByNum(number)).thenReturn(block); + + CommonCheckpointRecoveryStateAdapter adapter = + new CommonCheckpointRecoveryStateAdapter(dynamic, chainBase); + CommonCheckpointHotRecovery.PersistentDynamicHead head = adapter.load(); + BlockSnapshotMeta meta = adapter.loadIfPresent(number); + + assertEquals(number, head.getBlockNumber()); + assertArrayEquals(id.getBytes(), head.getBlockHash()); + assertEquals(number, meta.getBlockNumber()); + assertArrayEquals(id.getBytes(), meta.getBlockHash()); + assertArrayEquals(parent.getBytes(), meta.getParentHash()); + assertEquals(1234L, meta.getTimestamp()); + } + + @Test + public void rejectsUnavailablePersistentDynamicIdentity() throws Exception { + DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); + when(dynamic.getLatestBlockHeaderNumberFromDB()).thenReturn(-1L); + CommonCheckpointRecoveryStateAdapter adapter = + new CommonCheckpointRecoveryStateAdapter(dynamic, mock(ChainBaseManager.class)); + + assertThrows(java.io.IOException.class, adapter::load); + } + + @Test + public void distinguishesMissingAndCorruptBlockStoreRecords() throws Exception { + long number = 73L; + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getBlockByNum(number)).thenThrow(new ItemNotFoundException()); + CommonCheckpointRecoveryStateAdapter adapter = new CommonCheckpointRecoveryStateAdapter( + mock(DynamicPropertiesStore.class), chainBase); + assertNull(adapter.loadIfPresent(number)); + + doThrow(new BadItemException()).when(chainBase).getBlockByNum(number); + assertThrows(java.io.IOException.class, () -> adapter.loadIfPresent(number)); + } + + private static byte[] hash(int seed) { + byte[] value = new byte[32]; + java.util.Arrays.fill(value, (byte) seed); + return value; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index 5be947bdf95..73339c466c5 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -27,6 +27,7 @@ import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.config.args.Storage; +import org.tron.core.config.args.StorageConfig.StateArchiveHotStoreConfig; import org.tron.core.db.Manager; import org.tron.core.db2.ISession; import org.tron.core.db2.archive.BlockSnapshotMeta; @@ -344,6 +345,7 @@ public void commonCheckpointFreshFlushAndRestartUseOneDurableBoundary() throws E assertTrue(Files.isRegularFile(output.resolve("path-state-root") .resolve(PathStateCheckpointMaterializer.COMMON_MODE_FILE))); assertFalse(Files.exists(output.resolve("path-state-root/CURRENT"))); + assertFalse(Files.exists(output.resolve("state-archive/hot"))); BlockId childId = new BlockId(Sha256Hash.wrap(bytes(32)), 101L); byte[] childHash = childId.getBytes(); @@ -391,6 +393,52 @@ public void commonCheckpointFreshFlushAndRestartUseOneDurableBoundary() throws E invoke(manager, "closePathStateRoot"); } + @Test + public void hotCommonCheckpointUsesDualGateAndPersistentRecoverySources() throws Exception { + Path output = temporaryFolder.newFolder("hot-common-checkpoint-startup").toPath(); + long baseNumber = 100L; + BlockId baseId = new BlockId(Sha256Hash.wrap(bytes(61)), baseNumber); + DynamicPropertiesStore dynamic = mock(DynamicPropertiesStore.class); + when(dynamic.getLatestBlockHeaderNumber()).thenReturn(baseNumber); + when(dynamic.getLatestBlockHeaderHash()).thenReturn(baseId); + when(dynamic.getLatestBlockHeaderTimestamp()).thenReturn(300L); + when(dynamic.getLatestBlockHeaderNumberFromDB()).thenReturn(baseNumber); + when(dynamic.getLatestBlockHeaderHashFromDB()).thenReturn(baseId); + when(dynamic.getAllowAccountAssetOptimizationFromRoot()).thenReturn(1L); + BlockCapsule baseBlock = mock(BlockCapsule.class); + when(baseBlock.getNum()).thenReturn(baseNumber); + when(baseBlock.getBlockId()).thenReturn(baseId); + when(baseBlock.getParentHash()).thenReturn(Sha256Hash.wrap(bytes(60))); + when(baseBlock.getTimeStamp()).thenReturn(300L); + ChainBaseManager chainBase = mock(ChainBaseManager.class); + when(chainBase.getDynamicPropertiesStore()).thenReturn(dynamic); + when(chainBase.getBlockByNum(baseNumber)).thenReturn(baseBlock); + when(chainBase.getAccountAssetStore()).thenReturn(mock(AccountAssetStore.class)); + + Manager manager = new Manager(); + setChainBaseManager(manager, chainBase); + withHotCommonConfig(output, () -> { + SnapshotManager snapshots = new SnapshotManager(""); + AtomicInteger closed = new AtomicInteger(); + for (PathStateParticipantDescriptor.StoreIdentity participant + : PathStateParticipantDescriptor.current().getStores()) { + snapshots.getDbs().add(emptyNativeStore(participant.getDbName(), baseNumber, + baseId.getBytes(), closed)); + } + snapshots.enable(); + snapshots.setUnChecked(false); + setField(manager, "revokingStore", snapshots); + invoke(manager, "initCommonCheckpoint"); + }); + + assertNotNull(manager.getCommonCheckpointRuntime()); + assertTrue(Files.isRegularFile(output.resolve("state-archive/hot/CURRENT"))); + assertFalse(Files.exists(output.resolve("state-archive/READABLE"))); + assertFalse(Files.exists(output.resolve("state-archive/checkpoint-targets"))); + invoke(manager, "closeCommonCheckpoint"); + invoke(manager, "closePathStateRoot"); + } + @Test public void commonCheckpointAdoptsCompatibleLegacyCurrentWithoutRebuild() throws Exception { Path output = temporaryFolder.newFolder("common-checkpoint-legacy-current").toPath(); @@ -514,6 +562,11 @@ private static void withConfig(Path output, boolean enabled, ThrowingRunnable ac } private static void withCommonConfig(Path output, ThrowingRunnable action) throws Exception { + withCommonConfig(output, false, action); + } + + private static void withCommonConfig(Path output, boolean hotEnabled, + ThrowingRunnable action) throws Exception { CommonParameter args = CommonParameter.getInstance(); Storage oldStorage = args.getStorage(); String oldOutput = args.outputDirectory; @@ -524,6 +577,9 @@ private static void withCommonConfig(Path output, ThrowingRunnable action) throw storage.setDbEngine("ROCKSDB"); storage.setStateArchiveEnabled(true); storage.setStateArchiveDirectory("state-archive"); + StateArchiveHotStoreConfig hotConfig = new StateArchiveHotStoreConfig(); + hotConfig.setEnabled(hotEnabled); + storage.setStateArchiveHotStoreSettings(hotConfig); storage.setCommonCheckpointEnabled(true); storage.setCommonCheckpointDirectory("common-checkpoint"); storage.setPathStateRootEnabled(true); @@ -540,6 +596,10 @@ private static void withCommonConfig(Path output, ThrowingRunnable action) throw } } + private static void withHotCommonConfig(Path output, ThrowingRunnable action) throws Exception { + withCommonConfig(output, true, action); + } + private static void setChainBaseManager(Manager manager, ChainBaseManager chainBase) throws Exception { setField(manager, "chainBaseManager", chainBase); From 61b9701941de8d1f1828b7dbb71296e678bb435e Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 8 Sep 2026 10:06:16 +0800 Subject: [PATCH 125/161] feat(db): add rocksdb hot store column families --- .../StateArchiveCheckpointServingIndex.java | 13 +- .../db2/archive/StateArchiveHotStore.java | 59 ++- .../archive/StateArchiveIndexDatabase.java | 417 +++++++++++++++++- .../org/tron/core/config/args/Storage.java | 8 + .../tron/core/config/args/StorageConfig.java | 18 + common/src/main/resources/reference.conf | 6 + .../core/config/args/StorageConfigTest.java | 17 +- .../java/org/tron/core/config/args/Args.java | 3 + .../main/java/org/tron/core/db/Manager.java | 60 ++- framework/src/main/resources/config.conf | 4 + .../org/tron/core/config/args/ArgsTest.java | 22 + .../tron/core/config/args/StorageTest.java | 3 + .../StateArchiveHotProcessRecoveryTest.java | 67 ++- .../db2/archive/StateArchiveHotStoreTest.java | 72 ++- .../StateArchiveIndexDatabaseTest.java | 13 + 15 files changed, 715 insertions(+), 67 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java index 83033f627e3..c1ac1ad1716 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointServingIndex.java @@ -112,10 +112,17 @@ static Reader openTrustedReader(Path archiveDirectory, CommonCheckpointTarget ta static Engine configuredEngine() { org.tron.core.config.args.Storage storage = CommonParameter.getInstance().getStorage(); - if (storage == null || storage.getDbEngine() == null) { - return Engine.LEVELDB; + if (storage == null) { + return Engine.ROCKSDB; } - return Engine.valueOf(storage.getDbEngine().toUpperCase(Locale.ROOT)); + String configured = storage.getStateArchiveServingIndexEngine(); + if (configured == null) { + configured = storage.getDbEngine(); + } + if (configured == null) { + return Engine.ROCKSDB; + } + return Engine.valueOf(configured.trim().toUpperCase(Locale.ROOT)); } private static void requireParent(Marker marker, CommonCheckpointTarget target) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java index 257cf5a2efb..0e65aca6af1 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java @@ -58,6 +58,8 @@ public final class StateArchiveHotStore implements Closeable { private static final short RECORD_VIEW_DIGEST = 1; private static final byte[] BODY_PREFIX = new byte[]{0x42}; private static final byte[] INDEX_PREFIX = new byte[]{0x4b}; + private static final String BLOCKS_COLUMN = + StateArchiveIndexDatabase.HOT_BLOCKS_COLUMN_FAMILY; private static final byte[] META_FORMAT = bytes("meta/format"); private static final byte[] META_ID = bytes("meta/id"); private static final byte[] META_BASE_BLOCK = bytes("meta/base-block"); @@ -205,7 +207,8 @@ private void initialize(long baseBlockNumber, byte[] baseBlockHash) throws IOExc } loadAndValidateGenerations(selected); current = selected; - writer = StateArchiveIndexDatabase.openWriter(generationPath(current.id).resolve(DATABASE), + writer = StateArchiveIndexDatabase.openHotWriter( + generationPath(current.id).resolve(DATABASE), engine, dbSettings); resumeTruncateIfPresent(); } @@ -325,14 +328,16 @@ private void append(List diffs, byte[] preparedTarget, throw new IllegalArgumentException("Hot Archive block sequence is not contiguous"); } byte[] bodyKey = bodyKey(meta.getBlockNumber()); - requireNewKey(bodyKey, newKeys); + requireNewKey(BLOCKS_COLUMN, bodyKey, newKeys); byte[] record = encodeRecord(diff); - mutations.add(StateArchiveIndexDatabase.put(bodyKey, record)); + mutations.add(StateArchiveIndexDatabase.put(BLOCKS_COLUMN, bodyKey, record)); for (DbGroup group : diff.getGroups()) { + String storeColumn = storeColumn(group.getDbName()); for (Entry entry : group.getEntries()) { byte[] indexKey = indexKey(group.getDbName(), entry.getKey(), meta.getBlockNumber()); - requireNewKey(indexKey, newKeys); - mutations.add(StateArchiveIndexDatabase.put(indexKey, longBytes(meta.getBlockNumber()))); + requireNewKey(storeColumn, indexKey, newKeys); + mutations.add(StateArchiveIndexDatabase.put(storeColumn, indexKey, + longBytes(meta.getBlockNumber()))); } } if (startBlock < 0) { @@ -498,7 +503,7 @@ public synchronized long sealCurrent() throws IOException { faultHook.after(Stage.AFTER_CURRENT); frozen.add(current); current = next; - writer = StateArchiveIndexDatabase.openWriter(generationPath(nextId).resolve(DATABASE), + writer = StateArchiveIndexDatabase.openHotWriter(generationPath(nextId).resolve(DATABASE), engine, dbSettings); return frozenId; } @@ -521,6 +526,12 @@ public synchronized Optional findOldValueAfter(String dbName, byte[] return Optional.empty(); } byte[] prefix = indexPrefix(dbName, rawKey); + String storeColumn; + try { + storeColumn = storeColumn(dbName); + } catch (IllegalArgumentException unknownStore) { + return Optional.empty(); + } byte[] seek = ByteBuffer.allocate(prefix.length + Long.BYTES).put(prefix) .putLong(targetBlock + 1).array(); long candidate = Long.MAX_VALUE; @@ -528,9 +539,9 @@ public synchronized Optional findOldValueAfter(String dbName, byte[] if (generation.blockCount == 0 || generation.publishedBlock <= targetBlock) { continue; } - try (StateArchiveIndexDatabase.Reader reader = StateArchiveIndexDatabase.openReader( + try (StateArchiveIndexDatabase.Reader reader = StateArchiveIndexDatabase.openHotReader( generationPath(generation.id).resolve(DATABASE), engine, dbSettings)) { - StateArchiveIndexDatabase.KeyValue found = reader.seek(seek); + StateArchiveIndexDatabase.KeyValue found = reader.seek(storeColumn, seek); if (found != null && isIndexCandidate(found.getKey(), prefix)) { long block = ByteBuffer.wrap(found.getKey(), prefix.length, Long.BYTES).getLong(); if (!Arrays.equals(found.getValue(), longBytes(block))) { @@ -556,9 +567,9 @@ public synchronized BlockReverseDiff loadBlock(long blockNumber) throws IOExcept || blockNumber > generation.publishedBlock) { continue; } - try (StateArchiveIndexDatabase.Reader reader = StateArchiveIndexDatabase.openReader( + try (StateArchiveIndexDatabase.Reader reader = StateArchiveIndexDatabase.openHotReader( generationPath(generation.id).resolve(DATABASE), engine, dbSettings)) { - byte[] encoded = reader.get(bodyKey(blockNumber)); + byte[] encoded = reader.get(BLOCKS_COLUMN, bodyKey(blockNumber)); if (encoded == null) { throw new ArchivePersistenceException("Hot Archive body is missing for indexed block"); } @@ -704,7 +715,7 @@ private long completeTruncate(RecoveryCeiling ceiling) throws IOException { .published(current.publishedTarget, current.publishedDescriptor); long removed = current.endBlock - ceiling.blockNumber; for (long block = current.endBlock; block > ceiling.blockNumber; block--) { - byte[] body = writer.get(bodyKey(block)); + byte[] body = writer.get(BLOCKS_COLUMN, bodyKey(block)); if (body == null) { continue; } @@ -716,11 +727,11 @@ private long completeTruncate(RecoveryCeiling ceiling) throws IOException { List deletes = new ArrayList<>(); for (DbGroup group : diff.getGroups()) { for (Entry entry : group.getEntries()) { - deletes.add(StateArchiveIndexDatabase.delete( + deletes.add(StateArchiveIndexDatabase.delete(storeColumn(group.getDbName()), indexKey(group.getDbName(), entry.getKey(), block))); } } - deletes.add(StateArchiveIndexDatabase.delete(bodyKey(block))); + deletes.add(StateArchiveIndexDatabase.delete(BLOCKS_COLUMN, bodyKey(block))); writer.write(deletes, true); faultHook.after(Stage.AFTER_TRUNCATE_DELETE_BATCH); } @@ -739,7 +750,7 @@ private GenerationMeta rebuildCurrentThrough(long ceiling) throws IOException { GenerationMeta retained = GenerationMeta.empty(current.id, current.baseBlock, current.baseHash); for (long block = current.baseBlock + 1; block <= ceiling; block++) { - byte[] record = writer.get(bodyKey(block)); + byte[] record = writer.get(BLOCKS_COLUMN, bodyKey(block)); if (record == null) { throw new ArchivePersistenceException( "Hot Archive retained prefix body is missing"); @@ -761,7 +772,7 @@ private void requireCurrentIdentity(long blockNumber, byte[] expectedHash) throw if (blockNumber == current.baseBlock) { actual = current.baseHash; } else { - byte[] record = writer.get(bodyKey(blockNumber)); + byte[] record = writer.get(BLOCKS_COLUMN, bodyKey(blockNumber)); if (record == null) { throw new ArchivePersistenceException( "Hot Archive recovery ceiling body is missing"); @@ -798,7 +809,7 @@ private GenerationMeta createGeneration(long id, long baseBlock, byte[] baseHash StateArchiveIndexEngineManifest.openOrCreate(path, engine); GenerationMeta meta = GenerationMeta.empty(id, baseBlock, baseHash) .published(publishedTarget, publishedDescriptor); - try (StateArchiveIndexDatabase.Writer created = StateArchiveIndexDatabase.openWriter( + try (StateArchiveIndexDatabase.Writer created = StateArchiveIndexDatabase.openHotWriter( path.resolve(DATABASE), engine, dbSettings)) { created.write(meta.createMutations(formatIdentity), true); } @@ -840,7 +851,7 @@ private void loadAndValidateGenerations(GenerationMeta selected) throws IOExcept private GenerationMeta loadGeneration(long id) throws IOException { Path path = generationPath(id); StateArchiveIndexEngineManifest.require(path, engine); - try (StateArchiveIndexDatabase.Reader reader = StateArchiveIndexDatabase.openReader( + try (StateArchiveIndexDatabase.Reader reader = StateArchiveIndexDatabase.openHotReader( path.resolve(DATABASE), engine, dbSettings)) { byte[] storedFormat = required(reader, META_FORMAT, HASH_LENGTH); if (!Arrays.equals(formatIdentity, storedFormat)) { @@ -908,7 +919,7 @@ private void requireExactDescriptor(StateArchiveIndexDatabase.Reader reader, long encodedBytes = 0; byte[] contentDigest = descriptor.getParentContentDigest(); for (StateArchiveHotBatchDescriptor.BlockDigest block : descriptor.getBlocks()) { - byte[] record = reader.get(bodyKey(block.getMeta().getBlockNumber())); + byte[] record = reader.get(BLOCKS_COLUMN, bodyKey(block.getMeta().getBlockNumber())); if (record == null) { throw new ArchivePersistenceException("Hot Archive descriptor body is missing"); } @@ -1074,12 +1085,20 @@ private BlockReverseDiff decodeRecord(byte[] encoded) throws IOException { } } - private void requireNewKey(byte[] key, Set newKeys) throws IOException { - if (!newKeys.add(new ByteArrayKey(key)) || writer.get(key) != null) { + private void requireNewKey(String columnFamily, byte[] key, Set newKeys) + throws IOException { + byte[] column = columnFamily.getBytes(StandardCharsets.UTF_8); + byte[] qualified = ByteBuffer.allocate(Integer.BYTES + column.length + key.length) + .putInt(column.length).put(column).put(key).array(); + if (!newKeys.add(new ByteArrayKey(qualified)) || writer.get(columnFamily, key) != null) { throw new ArchivePersistenceException("Hot Archive append would overwrite existing data"); } } + private static String storeColumn(String dbName) { + return StateArchiveIndexDatabase.hotStoreColumnFamily(dbName); + } + private static OldValue findExactOldValue(BlockReverseDiff diff, String dbName, byte[] rawKey) throws IOException { for (DbGroup group : diff.getGroups()) { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java index ab02bdd347f..cb8c4c4ec40 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java @@ -4,16 +4,23 @@ import java.io.Closeable; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.OptionalLong; +import java.util.Set; import org.iq80.leveldb.DB; import org.iq80.leveldb.DBIterator; import org.iq80.leveldb.ReadOptions; @@ -28,8 +35,14 @@ final class StateArchiveIndexDatabase { private static final Logger logger = LoggerFactory.getLogger("DB"); + static final String DEFAULT_COLUMN_FAMILY = "default"; + static final String HOT_BLOCKS_COLUMN_FAMILY = "blocks"; private static final Map LEVEL_DATABASES = new HashMap<>(); private static final Map ROCKS_DATABASES = new HashMap<>(); + private static final Map HOT_ROCKS_DATABASES = new HashMap<>(); + private static final List HOT_COLUMN_FAMILIES = createHotColumnFamilies(); + private static final Set HOT_COLUMN_FAMILY_SET = + Collections.unmodifiableSet(new LinkedHashSet<>(HOT_COLUMN_FAMILIES)); private StateArchiveIndexDatabase() { } @@ -58,6 +71,45 @@ static Writer openWriter(Path directory, Engine engine, NativeDbConfig suppliedC : new RocksWriter(acquireRocks(path, true, config)); } + static Reader openHotReader(Path directory, Engine engine, NativeDbConfig suppliedConfig) + throws IOException { + Path path = normalize(directory); + NativeDbConfig config = Objects.requireNonNull(suppliedConfig, "suppliedConfig"); + return engine == Engine.LEVELDB ? new LevelReader(acquireLevel(path, false, config)) + : new HotRocksReader(acquireHotRocks(path, false, config)); + } + + static Writer openHotWriter(Path directory, Engine engine, NativeDbConfig suppliedConfig) + throws IOException { + Path path = normalize(directory); + NativeDbConfig config = Objects.requireNonNull(suppliedConfig, "suppliedConfig"); + return engine == Engine.LEVELDB ? new LevelWriter(acquireLevel(path, true, config)) + : new HotRocksWriter(acquireHotRocks(path, true, config)); + } + + static String hotStoreColumnFamily(String dbName) { + int storeId = ArchiveParticipantDescriptor.current().getStoreId(dbName); + return String.format("store-s%04d-%s", storeId, dbName); + } + + static List hotColumnFamilies() { + return HOT_COLUMN_FAMILIES; + } + + private static List createHotColumnFamilies() { + List stores = new ArrayList<>( + ArchiveParticipantDescriptor.current().getParticipants()); + stores.sort(Comparator.comparingInt( + ArchiveParticipantDescriptor.current()::getStoreId)); + List names = new ArrayList<>(); + names.add(DEFAULT_COLUMN_FAMILY); + names.add(HOT_BLOCKS_COLUMN_FAMILY); + for (String store : stores) { + names.add(hotStoreColumnFamily(store)); + } + return Collections.unmodifiableList(names); + } + static void checkpoint(Path source, Path target, Engine engine) throws IOException { Path from = normalize(source); Path to = normalize(target); @@ -119,17 +171,40 @@ private static void checkpointLevel(Path source, Path target) throws IOException } static Mutation put(byte[] key, byte[] value) { - return new Mutation(key, value); + return put(DEFAULT_COLUMN_FAMILY, key, value); + } + + static Mutation put(String columnFamily, byte[] key, byte[] value) { + return new Mutation(columnFamily, key, value); } static Mutation delete(byte[] key) { - return new Mutation(key, null); + return delete(DEFAULT_COLUMN_FAMILY, key); + } + + static Mutation delete(String columnFamily, byte[] key) { + return new Mutation(columnFamily, key, null); } private static Path normalize(Path directory) { return Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize(); } + private static String requireColumnFamily(String columnFamily) { + String name = Objects.requireNonNull(columnFamily, "columnFamily"); + if (!HOT_COLUMN_FAMILY_SET.contains(name)) { + throw new IllegalArgumentException("Unknown Hot Archive column family: " + name); + } + return name; + } + + private static void requireDefaultColumnFamily(String columnFamily) { + if (!DEFAULT_COLUMN_FAMILY.equals(requireColumnFamily(columnFamily))) { + throw new IllegalArgumentException( + "Flat Archive database only supports the default column family"); + } + } + private static synchronized SharedLevelDatabase acquireLevel(Path directory, boolean create, NativeDbConfig config) throws IOException { @@ -184,6 +259,28 @@ private static synchronized SharedRocksDatabase acquireRocks(Path directory, boo return shared; } + private static synchronized SharedHotRocksDatabase acquireHotRocks(Path directory, + boolean create, NativeDbConfig config) throws IOException { + SharedHotRocksDatabase shared = HOT_ROCKS_DATABASES.get(directory); + if (shared == null) { + RocksColumnFamilyResources resources = new RocksColumnFamilyResources(config, create); + try { + resources.open(directory); + shared = new SharedHotRocksDatabase(directory, resources); + } catch (org.rocksdb.RocksDBException | RuntimeException failure) { + resources.close(); + throw new IOException("Failed to open RocksDB Hot Archive column families", failure); + } + HOT_ROCKS_DATABASES.put(directory, shared); + logger.info("Hot Archive native database opened: directory={}, engine=ROCKSDB, " + + "columnFamilies={}, blockBytes={}, writeBufferBytes={}, cacheBytes={}, " + + "maxOpenFiles={}", directory, resources.handles.size(), config.getBlockSize(), + config.getWriteBufferSize(), config.getCacheSize(), config.getMaxOpenFiles()); + } + shared.references++; + return shared; + } + private static synchronized void releaseRocks(SharedRocksDatabase shared) { if (--shared.references != 0) { return; @@ -193,6 +290,14 @@ private static synchronized void releaseRocks(SharedRocksDatabase shared) { shared.resources.close(); } + private static synchronized void releaseHotRocks(SharedHotRocksDatabase shared) { + if (--shared.references != 0) { + return; + } + HOT_ROCKS_DATABASES.remove(shared.directory); + shared.resources.close(); + } + private static void checkpointRocks(Path source, Path target) throws IOException { SharedRocksDatabase shared = acquireRocks(source, false, configuredOptions()); try (org.rocksdb.Checkpoint checkpoint = org.rocksdb.Checkpoint.create(shared.database)) { @@ -221,9 +326,17 @@ private static synchronized void releaseLevel(SharedLevelDatabase shared) throws interface Reader extends Closeable { - byte[] get(byte[] key) throws IOException; + default byte[] get(byte[] key) throws IOException { + return get(DEFAULT_COLUMN_FAMILY, key); + } + + byte[] get(String columnFamily, byte[] key) throws IOException; + + default KeyValue seek(byte[] key) throws IOException { + return seek(DEFAULT_COLUMN_FAMILY, key); + } - KeyValue seek(byte[] key) throws IOException; + KeyValue seek(String columnFamily, byte[] key) throws IOException; Cursor cursor() throws IOException; @@ -232,7 +345,11 @@ interface Reader extends Closeable { interface Writer extends Closeable { - byte[] get(byte[] key) throws IOException; + default byte[] get(byte[] key) throws IOException { + return get(DEFAULT_COLUMN_FAMILY, key); + } + + byte[] get(String columnFamily, byte[] key) throws IOException; void write(List mutations) throws IOException; @@ -247,10 +364,12 @@ interface Cursor extends Closeable { } static final class Mutation { + private final String columnFamily; private final byte[] key; private final byte[] value; - private Mutation(byte[] key, byte[] value) { + private Mutation(String columnFamily, byte[] key, byte[] value) { + this.columnFamily = requireColumnFamily(columnFamily); this.key = Arrays.copyOf(Objects.requireNonNull(key, "key"), key.length); this.value = value == null ? null : Arrays.copyOf(value, value.length); } @@ -299,6 +418,17 @@ private SharedRocksDatabase(Path directory, org.rocksdb.RocksDB database, } } + private static final class SharedHotRocksDatabase { + private final Path directory; + private final RocksColumnFamilyResources resources; + private int references; + + private SharedHotRocksDatabase(Path directory, RocksColumnFamilyResources resources) { + this.directory = directory; + this.resources = resources; + } + } + private static final class RocksResources implements Closeable { private final org.rocksdb.LRUCache cache; private final org.rocksdb.BloomFilter filter; @@ -345,6 +475,128 @@ public void close() { } } + private static final class RocksColumnFamilyResources implements Closeable { + private final NativeDbConfig config; + private final boolean create; + private final org.rocksdb.DBOptions databaseOptions; + private final org.rocksdb.LRUCache cache; + private final List filters = new ArrayList<>(); + private final List columnOptions = new ArrayList<>(); + private final List handles = new ArrayList<>(); + private final Map handlesByName = + new LinkedHashMap<>(); + private org.rocksdb.RocksDB database; + + private RocksColumnFamilyResources(NativeDbConfig config, boolean create) { + org.rocksdb.RocksDB.loadLibrary(); + this.config = config; + this.create = create; + cache = new org.rocksdb.LRUCache(config.getCacheSize()); + databaseOptions = new org.rocksdb.DBOptions() + .setCreateIfMissing(create) + .setCreateMissingColumnFamilies(create) + .setParanoidChecks(true) + .setMaxOpenFiles(config.getMaxOpenFiles()) + .setMaxBackgroundCompactions(config.getBackgroundCompactions()) + .setMaxBackgroundFlushes(config.getBackgroundFlushes()); + } + + private void open(Path directory) throws org.rocksdb.RocksDBException, IOException { + List expected = hotColumnFamilies(); + if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + Set actual = listColumnFamilies(directory); + if (!actual.equals(new LinkedHashSet<>(expected))) { + throw new ArchivePersistenceException( + "Hot Archive RocksDB column-family layout differs; rebuild/resync is required"); + } + } else if (!create) { + throw new ArchivePersistenceException("Hot Archive RocksDB directory is missing"); + } + List descriptors = new ArrayList<>(); + for (String name : expected) { + org.rocksdb.BloomFilter filter = new org.rocksdb.BloomFilter( + config.getBloomBitsPerKey(), false); + filters.add(filter); + org.rocksdb.BlockBasedTableConfig table = new org.rocksdb.BlockBasedTableConfig() + .setBlockSize(config.getBlockSize()) + .setChecksumType(org.rocksdb.ChecksumType.kCRC32c) + .setBlockCache(cache) + .setCacheIndexAndFilterBlocks(true) + .setPinL0FilterAndIndexBlocksInCache(false) + .setWholeKeyFiltering(true) + .setFilter(filter); + org.rocksdb.ColumnFamilyOptions options = new org.rocksdb.ColumnFamilyOptions() + .setCompressionType(org.rocksdb.CompressionType.SNAPPY_COMPRESSION) + .setWriteBufferSize(config.getWriteBufferSize()) + .setMaxWriteBufferNumber(config.getMaxWriteBufferNumber()) + .setMinWriteBufferNumberToMerge(1) + .setNumLevels(config.getLevelNumber()) + .setLevelCompactionDynamicLevelBytes(true) + .setLevel0FileNumCompactionTrigger(config.getLevel0FileNumCompactionTrigger()) + .setLevel0SlowdownWritesTrigger(config.getLevel0SlowdownWritesTrigger()) + .setLevel0StopWritesTrigger(config.getLevel0StopWritesTrigger()) + .setTargetFileSizeBase(config.getTargetFileSizeBase()) + .setMaxBytesForLevelBase(config.getMaxBytesForLevelBase()) + .setMaxBytesForLevelMultiplier(config.getMaxBytesForLevelMultiplier()) + .setTableFormatConfig(table); + columnOptions.add(options); + descriptors.add(new org.rocksdb.ColumnFamilyDescriptor( + name.getBytes(StandardCharsets.UTF_8), options)); + } + database = org.rocksdb.RocksDB.open(databaseOptions, directory.toString(), descriptors, + handles); + if (handles.size() != expected.size()) { + throw new ArchivePersistenceException("Hot Archive RocksDB did not open every column"); + } + for (int index = 0; index < expected.size(); index++) { + handlesByName.put(expected.get(index), handles.get(index)); + } + } + + private Set listColumnFamilies(Path directory) + throws org.rocksdb.RocksDBException { + try (org.rocksdb.Options options = new org.rocksdb.Options()) { + Set names = new LinkedHashSet<>(); + for (byte[] name : org.rocksdb.RocksDB.listColumnFamilies( + options, directory.toString())) { + names.add(new String(name, StandardCharsets.UTF_8)); + } + return names; + } + } + + private org.rocksdb.ColumnFamilyHandle handle(String columnFamily) { + org.rocksdb.ColumnFamilyHandle handle = handlesByName.get( + requireColumnFamily(columnFamily)); + if (handle == null) { + throw new IllegalArgumentException( + "Hot Archive column family was not opened: " + columnFamily); + } + return handle; + } + + @Override + public void close() { + for (org.rocksdb.ColumnFamilyHandle handle : handles) { + handle.close(); + } + handles.clear(); + handlesByName.clear(); + if (database != null) { + database.close(); + database = null; + } + for (org.rocksdb.ColumnFamilyOptions options : columnOptions) { + options.close(); + } + for (org.rocksdb.BloomFilter filter : filters) { + filter.close(); + } + databaseOptions.close(); + cache.close(); + } + } + private static final class LevelReader implements Reader { private final SharedLevelDatabase shared; private final Snapshot snapshot; @@ -370,12 +622,14 @@ private LevelReader(SharedLevelDatabase shared) throws IOException { } @Override - public byte[] get(byte[] key) { + public byte[] get(String columnFamily, byte[] key) { + requireColumnFamily(columnFamily); return shared.database.get(key, reads); } @Override - public KeyValue seek(byte[] key) throws IOException { + public KeyValue seek(String columnFamily, byte[] key) throws IOException { + requireColumnFamily(columnFamily); try (DBIterator iterator = shared.database.iterator(reads)) { iterator.seek(key); if (!iterator.hasNext()) { @@ -415,7 +669,8 @@ private LevelWriter(SharedLevelDatabase shared) { } @Override - public byte[] get(byte[] key) { + public byte[] get(String columnFamily, byte[] key) { + requireColumnFamily(columnFamily); return shared.database.get(key); } @@ -494,7 +749,8 @@ private RocksReader(SharedRocksDatabase shared) { } @Override - public byte[] get(byte[] key) throws IOException { + public byte[] get(String columnFamily, byte[] key) throws IOException { + requireDefaultColumnFamily(columnFamily); try { return shared.database.get(reads, key); } catch (org.rocksdb.RocksDBException failure) { @@ -503,7 +759,8 @@ public byte[] get(byte[] key) throws IOException { } @Override - public KeyValue seek(byte[] key) throws IOException { + public KeyValue seek(String columnFamily, byte[] key) throws IOException { + requireDefaultColumnFamily(columnFamily); try (org.rocksdb.ReadOptions seekReads = snapshotReads(snapshot); org.rocksdb.RocksIterator iterator = shared.database.newIterator(seekReads)) { iterator.seek(key); @@ -556,7 +813,8 @@ private RocksWriter(SharedRocksDatabase shared) { } @Override - public byte[] get(byte[] key) throws IOException { + public byte[] get(String columnFamily, byte[] key) throws IOException { + requireDefaultColumnFamily(columnFamily); try { return shared.database.get(key); } catch (org.rocksdb.RocksDBException failure) { @@ -573,6 +831,7 @@ public void write(List mutations) throws IOException { public void write(List mutations, boolean sync) throws IOException { try (org.rocksdb.WriteBatch batch = new org.rocksdb.WriteBatch()) { for (Mutation mutation : mutations) { + requireDefaultColumnFamily(mutation.columnFamily); if (mutation.value == null) { batch.delete(mutation.key); } else { @@ -596,6 +855,132 @@ public void close() { } } + private static final class HotRocksReader implements Reader { + private final SharedHotRocksDatabase shared; + private final org.rocksdb.Snapshot snapshot; + private final org.rocksdb.ReadOptions reads; + private boolean closed; + + private HotRocksReader(SharedHotRocksDatabase shared) { + this.shared = shared; + snapshot = shared.resources.database.getSnapshot(); + try { + reads = new org.rocksdb.ReadOptions().setVerifyChecksums(true).setFillCache(true) + .setSnapshot(snapshot); + } catch (RuntimeException failure) { + shared.resources.database.releaseSnapshot(snapshot); + releaseHotRocks(shared); + throw failure; + } + } + + @Override + public byte[] get(String columnFamily, byte[] key) throws IOException { + try { + return shared.resources.database.get(shared.resources.handle(columnFamily), reads, key); + } catch (org.rocksdb.RocksDBException failure) { + throw new IOException("Failed to read RocksDB Hot Archive column family", failure); + } + } + + @Override + public KeyValue seek(String columnFamily, byte[] key) throws IOException { + try (org.rocksdb.ReadOptions seekReads = snapshotReads(snapshot); + org.rocksdb.RocksIterator iterator = shared.resources.database.newIterator( + shared.resources.handle(columnFamily), seekReads)) { + iterator.seek(key); + if (!iterator.isValid()) { + iterator.status(); + return null; + } + return new KeyValue(iterator.key(), iterator.value()); + } catch (org.rocksdb.RocksDBException failure) { + throw new IOException("Failed to seek RocksDB Hot Archive column family", failure); + } + } + + @Override + public Cursor cursor() { + return new RocksCursor(shared.resources.database, + shared.resources.handle(DEFAULT_COLUMN_FAMILY), snapshotReads(snapshot), true); + } + + @Override + public OptionalLong readLongProperty(String name) { + try { + return OptionalLong.of(shared.resources.database.getLongProperty( + shared.resources.handle(DEFAULT_COLUMN_FAMILY), name)); + } catch (org.rocksdb.RocksDBException | IllegalArgumentException failure) { + return OptionalLong.empty(); + } + } + + @Override + public void close() { + if (!closed) { + closed = true; + reads.close(); + shared.resources.database.releaseSnapshot(snapshot); + releaseHotRocks(shared); + } + } + + private static org.rocksdb.ReadOptions snapshotReads(org.rocksdb.Snapshot snapshot) { + return new org.rocksdb.ReadOptions().setVerifyChecksums(true).setFillCache(true) + .setSnapshot(snapshot); + } + } + + private static final class HotRocksWriter implements Writer { + private final SharedHotRocksDatabase shared; + private boolean closed; + + private HotRocksWriter(SharedHotRocksDatabase shared) { + this.shared = shared; + } + + @Override + public byte[] get(String columnFamily, byte[] key) throws IOException { + try { + return shared.resources.database.get(shared.resources.handle(columnFamily), key); + } catch (org.rocksdb.RocksDBException failure) { + throw new IOException("Failed to read RocksDB Hot Archive column family", failure); + } + } + + @Override + public void write(List mutations) throws IOException { + write(mutations, true); + } + + @Override + public void write(List mutations, boolean sync) throws IOException { + try (org.rocksdb.WriteBatch batch = new org.rocksdb.WriteBatch()) { + for (Mutation mutation : mutations) { + org.rocksdb.ColumnFamilyHandle handle = shared.resources.handle(mutation.columnFamily); + if (mutation.value == null) { + batch.delete(handle, mutation.key); + } else { + batch.put(handle, mutation.key, mutation.value); + } + } + try (org.rocksdb.WriteOptions selected = new org.rocksdb.WriteOptions().setSync(sync)) { + shared.resources.database.write(selected, batch); + } + } catch (org.rocksdb.RocksDBException failure) { + throw new IOException("Failed to write RocksDB Hot Archive column families", failure); + } + } + + @Override + public void close() { + if (!closed) { + closed = true; + releaseHotRocks(shared); + } + } + } + private static final class RocksCursor implements Cursor { private final org.rocksdb.ReadOptions reads; private final org.rocksdb.RocksIterator iterator; @@ -608,6 +993,14 @@ private RocksCursor(org.rocksdb.RocksDB database, org.rocksdb.ReadOptions reads, this.ownsReadOptions = ownsReadOptions; } + private RocksCursor(org.rocksdb.RocksDB database, + org.rocksdb.ColumnFamilyHandle columnFamily, org.rocksdb.ReadOptions reads, + boolean ownsReadOptions) { + this.reads = reads; + this.iterator = database.newIterator(columnFamily, reads); + this.ownsReadOptions = ownsReadOptions; + } + @Override public void seek(byte[] key) { iterator.seek(key); diff --git a/common/src/main/java/org/tron/core/config/args/Storage.java b/common/src/main/java/org/tron/core/config/args/Storage.java index c1c5c1e0943..38730134fef 100644 --- a/common/src/main/java/org/tron/core/config/args/Storage.java +++ b/common/src/main/java/org/tron/core/config/args/Storage.java @@ -101,6 +101,10 @@ public class Storage { @Setter private int stateArchiveQueueCapacity; + @Getter + @Setter + private String stateArchiveServingIndexEngine; + @Getter @Setter private StorageConfig.NativeDbConfig stateArchiveServingIndexDbSettings; @@ -129,6 +133,10 @@ public class Storage { @Setter private String pathStateRootDirectory; + @Getter + @Setter + private String pathStateRootEngine; + @Getter @Setter private int pathStateRootFormatVersion; diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index e9e996dca2f..89dfd8369d8 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -152,6 +152,7 @@ public static class StateArchiveConfig { private String directory = "state-archive"; private long maxSegmentSize = 1073741824L; private int queueCapacity = 256; + private String servingIndexEngine = "ROCKSDB"; private NativeDbConfig servingIndex = NativeDbConfig.large(); private StateArchiveHotStoreConfig hotStore = new StateArchiveHotStoreConfig(); @@ -167,6 +168,8 @@ void postProcess() { throw new IllegalArgumentException( "stateArchive.queueCapacity must be in [1, 65536]"); } + servingIndexEngine = normalizeAuxiliaryEngine(servingIndexEngine, + "storage.stateArchive.servingIndexEngine"); servingIndex.validate("storage.stateArchive.servingIndex"); hotStore.postProcess(); } @@ -178,6 +181,7 @@ void postProcess() { public static class StateArchiveHotStoreConfig { private boolean enabled = false; + private String engine = "ROCKSDB"; private long maxBlocks = 10000L; private long maxEncodedBytes = 2147483648L; private int maxFrozenGenerations = 8; @@ -190,6 +194,7 @@ void postProcess() { } public void validate() { + engine = normalizeAuxiliaryEngine(engine, "storage.stateArchive.hotStore.engine"); if (maxBlocks <= 0 || maxEncodedBytes <= 0) { throw new IllegalArgumentException( "stateArchive.hotStore rotation limits must be positive"); @@ -223,6 +228,7 @@ void postProcess() { public static class PathStateRootConfig { private boolean enabled = false; + private String engine = "ROCKSDB"; private String mode = "shadow"; private String directory = "path-state-root"; private int formatVersion = 1; @@ -239,6 +245,7 @@ public static class PathStateRootConfig { private PathStateDbSettingsConfig dbSettings = new PathStateDbSettingsConfig(); void postProcess() { + engine = normalizeAuxiliaryEngine(engine, "storage.pathStateRoot.engine"); if (!"shadow".equals(mode)) { throw new IllegalArgumentException("pathStateRoot.mode must be shadow"); } @@ -271,6 +278,17 @@ void postProcess() { } } + private static String normalizeAuxiliaryEngine(String engine, String path) { + if (engine == null) { + throw new IllegalArgumentException(path + " must be LEVELDB or ROCKSDB"); + } + String normalized = engine.trim().toUpperCase(java.util.Locale.ROOT); + if (!"LEVELDB".equals(normalized) && !"ROCKSDB".equals(normalized)) { + throw new IllegalArgumentException(path + " must be LEVELDB or ROCKSDB"); + } + return normalized; + } + /** Engine-neutral native options for one Archive/PathState resource tier. */ @Getter @Setter diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index a83b9b416f5..1b463a23d72 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -137,6 +137,8 @@ storage { stateArchive.directory = "state-archive" stateArchive.maxSegmentSize = 1073741824 # 1 GiB stateArchive.queueCapacity = 256 + # Persistent engine identity for the serving-index DB. A change requires rebuild/resync. + stateArchive.servingIndexEngine = "ROCKSDB" stateArchive.servingIndex { blockSize = 4096 writeBufferSize = 67108864 @@ -157,6 +159,8 @@ storage { # Independent Hot DB candidate. It has no production caller while disabled. stateArchive.hotStore { enabled = false + # Persistent engine identity for current/frozen Hot DB generations. + engine = "ROCKSDB" maxBlocks = 10000 maxEncodedBytes = 2147483648 # 2 GiB maxFrozenGenerations = 8 @@ -186,6 +190,8 @@ storage { # Experimental current-only, non-consensus path state root. Disabled by default. pathStateRoot.enabled = false + # Persistent engine identity for all PathState physical stores. + pathStateRoot.engine = "ROCKSDB" pathStateRoot.mode = "shadow" pathStateRoot.directory = "path-state-root" pathStateRoot.formatVersion = 1 diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java index 405c088e319..e2ed4c9aa9c 100644 --- a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -64,7 +64,9 @@ public void testStateArchiveDefaultsAndOverrides() { assertEquals("state-archive", defaults.getStateArchive().getDirectory()); assertEquals(1073741824L, defaults.getStateArchive().getMaxSegmentSize()); assertEquals(256, defaults.getStateArchive().getQueueCapacity()); + assertEquals("ROCKSDB", defaults.getStateArchive().getServingIndexEngine()); assertFalse(defaults.getStateArchive().getHotStore().isEnabled()); + assertEquals("ROCKSDB", defaults.getStateArchive().getHotStore().getEngine()); assertEquals(10000L, defaults.getStateArchive().getHotStore().getMaxBlocks()); assertEquals(2147483648L, defaults.getStateArchive().getHotStore().getMaxEncodedBytes()); @@ -74,11 +76,14 @@ public void testStateArchiveDefaultsAndOverrides() { StorageConfig configured = StorageConfig.fromConfig(withRef( "storage.stateArchive { enabled = true, directory = archive-test, " - + "maxSegmentSize = 134217728, queueCapacity = 8 }")); + + "maxSegmentSize = 134217728, queueCapacity = 8, " + + "servingIndexEngine = leveldb, hotStore.engine = leveldb }")); assertTrue(configured.getStateArchive().isEnabled()); assertEquals("archive-test", configured.getStateArchive().getDirectory()); assertEquals(134217728L, configured.getStateArchive().getMaxSegmentSize()); assertEquals(8, configured.getStateArchive().getQueueCapacity()); + assertEquals("LEVELDB", configured.getStateArchive().getServingIndexEngine()); + assertEquals("LEVELDB", configured.getStateArchive().getHotStore().getEngine()); } @Test @@ -183,6 +188,7 @@ public void testPathStateRootDefaultsAndOverrides() { assertFalse(defaults.getPathStateRoot().isEnabled()); assertEquals("shadow", defaults.getPathStateRoot().getMode()); assertEquals("path-state-root", defaults.getPathStateRoot().getDirectory()); + assertEquals("ROCKSDB", defaults.getPathStateRoot().getEngine()); assertEquals(1, defaults.getPathStateRoot().getFormatVersion()); assertEquals(128, defaults.getPathStateRoot().getReversibleLayerLimit()); assertEquals(2147483648L, defaults.getPathStateRoot().getReversibleLayerBytes()); @@ -196,7 +202,8 @@ public void testPathStateRootDefaultsAndOverrides() { assertFalse(defaults.getPathStateRoot().isAsyncPrepareBenchmark()); StorageConfig configured = StorageConfig.fromConfig(withRef( - "storage.pathStateRoot { enabled = true, mode = shadow, directory = root-test, " + "storage.pathStateRoot { enabled = true, engine = leveldb, mode = shadow, " + + "directory = root-test, " + "formatVersion = 1, reversibleLayerLimit = 8, reversibleLayerBytes = 4096, " + "writeBufferBytes = 1024, nodeCacheBytes = 2048, participantThreads = 2, " + "branchThreads = 3, rebuildFromGenesis = false, " @@ -204,6 +211,7 @@ public void testPathStateRootDefaultsAndOverrides() { + "asyncPrepareBenchmark = true }")); assertTrue(configured.getPathStateRoot().isEnabled()); assertEquals("root-test", configured.getPathStateRoot().getDirectory()); + assertEquals("LEVELDB", configured.getPathStateRoot().getEngine()); assertEquals(8, configured.getPathStateRoot().getReversibleLayerLimit()); assertEquals(4096L, configured.getPathStateRoot().getReversibleLayerBytes()); assertEquals(1024L, configured.getPathStateRoot().getWriteBufferBytes()); @@ -224,6 +232,11 @@ public void testPathStateRootRejectsUnsupportedMode() { StorageConfig.fromConfig(withRef("storage.pathStateRoot.mode = consensus")); } + @Test(expected = IllegalArgumentException.class) + public void testRejectsUnsupportedAuxiliaryDatabaseEngine() { + StorageConfig.fromConfig(withRef("storage.stateArchive.servingIndexEngine = memory")); + } + @Test public void testDbSettingsDefaults() { // These defaults must match develop's Args.initRocksDbSettings() fallbacks so that diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index f64f03e181d..162603f18ba 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -221,6 +221,8 @@ private static void applyStorageConfig(StorageConfig sc) { PARAMETER.storage.setStateArchiveDirectory(sc.getStateArchive().getDirectory()); PARAMETER.storage.setStateArchiveMaxSegmentSize(sc.getStateArchive().getMaxSegmentSize()); PARAMETER.storage.setStateArchiveQueueCapacity(sc.getStateArchive().getQueueCapacity()); + PARAMETER.storage.setStateArchiveServingIndexEngine( + sc.getStateArchive().getServingIndexEngine()); PARAMETER.storage.setStateArchiveServingIndexDbSettings( sc.getStateArchive().getServingIndex()); PARAMETER.storage.setStateArchiveHotStoreSettings(sc.getStateArchive().getHotStore()); @@ -229,6 +231,7 @@ private static void applyStorageConfig(StorageConfig sc) { PARAMETER.storage.setPathStateRootEnabled(sc.getPathStateRoot().isEnabled()); PARAMETER.storage.setPathStateRootMode(sc.getPathStateRoot().getMode()); PARAMETER.storage.setPathStateRootDirectory(sc.getPathStateRoot().getDirectory()); + PARAMETER.storage.setPathStateRootEngine(sc.getPathStateRoot().getEngine()); PARAMETER.storage.setPathStateRootFormatVersion(sc.getPathStateRoot().getFormatVersion()); PARAMETER.storage.setPathStateRootReversibleLayerLimit( sc.getPathStateRoot().getReversibleLayerLimit()); diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index f8d2a69a9fa..cc9bb9df003 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -29,6 +29,7 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -749,7 +750,9 @@ private void initStateArchive() { recovered = null; logger.info("State archive runtime attached: directory={}, head={}, actions={}, engine={}", archiveDirectory, archiveHead.getBlockNumber(), - stateArchiveRuntime.getStartupRecoveryActionCount(), storage.getDbEngine()); + stateArchiveRuntime.getStartupRecoveryActionCount(), + configuredAuxiliaryEngine(storage.getStateArchiveServingIndexEngine(), + storage.getDbEngine())); } catch (java.io.IOException | BadItemException | ItemNotFoundException | RuntimeException failure) { if (recovered != null) { @@ -781,8 +784,10 @@ private void initCommonCheckpoint() { CommonCheckpointRuntimeAttachment attachment = null; StateArchiveHotStore hotStore = null; try { - PathStateStoreManifest.Engine engine = PathStateStoreManifest.Engine.valueOf( - storage.getDbEngine()); + PathStateStoreManifest.Engine pathEngine = configuredAuxiliaryEngine( + storage.getPathStateRootEngine(), storage.getDbEngine()); + PathStateStoreManifest.Engine servingIndexEngine = configuredAuxiliaryEngine( + storage.getStateArchiveServingIndexEngine(), storage.getDbEngine()); boolean pathExisted = Files.exists(pathDirectory, LinkOption.NOFOLLOW_LINKS); boolean modeAdmitted = pathExisted && PathStateCheckpointMaterializer.isCommonModeAdmitted(pathDirectory, formatIdentity); @@ -807,14 +812,14 @@ private void initCommonCheckpoint() { } } if (!pathExisted) { - rebuildPathStateRoot(snapshots, pathDirectory, engine); + rebuildPathStateRoot(snapshots, pathDirectory, pathEngine); } else if (!modeAdmitted && !Files.isRegularFile( pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), LinkOption.NOFOLLOW_LINKS) && !Files.isRegularFile(pathDirectory.resolve( PathStateCheckpointMaterializer.COMMON_BASELINE_HEAD_FILE), LinkOption.NOFOLLOW_LINKS)) { - rebuildPathStateRoot(snapshots, pathDirectory, engine); + rebuildPathStateRoot(snapshots, pathDirectory, pathEngine); } PathStateLayerLimits limits = new PathStateLayerLimits( @@ -825,7 +830,7 @@ private void initCommonCheckpoint() { if (modeAdmitted && Files.isRegularFile( pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), LinkOption.NOFOLLOW_LINKS)) { - pathOwner = PathStatePhysicalOverlayHead.openCommonCheckpoint(pathDirectory, engine, + pathOwner = PathStatePhysicalOverlayHead.openCommonCheckpoint(pathDirectory, pathEngine, limits, storage.getPathStateRootNodeCacheBytes(), storage.getPathStateRootParticipantThreads(), storage.getPathStateRootBranchThreads(), formatIdentity, canonical, phase); @@ -833,11 +838,11 @@ private void initCommonCheckpoint() { pathOwner = modeAdmitted || Files.isRegularFile(pathDirectory.resolve( PathStateCheckpointMaterializer.COMMON_BASELINE_HEAD_FILE), LinkOption.NOFOLLOW_LINKS) - ? PathStatePhysicalOverlayHead.openCommonBaseline(pathDirectory, engine, limits, + ? PathStatePhysicalOverlayHead.openCommonBaseline(pathDirectory, pathEngine, limits, storage.getPathStateRootNodeCacheBytes(), storage.getPathStateRootParticipantThreads(), storage.getPathStateRootBranchThreads()) - : PathStatePhysicalOverlayHead.open(pathDirectory, engine, limits, + : PathStatePhysicalOverlayHead.open(pathDirectory, pathEngine, limits, storage.getPathStateRootNodeCacheBytes(), storage.getPathStateRootParticipantThreads(), storage.getPathStateRootBranchThreads()); @@ -870,6 +875,10 @@ private void initCommonCheckpoint() { org.tron.core.config.args.StorageConfig.StateArchiveHotStoreConfig hotConfig = storage.getStateArchiveHotStoreSettings(); boolean hotEnabled = hotConfig != null && hotConfig.isEnabled(); + PathStateStoreManifest.Engine hotEngine = configuredAuxiliaryEngine( + hotConfig == null ? null : hotConfig.getEngine(), storage.getDbEngine()); + PathStateStoreManifest.Engine archiveRuntimeEngine = hotEnabled + ? hotEngine : servingIndexEngine; Path hotDirectory = archiveDirectory.resolve("hot"); if (hotEnabled && !Files.exists(hotDirectory, LinkOption.NOFOLLOW_LINKS)) { requireEmptyOrMissing(archiveDirectory, "State Archive v2"); @@ -882,13 +891,13 @@ private void initCommonCheckpoint() { StateArchiveHotCheckpointMaterializer hotMaterializer = null; org.tron.core.db2.core.CommonCheckpointMaterializer archiveMaterializer; if (hotEnabled) { - hotStore = StateArchiveHotStore.openOrCreate(hotDirectory, formatIdentity, engine, + hotStore = StateArchiveHotStore.openOrCreate(hotDirectory, formatIdentity, hotEngine, baseline.getHead().getBlockNumber(), baseline.getHead().getBlockHash(), hotConfig); hotMaterializer = new StateArchiveHotCheckpointMaterializer(hotStore); archiveMaterializer = hotMaterializer; } else { archiveMaterializer = new StateArchiveCheckpointMaterializer(archiveDirectory, - formatIdentity, baseline, engine); + formatIdentity, baseline, servingIndexEngine); } CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( checkpointFile, @@ -903,7 +912,7 @@ private void initCommonCheckpoint() { CommonCheckpointRuntimeOwner owner = new CommonCheckpointRuntimeOwner(coordinator); if (admittedHotMaterializer == null) { return new CommonCheckpointRuntime(owner, snapshots.getDbs(), archiveDirectory, - formatIdentity, engine, latest::pin, + formatIdentity, archiveRuntimeEngine, latest::pin, admittedOwner::prepareCommonCheckpointRebase); } CommonCheckpointRecoveryStateAdapter recoveryState = @@ -912,7 +921,7 @@ private void initCommonCheckpoint() { CommonCheckpointHotRecovery recovery = new CommonCheckpointHotRecovery(checkpointFile, recoveryState, recoveryState, admittedHotMaterializer::reconcilePreparedTail); return new CommonCheckpointRuntime(owner, snapshots.getDbs(), archiveDirectory, - formatIdentity, engine, latest::pin, + formatIdentity, archiveRuntimeEngine, latest::pin, admittedOwner::prepareCommonCheckpointRebase, admittedHotMaterializer, recovery); }); @@ -928,7 +937,7 @@ private void initCommonCheckpoint() { LinkOption.NOFOLLOW_LINKS)) { if (admittedHotStore == null) { requireCommonPublishedAuthorities(checkpointDirectory, archiveDirectory, pathDirectory, - formatIdentity, engine); + formatIdentity, servingIndexEngine); } else { requireHotCommonPublishedAuthorities(checkpointDirectory, pathDirectory, formatIdentity, admittedHotStore); @@ -950,8 +959,9 @@ private void initCommonCheckpoint() { attachment = null; hotStore = null; logger.info("Common checkpoint runtime attached: checkpoint={}, archive={}, path={}, " - + "head={}, format={}", checkpointDirectory, archiveDirectory, pathDirectory, - canonical.getBlockNumber(), CommonCheckpointFormat.ID); + + "head={}, format={}, pathEngine={}, archiveEngine={}", checkpointDirectory, + archiveDirectory, pathDirectory, canonical.getBlockNumber(), CommonCheckpointFormat.ID, + pathEngine, archiveRuntimeEngine); } catch (java.io.IOException | BadItemException | ItemNotFoundException | RuntimeException failure) { if (pathStateRuntime != null) { @@ -1069,6 +1079,20 @@ private static void requireEmptyOrMissing(Path directory, String label) } } + private static PathStateStoreManifest.Engine configuredAuxiliaryEngine(String configured, + String fallback) { + String selected = configured == null ? fallback : configured; + if (selected == null) { + return PathStateStoreManifest.Engine.ROCKSDB; + } + try { + return PathStateStoreManifest.Engine.valueOf(selected.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException failure) { + throw new IllegalStateException("Unsupported auxiliary database engine: " + selected, + failure); + } + } + private java.util.Map commonCheckpointSupplementalStores(SnapshotManager snapshots) { if (snapshots.getDbs().stream().anyMatch(database -> @@ -1114,8 +1138,8 @@ private void initPathStateRoot() { storage.getPathStateRootDirectory()).normalize(); PathStateHead recovered = null; try { - PathStateStoreManifest.Engine engine = PathStateStoreManifest.Engine.valueOf( - storage.getDbEngine()); + PathStateStoreManifest.Engine engine = configuredAuxiliaryEngine( + storage.getPathStateRootEngine(), storage.getDbEngine()); PathStatePhysicalRuntimeAdmission.Result admission = PathStatePhysicalRuntimeAdmission.inspect(true, directory, engine); if (admission.getStatus() @@ -1154,7 +1178,7 @@ private void initPathStateRoot() { + "volatileSnapshotBenchmark={}, asyncPrepareBenchmark={}, nodeCacheBytes={}, " + "participantThreads={}, branchThreads={}", directory, - recoveredHead.getBlockNumber(), storage.getDbEngine(), + recoveredHead.getBlockNumber(), engine, storage.isPathStateRootVolatileSnapshotBenchmark(), storage.isPathStateRootAsyncPrepareBenchmark(), storage.getPathStateRootNodeCacheBytes(), storage.getPathStateRootParticipantThreads(), diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index cb454edae9a..a3c1fa528a7 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -42,6 +42,9 @@ storage { stateArchive.directory = "state-archive" stateArchive.maxSegmentSize = 1073741824 stateArchive.queueCapacity = 256 + # Independent auxiliary DB engines. Changing one requires rebuilding/resyncing that DB. + stateArchive.servingIndexEngine = "ROCKSDB" + stateArchive.hotStore.engine = "ROCKSDB" commonCheckpoint.enabled = false commonCheckpoint.directory = "common-checkpoint" @@ -49,6 +52,7 @@ storage { pathStateRoot.enabled = false pathStateRoot.mode = "shadow" pathStateRoot.directory = "path-state-root" + pathStateRoot.engine = "ROCKSDB" pathStateRoot.formatVersion = 1 pathStateRoot.reversibleLayerLimit = 128 pathStateRoot.reversibleLayerBytes = 2147483648 diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 3e5628f73e0..95227a53b5e 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -316,6 +316,7 @@ public void testPathStateRootStorageConfigMapping() { Map override = new HashMap<>(); override.put("storage.db.directory", "database"); override.put("storage.pathStateRoot.enabled", "true"); + override.put("storage.pathStateRoot.engine", "LEVELDB"); override.put("storage.pathStateRoot.directory", "root-mapped"); override.put("storage.pathStateRoot.reversibleLayerLimit", "9"); override.put("storage.pathStateRoot.reversibleLayerBytes", "8192"); @@ -333,6 +334,7 @@ public void testPathStateRootStorageConfigMapping() { Assert.assertTrue(storage.isPathStateRootEnabled()); Assert.assertEquals("shadow", storage.getPathStateRootMode()); Assert.assertEquals("root-mapped", storage.getPathStateRootDirectory()); + Assert.assertEquals("LEVELDB", storage.getPathStateRootEngine()); Assert.assertEquals(1, storage.getPathStateRootFormatVersion()); Assert.assertEquals(9, storage.getPathStateRootReversibleLayerLimit()); Assert.assertEquals(8192L, storage.getPathStateRootReversibleLayerBytes()); @@ -347,6 +349,26 @@ public void testPathStateRootStorageConfigMapping() { Args.clearParam(); } + @Test + public void testAuxiliaryDatabaseEnginesMapIndependentlyFromChainbase() { + Map override = new HashMap<>(); + override.put("storage.db.engine", "LEVELDB"); + override.put("storage.stateArchive.servingIndexEngine", "ROCKSDB"); + override.put("storage.stateArchive.hotStore.engine", "LEVELDB"); + override.put("storage.pathStateRoot.engine", "ROCKSDB"); + Config config = ConfigFactory.parseMap(override) + .withFallback(ConfigFactory.defaultReference()); + + Args.applyConfigParams(config); + + Storage storage = Args.getInstance().getStorage(); + Assert.assertEquals("LEVELDB", storage.getDbEngine()); + Assert.assertEquals("ROCKSDB", storage.getStateArchiveServingIndexEngine()); + Assert.assertEquals("LEVELDB", storage.getStateArchiveHotStoreSettings().getEngine()); + Assert.assertEquals("ROCKSDB", storage.getPathStateRootEngine()); + Args.clearParam(); + } + /** * Verify that event.subscribe.enable = false from config is read correctly. */ diff --git a/framework/src/test/java/org/tron/core/config/args/StorageTest.java b/framework/src/test/java/org/tron/core/config/args/StorageTest.java index c4059bcd122..f0b78fb8a66 100644 --- a/framework/src/test/java/org/tron/core/config/args/StorageTest.java +++ b/framework/src/test/java/org/tron/core/config/args/StorageTest.java @@ -73,16 +73,19 @@ public void getDirectory() { @Test public void archiveDatabaseProfilesAreBridgedFromConfiguration() { + Assert.assertEquals("ROCKSDB", storage.getPathStateRootEngine()); Assert.assertNotNull(storage.getPathStateRootDbSettings()); Assert.assertEquals(16 * 1024 * 1024, storage.getPathStateRootDbSettings().getSmall().getWriteBufferSize()); Assert.assertEquals(64 * 1024 * 1024L, storage.getPathStateRootDbSettings().getGiant().getCacheSize()); Assert.assertNotNull(storage.getStateArchiveServingIndexDbSettings()); + Assert.assertEquals("ROCKSDB", storage.getStateArchiveServingIndexEngine()); Assert.assertEquals(64 * 1024 * 1024, storage.getStateArchiveServingIndexDbSettings().getWriteBufferSize()); Assert.assertNotNull(storage.getStateArchiveHotStoreSettings()); Assert.assertFalse(storage.getStateArchiveHotStoreSettings().isEnabled()); + Assert.assertEquals("ROCKSDB", storage.getStateArchiveHotStoreSettings().getEngine()); Assert.assertEquals(10_000L, storage.getStateArchiveHotStoreSettings().getMaxBlocks()); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java index bba0cc77f5c..03f636b48e5 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java @@ -7,8 +7,11 @@ import java.io.File; import java.net.URL; import java.net.URLClassLoader; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; @@ -36,15 +39,23 @@ public class StateArchiveHotProcessRecoveryTest { @Test public void jvmHaltAfterHotPrepareBeforeCommonWalTruncatesOrphanOnRestart() throws Exception { - Path root = temporaryFolder.newFolder("hot-process-recovery").toPath(); + for (Engine engine : Engine.values()) { + assertHaltRecovery(engine); + } + } + + private void assertHaltRecovery(Engine engine) throws Exception { + Path root = temporaryFolder.newFolder("hot-process-recovery-" + engine.name()).toPath(); Process child = new ProcessBuilder(javaExecutable(), "-cp", runtimeClasspath(), StateArchiveHotProcessRecoveryTest.class.getName(), "halt-after-prepare", - root.toString()).redirectErrorStream(true) + root.toString(), engine.name()).redirectErrorStream(true) .redirectOutput(root.resolve("halt-after-prepare.log").toFile()).start(); assertTrue("child process timed out", child.waitFor(30, TimeUnit.SECONDS)); assertEquals(HALT_CODE, child.exitValue()); - try (StateArchiveHotStore store = open(root.resolve("hot"))) { + Path hotRoot = root.resolve("hot"); + assertPreparedBatch(hotRoot, engine); + try (StateArchiveHotStore store = open(hotRoot, engine)) { assertEquals(StateArchiveHotStore.HotCheckpointStatus.MATERIALIZED, store.inspectCheckpoint(TARGET)); assertEquals(0L, store.getCommittedHead()); @@ -62,7 +73,7 @@ public void jvmHaltAfterHotPrepareBeforeCommonWalTruncatesOrphanOnRestart() assertThrows(ArchivePersistenceException.class, () -> store.loadBlock(1)); } - try (StateArchiveHotStore reopened = open(root.resolve("hot"))) { + try (StateArchiveHotStore reopened = open(hotRoot, engine)) { assertEquals(0L, reopened.getCommittedHead()); assertEquals(0L, reopened.getMaterializedHead()); assertEquals(StateArchiveHotStore.HotCheckpointStatus.NEEDS_MATERIALIZATION, @@ -72,23 +83,57 @@ public void jvmHaltAfterHotPrepareBeforeCommonWalTruncatesOrphanOnRestart() /** Child-process entry point used to leave only the native-sync Hot PREPARED authority. */ public static void main(String[] args) throws Exception { - if (args.length != 2 || !"halt-after-prepare".equals(args[0])) { + if (args.length != 3 || !"halt-after-prepare".equals(args[0])) { throw new IllegalArgumentException("unknown process recovery mode"); } - StateArchiveHotStore store = open(Paths.get(args[1]).resolve("hot")); + StateArchiveHotStore store = open(Paths.get(args[1]).resolve("hot"), + Engine.valueOf(args[2])); store.prepareCheckpoint(TARGET, Collections.singletonList(diff(1, 0))); Runtime.getRuntime().halt(HALT_CODE); } - private static StateArchiveHotStore open(Path root) throws Exception { - return StateArchiveHotStore.openOrCreate(root, FORMAT, Engine.LEVELDB, 0, BASE_HASH, + private static StateArchiveHotStore open(Path root, Engine engine) throws Exception { + return StateArchiveHotStore.openOrCreate(root, FORMAT, engine, 0, BASE_HASH, 3, 10, 1024 * 1024); } + private static void assertPreparedBatch(Path hotRoot, Engine engine) throws Exception { + if (engine != Engine.ROCKSDB) { + return; + } + Path database = hotRoot.resolve(StateArchiveHotStore.GENERATIONS) + .resolve("00000000000000000000").resolve(StateArchiveHotStore.DATABASE); + try (StateArchiveIndexDatabase.Reader reader = StateArchiveIndexDatabase.openHotReader( + database, engine, org.tron.core.config.args.StorageConfig.NativeDbConfig.large())) { + assertEquals(29, StateArchiveIndexDatabase.hotColumnFamilies().size()); + org.junit.Assert.assertArrayEquals(TARGET, reader.get( + "meta/prepared-target".getBytes(StandardCharsets.US_ASCII))); + org.junit.Assert.assertNotNull(reader.get( + StateArchiveIndexDatabase.HOT_BLOCKS_COLUMN_FAMILY, + ByteBuffer.allocate(1 + Long.BYTES).put((byte) 0x42).putLong(1).array())); + for (String store : Arrays.asList("account", "code", "storage-row")) { + org.junit.Assert.assertNotNull(reader.get( + StateArchiveIndexDatabase.hotStoreColumnFamily(store), + hotIndexKey(store, new byte[]{(byte) store.length()}, 1))); + } + } + } + private static BlockReverseDiff diff(long block, int parent) { - return new BlockReverseDiff(meta(block, parent), Collections.singletonList( - new DbGroup("code", Collections.singletonList( - new Entry(new byte[]{1}, OldValue.absent())))), hash(60 + (int) block)); + return new BlockReverseDiff(meta(block, parent), Arrays.asList( + group("account"), group("code"), group("storage-row")), hash(60 + (int) block)); + } + + private static DbGroup group(String store) { + return new DbGroup(store, Collections.singletonList( + new Entry(new byte[]{(byte) store.length()}, OldValue.absent()))); + } + + private static byte[] hotIndexKey(String store, byte[] key, long block) { + byte[] storeBytes = store.getBytes(StandardCharsets.UTF_8); + return ByteBuffer.allocate(1 + Short.BYTES + storeBytes.length + Integer.BYTES + key.length + + Long.BYTES).put((byte) 0x4b).putShort((short) storeBytes.length).put(storeBytes) + .putInt(key.length).put(key).putLong(block).array(); } private static BlockSnapshotMeta meta(long block, int parent) { diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java index 89073bf7cd0..cfbb2938f08 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java @@ -3,16 +3,20 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Optional; +import java.util.Set; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -65,6 +69,61 @@ public void appendsSealsQueriesAndReopensAcrossBothEngines() throws Exception { } } + @Test + public void rocksCheckpointWritesOneExactCrossColumnFamilyBatch() throws Exception { + Path root = temporaryFolder.newFolder("hot-rocks-column-families").toPath(); + byte[] format = hash(120); + byte[] target = hash(121); + BlockReverseDiff block = new BlockReverseDiff(meta(1, 0, 1), Arrays.asList( + new DbGroup("account", Collections.singletonList( + new Entry(new byte[]{1}, OldValue.absent()))), + new DbGroup("code", Collections.singletonList( + new Entry(new byte[]{2}, OldValue.present(new byte[]{22})))), + new DbGroup("storage-row", Collections.singletonList( + new Entry(new byte[]{3}, OldValue.present(new byte[0]))))), hash(122)); + Path database = root.resolve(StateArchiveHotStore.GENERATIONS) + .resolve("00000000000000000000").resolve(StateArchiveHotStore.DATABASE); + + try (StateArchiveHotStore store = open(root, format, Engine.ROCKSDB, + 0, hash(0), 3, 10)) { + store.prepareCheckpoint(target, Collections.singletonList(block)); + try (StateArchiveIndexDatabase.Reader reader = StateArchiveIndexDatabase.openHotReader( + database, Engine.ROCKSDB, NativeDbConfig.large())) { + assertArrayEquals(target, reader.get("meta/prepared-target" + .getBytes(StandardCharsets.US_ASCII))); + assertNotNull(reader.get(StateArchiveIndexDatabase.HOT_BLOCKS_COLUMN_FAMILY, + hotBodyKey(1))); + for (DbGroup group : block.getGroups()) { + Entry entry = group.getEntries().get(0); + assertArrayEquals(ByteBuffer.allocate(Long.BYTES).putLong(1).array(), reader.get( + StateArchiveIndexDatabase.hotStoreColumnFamily(group.getDbName()), + hotIndexKey(group.getDbName(), entry.getKey(), 1))); + } + } + store.publishCheckpoint(target); + } + + Set actual = new LinkedHashSet<>(); + try (org.rocksdb.Options options = new org.rocksdb.Options()) { + for (byte[] name : org.rocksdb.RocksDB.listColumnFamilies(options, database.toString())) { + actual.add(new String(name, StandardCharsets.UTF_8)); + } + } + assertEquals(new LinkedHashSet<>(StateArchiveIndexDatabase.hotColumnFamilies()), actual); + assertEquals(29, actual.size()); + + try (StateArchiveHotStore reopened = open(root, format, Engine.ROCKSDB, + 0, hash(0), 3, 10)) { + assertEquals(1, reopened.getCommittedHead()); + assertLookup(reopened.findOldValueAfter("account", new byte[]{1}, 0), 1, + OldValue.absent()); + assertLookup(reopened.findOldValueAfter("code", new byte[]{2}, 0), 1, + OldValue.present(new byte[]{22})); + assertLookup(reopened.findOldValueAfter("storage-row", new byte[]{3}, 0), 1, + OldValue.present(new byte[0])); + } + } + @Test public void recoversEveryRotationPublicationBoundary() throws Exception { for (StateArchiveHotStore.Stage failedStage : Arrays.asList( @@ -292,7 +351,7 @@ public void persistsAndRevalidatesExactPreparedDescriptorAcrossBothEngines() } Path database = root.resolve(StateArchiveHotStore.GENERATIONS) .resolve("00000000000000000000").resolve(StateArchiveHotStore.DATABASE); - try (StateArchiveIndexDatabase.Writer writer = StateArchiveIndexDatabase.openWriter( + try (StateArchiveIndexDatabase.Writer writer = StateArchiveIndexDatabase.openHotWriter( database, engine, NativeDbConfig.large())) { writer.write(Collections.singletonList(StateArchiveIndexDatabase.put( "meta/prepared-descriptor".getBytes(StandardCharsets.US_ASCII), @@ -400,6 +459,17 @@ private static BlockSnapshotMeta meta(long block, int parent, int hashMarker) { return BlockSnapshotMeta.forBlock(block, hash(hashMarker), hash(parent), block * 3_000); } + private static byte[] hotBodyKey(long block) { + return ByteBuffer.allocate(1 + Long.BYTES).put((byte) 0x42).putLong(block).array(); + } + + private static byte[] hotIndexKey(String dbName, byte[] rawKey, long block) { + byte[] name = dbName.getBytes(StandardCharsets.UTF_8); + return ByteBuffer.allocate(1 + Short.BYTES + name.length + Integer.BYTES + rawKey.length + + Long.BYTES).put((byte) 0x4b).putShort((short) name.length).put(name) + .putInt(rawKey.length).put(rawKey).putLong(block).array(); + } + private static void assertLookup(Optional found, long block, OldValue oldValue) { assertTrue(found.isPresent()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java index a368be76235..b1684127a32 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveIndexDatabaseTest.java @@ -17,6 +17,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.tron.core.config.args.StorageConfig.NativeDbConfig; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; public class StateArchiveIndexDatabaseTest { @@ -69,6 +70,18 @@ public void rocksReaderAndWriterShareConfiguredDatabaseHandle() throws Exception assertTrue(nativeOptions.contains("filter_policy=rocksdb.BuiltinBloomFilter")); } + @Test + public void hotRocksRejectsLegacySingleColumnFamilyLayout() throws Exception { + Path database = temporaryFolder.newFolder("rocks-flat-hot-reject").toPath().resolve("keys"); + try (StateArchiveIndexDatabase.Writer writer = + StateArchiveIndexDatabase.openWriter(database, Engine.ROCKSDB)) { + writer.write(Arrays.asList(StateArchiveIndexDatabase.put(new byte[]{1}, new byte[]{2}))); + } + + assertThrows(IOException.class, () -> StateArchiveIndexDatabase.openHotWriter( + database, Engine.ROCKSDB, NativeDbConfig.large())); + } + @Test public void rejectsExistingDatabaseWithoutEngineIdentity() throws Exception { Path root = temporaryFolder.newFolder("missing-manifest").toPath(); From 9df121ecbe7d4365f8366e1cff67e4aabf204869 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 8 Sep 2026 15:40:41 +0800 Subject: [PATCH 126/161] refactor(db): remove mutation view digest Use block number and hash for block identity and state roots for PathState consistency. Keep Archive and checkpoint digests limited to payload integrity. Bump affected persistent formats and reject legacy layouts. --- .../core/db2/archive/BlockChangeView.java | 33 ------------- .../core/db2/archive/BlockReverseDiff.java | 25 ---------- .../archive/SnapshotOldValueCollector.java | 2 +- .../SnapshotPathStateTransitionCollector.java | 3 +- .../StateArchiveCheckpointMaterializer.java | 8 ++-- .../StateArchiveHotBatchDescriptor.java | 41 ++++------------ .../StateArchiveHotBatchDescriptorCodec.java | 11 ++--- .../db2/archive/StateArchiveHotStore.java | 33 ++++--------- .../db2/core/CommonCheckpointCapture.java | 6 +-- .../db2/core/CommonCheckpointPayload.java | 47 +++++-------------- .../core/CommonCheckpointPayloadCodec.java | 22 +++------ .../core/CommonCheckpointPayloadFactory.java | 14 +----- .../org/tron/core/db2/core/SnapshotImpl.java | 8 ---- .../stateroot/PathStateBlockTransition.java | 14 ------ .../db2/stateroot/PathStateFlushTarget.java | 5 -- .../stateroot/PathStateRuntimeAttachment.java | 3 +- .../db2/stateroot/PathStateSnapshotDelta.java | 15 ++---- .../org/tron/core/db2/SnapshotImplTest.java | 25 ---------- .../SnapshotOldValueCollectorTest.java | 16 ++----- ...tateArchiveCheckpointMaterializerTest.java | 11 ++--- ...tateArchiveCheckpointReadSnapshotTest.java | 9 ++-- ...eArchiveHotCheckpointMaterializerTest.java | 6 +-- .../StateArchiveHotProcessRecoveryTest.java | 2 +- .../db2/archive/StateArchiveHotStoreTest.java | 29 ++++++++++-- .../ChainbaseCheckpointMaterializerTest.java | 24 ++++------ .../db2/core/CommonCheckpointFileTest.java | 4 +- .../core/CommonCheckpointHotRecoveryTest.java | 10 ++-- .../core/CommonCheckpointPayloadV2Test.java | 22 +++++---- .../CommonCheckpointRedoCoordinatorTest.java | 4 +- .../PathStateCheckpointMaterializerTest.java | 4 +- ...athStateManagerStartupIntegrationTest.java | 4 +- .../PathStateNativeNodeStoreTest.java | 7 ++- .../stateroot/PathStateSnapshotHeadTest.java | 17 ++++--- 33 files changed, 133 insertions(+), 351 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java b/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java index e900c117e1b..0e7e11ebfbb 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java @@ -1,8 +1,5 @@ package org.tron.core.db2.archive; -import com.google.common.hash.Hasher; -import com.google.common.hash.Hashing; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -22,18 +19,12 @@ */ public final class BlockChangeView { - private static final byte[] DIGEST_DOMAIN = - "java-tron/block-change-view".getBytes(StandardCharsets.US_ASCII); - private static final int DIGEST_VERSION = 1; - private final BlockSnapshotMeta meta; private final List databases; - private final byte[] mutationViewDigest; private BlockChangeView(BlockSnapshotMeta meta, List databases) { this.meta = Objects.requireNonNull(meta, "meta"); this.databases = Collections.unmodifiableList(new ArrayList<>(databases)); - this.mutationViewDigest = digest(meta, this.databases); } public static BlockChangeView capture(BlockSnapshotMeta meta, List databases) { @@ -67,30 +58,6 @@ public List getDatabases() { return databases; } - /** Canonical identity of the exact block-final database/key/post-value view. */ - public byte[] getMutationViewDigest() { - return Arrays.copyOf(mutationViewDigest, mutationViewDigest.length); - } - - private static byte[] digest(BlockSnapshotMeta meta, List databases) { - Hasher digest = Hashing.sha256().newHasher(); - digest.putInt(DIGEST_DOMAIN.length).putBytes(DIGEST_DOMAIN).putInt(DIGEST_VERSION) - .putLong(meta.getEpoch()).putLong(meta.getBlockNumber()).putBytes(meta.getBlockHash()) - .putBytes(meta.getParentHash()).putLong(meta.getTimestamp()).putInt(databases.size()); - for (DatabaseChanges database : databases) { - byte[] dbName = database.dbName.getBytes(StandardCharsets.UTF_8); - digest.putInt(dbName.length).putBytes(dbName).putInt(database.changes.size()); - for (Change change : database.changes) { - digest.putInt(change.key.length).putBytes(change.key) - .putBoolean(change.postValue.present); - if (change.postValue.present) { - digest.putInt(change.postValue.value.length).putBytes(change.postValue.value); - } - } - } - return digest.hash().asBytes(); - } - public static final class DatabaseChanges { private final String dbName; private final Snapshot previous; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java b/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java index 538441a4db0..26621d695d2 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/BlockReverseDiff.java @@ -13,19 +13,11 @@ public final class BlockReverseDiff { private final BlockSnapshotMeta meta; private final List groups; - private final byte[] mutationViewDigest; - public BlockReverseDiff(BlockSnapshotMeta meta, List groups) { - this(meta, groups, null); - } - - public BlockReverseDiff(BlockSnapshotMeta meta, List groups, - byte[] mutationViewDigest) { this.meta = Objects.requireNonNull(meta, "meta"); List sorted = new ArrayList<>(groups); sorted.sort(Comparator.comparing(DbGroup::getDbName)); this.groups = Collections.unmodifiableList(sorted); - this.mutationViewDigest = optionalDigest(mutationViewDigest); } public BlockSnapshotMeta getMeta() { @@ -36,23 +28,6 @@ public List getGroups() { return groups; } - /** Returns the block-final mutation-view identity, or null for decoded legacy payloads. */ - public byte[] getMutationViewDigest() { - return mutationViewDigest == null ? null - : Arrays.copyOf(mutationViewDigest, mutationViewDigest.length); - } - - private static byte[] optionalDigest(byte[] supplied) { - if (supplied == null) { - return null; - } - byte[] copy = Arrays.copyOf(supplied, supplied.length); - if (copy.length != 32) { - throw new IllegalArgumentException("mutationViewDigest must contain exactly 32 bytes"); - } - return copy; - } - public static final class DbGroup { private final String dbName; private final List entries; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java index 7843cbb5b3f..42141d4aebd 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotOldValueCollector.java @@ -71,7 +71,7 @@ public BlockReverseDiff collect(BlockChangeView view) { groups.add(new BlockReverseDiff.DbGroup( AccountAssetArchiveProjector.ACCOUNT_ASSET_DB, accountAssetEntries)); } - return new BlockReverseDiff(view.getMeta(), groups, view.getMutationViewDigest()); + return new BlockReverseDiff(view.getMeta(), groups); } /** Resolves proposal 66 from the same immutable target block view being projected. */ diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java index 15dbb42d20c..0d29228b62a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/SnapshotPathStateTransitionCollector.java @@ -68,8 +68,7 @@ public PathStateBlockTransition collect(BlockChangeView view) throws IOException } BlockSnapshotMeta meta = admitted.getMeta(); return new PathStateBlockTransition(meta.getBlockNumber(), meta.getBlockHash(), - meta.getParentHash(), meta.getTimestamp(), phase, mutations.values(), - admitted.getMutationViewDigest()); + meta.getParentHash(), meta.getTimestamp(), phase, mutations.values()); } private void collectActivationAccounts( diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java index 35cffb60970..96c01df5a25 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java @@ -40,13 +40,13 @@ public final class StateArchiveCheckpointMaterializer implements CommonCheckpoin private static final int TARGET_MAGIC = 0x53414354; // SACT private static final short TARGET_VERSION = 2; private static final int BLOCK_MAGIC = 0x53414342; // SACB - private static final short BLOCK_VERSION = 1; + private static final short BLOCK_VERSION = 2; private static final int DIGEST_LENGTH = 32; private static final int META_LENGTH = 3 * Long.BYTES + 2 * DIGEST_LENGTH; private static final int TARGET_LENGTH = Integer.BYTES + 2 * Short.BYTES + 4 * DIGEST_LENGTH + 2 * META_LENGTH + DIGEST_LENGTH; private static final int BLOCK_FIXED_LENGTH = Integer.BYTES + 2 * Short.BYTES - + DIGEST_LENGTH + Integer.BYTES + DIGEST_LENGTH; + + Integer.BYTES + DIGEST_LENGTH; private static final long MAX_BLOCK_LENGTH = BlockHistoryCodec.DEFAULT_MAX_RECORD_LENGTH + (long) BLOCK_FIXED_LENGTH; @@ -383,7 +383,6 @@ private byte[] encodeBlock(CommonCheckpointPayload.BlockPayload block) { output.writeInt(BLOCK_MAGIC); output.writeShort(BLOCK_VERSION); output.writeShort(0); - output.write(block.getMutationViewDigest()); output.writeInt(history.length); output.write(history); output.flush(); @@ -415,7 +414,6 @@ private static BlockReverseDiff decodeBlock(byte[] encoded) throws IOException { || input.readShort() != 0) { throw new IOException("State Archive checkpoint block format is unsupported"); } - byte[] viewDigest = readDigest(input); int historyLength = input.readInt(); if (historyLength <= 0 || historyLength != input.available()) { throw new IOException("State Archive checkpoint history length is invalid"); @@ -428,7 +426,7 @@ private static BlockReverseDiff decodeBlock(byte[] encoded) throws IOException { } catch (IllegalArgumentException invalid) { throw new IOException("State Archive checkpoint history is corrupt", invalid); } - return new BlockReverseDiff(decoded.getMeta(), decoded.getGroups(), viewDigest); + return decoded; } catch (EOFException truncated) { throw new IOException("State Archive checkpoint block is truncated", truncated); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptor.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptor.java index 3eb70ecfda1..739d481e93c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptor.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptor.java @@ -12,7 +12,7 @@ /** Immutable logical binding between transient Archive diffs and one Hot DB prepare batch. */ public final class StateArchiveHotBatchDescriptor { - public static final int HOT_FORMAT_VERSION = 1; + public static final int HOT_FORMAT_VERSION = 2; private static final int DIGEST_LENGTH = 32; private final Engine engine; @@ -24,13 +24,12 @@ public final class StateArchiveHotBatchDescriptor { private final byte[] parentContentDigest; private final byte[] resultContentDigest; private final byte[] orderedRecordDigest; - private final byte[] mutationViewRangeDigest; private final List blocks; StateArchiveHotBatchDescriptor(Engine engine, long parentPublishedBlock, byte[] parentPublishedHash, BlockSnapshotMeta firstBlock, BlockSnapshotMeta lastBlock, long encodedBytes, byte[] parentContentDigest, byte[] resultContentDigest, - byte[] orderedRecordDigest, byte[] mutationViewRangeDigest, List blocks) { + byte[] orderedRecordDigest, List blocks) { this.engine = Objects.requireNonNull(engine, "engine"); if (parentPublishedBlock < 0 || encodedBytes <= 0) { throw new IllegalArgumentException("Hot Archive batch counters are invalid"); @@ -43,8 +42,6 @@ public final class StateArchiveHotBatchDescriptor { this.parentContentDigest = digest(parentContentDigest, "parentContentDigest"); this.resultContentDigest = digest(resultContentDigest, "resultContentDigest"); this.orderedRecordDigest = digest(orderedRecordDigest, "orderedRecordDigest"); - this.mutationViewRangeDigest = digest(mutationViewRangeDigest, - "mutationViewRangeDigest"); List admitted = new ArrayList<>(Objects.requireNonNull(blocks, "blocks")); if (admitted.isEmpty() || admitted.size() != lastBlock.getBlockNumber() - firstBlock.getBlockNumber() + 1L @@ -55,7 +52,6 @@ public final class StateArchiveHotBatchDescriptor { } BlockDigest previous = null; Hasher orderedRecords = Hashing.sha256().newHasher(); - Hasher mutationViews = Hashing.sha256().newHasher(); for (BlockDigest block : admitted) { BlockDigest current = Objects.requireNonNull(block, "block"); if (current.meta.getEpoch() != current.meta.getBlockNumber()) { @@ -70,12 +66,9 @@ public final class StateArchiveHotBatchDescriptor { } orderedRecords.putLong(current.meta.getBlockNumber()) .putBytes(current.archiveRecordDigest); - mutationViews.putLong(current.meta.getBlockNumber()) - .putBytes(current.mutationViewDigest); previous = current; } - if (!Arrays.equals(this.orderedRecordDigest, orderedRecords.hash().asBytes()) - || !Arrays.equals(this.mutationViewRangeDigest, mutationViews.hash().asBytes())) { + if (!Arrays.equals(this.orderedRecordDigest, orderedRecords.hash().asBytes())) { throw new IllegalArgumentException("Hot Archive batch aggregate digest differs"); } this.blocks = Collections.unmodifiableList(admitted); @@ -85,11 +78,10 @@ public final class StateArchiveHotBatchDescriptor { public static StateArchiveHotBatchDescriptor restore(Engine engine, long parentPublishedBlock, byte[] parentPublishedHash, BlockSnapshotMeta firstBlock, BlockSnapshotMeta lastBlock, long encodedBytes, byte[] parentContentDigest, - byte[] resultContentDigest, byte[] orderedRecordDigest, - byte[] mutationViewRangeDigest, List blocks) { + byte[] resultContentDigest, byte[] orderedRecordDigest, List blocks) { return new StateArchiveHotBatchDescriptor(engine, parentPublishedBlock, parentPublishedHash, firstBlock, lastBlock, encodedBytes, parentContentDigest, - resultContentDigest, orderedRecordDigest, mutationViewRangeDigest, blocks); + resultContentDigest, orderedRecordDigest, blocks); } public Engine getEngine() { @@ -132,10 +124,6 @@ public byte[] getOrderedRecordDigest() { return copy(orderedRecordDigest); } - public byte[] getMutationViewRangeDigest() { - return copy(mutationViewRangeDigest); - } - public List getBlocks() { return blocks; } @@ -158,8 +146,7 @@ public boolean equals(Object object) { && Arrays.equals(parentPublishedHash, that.parentPublishedHash) && Arrays.equals(parentContentDigest, that.parentContentDigest) && Arrays.equals(resultContentDigest, that.resultContentDigest) - && Arrays.equals(orderedRecordDigest, that.orderedRecordDigest) - && Arrays.equals(mutationViewRangeDigest, that.mutationViewRangeDigest); + && Arrays.equals(orderedRecordDigest, that.orderedRecordDigest); } @Override @@ -170,7 +157,6 @@ public int hashCode() { result = 31 * result + Arrays.hashCode(parentContentDigest); result = 31 * result + Arrays.hashCode(resultContentDigest); result = 31 * result + Arrays.hashCode(orderedRecordDigest); - result = 31 * result + Arrays.hashCode(mutationViewRangeDigest); return result; } @@ -189,28 +175,21 @@ private static byte[] copy(byte[] value) { /** Digest-only per-block Archive identity retained by the coordination payload. */ public static final class BlockDigest { private final BlockSnapshotMeta meta; - private final byte[] mutationViewDigest; private final byte[] archiveRecordDigest; - BlockDigest(BlockSnapshotMeta meta, byte[] mutationViewDigest, byte[] archiveRecordDigest) { + BlockDigest(BlockSnapshotMeta meta, byte[] archiveRecordDigest) { this.meta = Objects.requireNonNull(meta, "meta"); - this.mutationViewDigest = digest(mutationViewDigest, "mutationViewDigest"); this.archiveRecordDigest = digest(archiveRecordDigest, "archiveRecordDigest"); } - public static BlockDigest restore(BlockSnapshotMeta meta, byte[] mutationViewDigest, - byte[] archiveRecordDigest) { - return new BlockDigest(meta, mutationViewDigest, archiveRecordDigest); + public static BlockDigest restore(BlockSnapshotMeta meta, byte[] archiveRecordDigest) { + return new BlockDigest(meta, archiveRecordDigest); } public BlockSnapshotMeta getMeta() { return meta; } - public byte[] getMutationViewDigest() { - return copy(mutationViewDigest); - } - public byte[] getArchiveRecordDigest() { return copy(archiveRecordDigest); } @@ -222,14 +201,12 @@ public boolean equals(Object object) { } BlockDigest that = (BlockDigest) object; return meta.equals(that.meta) - && Arrays.equals(mutationViewDigest, that.mutationViewDigest) && Arrays.equals(archiveRecordDigest, that.archiveRecordDigest); } @Override public int hashCode() { int result = meta.hashCode(); - result = 31 * result + Arrays.hashCode(mutationViewDigest); result = 31 * result + Arrays.hashCode(archiveRecordDigest); return result; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptorCodec.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptorCodec.java index 1d3b8c8934a..4e0211b02c6 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptorCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotBatchDescriptorCodec.java @@ -17,7 +17,7 @@ final class StateArchiveHotBatchDescriptorCodec { private static final int MAGIC = 0x53414844; // SAHD - private static final short VERSION = 1; + private static final short VERSION = 2; private static final int DIGEST_LENGTH = 32; private static final int HEADER_LENGTH = 44; private static final int MAX_BLOCKS = 100_000; @@ -38,10 +38,8 @@ byte[] encode(StateArchiveHotBatchDescriptor descriptor) { body.write(descriptor.getParentContentDigest()); body.write(descriptor.getResultContentDigest()); body.write(descriptor.getOrderedRecordDigest()); - body.write(descriptor.getMutationViewRangeDigest()); for (BlockDigest block : descriptor.getBlocks()) { writeMeta(body, block.getMeta()); - body.write(block.getMutationViewDigest()); body.write(block.getArchiveRecordDigest()); } body.flush(); @@ -102,23 +100,20 @@ private StateArchiveHotBatchDescriptor decodeBody(byte[] body) throws IOExceptio byte[] parentContent = readExact(input, DIGEST_LENGTH); byte[] resultContent = readExact(input, DIGEST_LENGTH); byte[] orderedRecords = readExact(input, DIGEST_LENGTH); - byte[] mutationViews = readExact(input, DIGEST_LENGTH); if (blockCount <= 0 || blockCount > MAX_BLOCKS) { throw new ArchivePersistenceException("Hot Archive descriptor block count is invalid"); } List blocks = new ArrayList<>((int) blockCount); for (long index = 0; index < blockCount; index++) { BlockSnapshotMeta meta = readMeta(input); - blocks.add(BlockDigest.restore(meta, readExact(input, DIGEST_LENGTH), - readExact(input, DIGEST_LENGTH))); + blocks.add(BlockDigest.restore(meta, readExact(input, DIGEST_LENGTH))); } if (input.available() != 0) { throw new ArchivePersistenceException("Hot Archive descriptor has trailing bytes"); } try { return StateArchiveHotBatchDescriptor.restore(engine, parentBlock, parentHash, first, - last, encodedBytes, parentContent, resultContent, orderedRecords, mutationViews, - blocks); + last, encodedBytes, parentContent, resultContent, orderedRecords, blocks); } catch (IllegalArgumentException invalid) { throw new ArchivePersistenceException("Hot Archive descriptor is inconsistent", invalid); } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java index 0e65aca6af1..de6f059c569 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java @@ -54,8 +54,7 @@ public final class StateArchiveHotStore implements Closeable { private static final int CATALOG_LENGTH = Integer.BYTES + 2 * Short.BYTES + Long.BYTES + HASH_LENGTH + Integer.BYTES; private static final int RECORD_MAGIC = 0x53414842; // SAHB - private static final short RECORD_VERSION = 1; - private static final short RECORD_VIEW_DIGEST = 1; + private static final short RECORD_VERSION = 2; private static final byte[] BODY_PREFIX = new byte[]{0x42}; private static final byte[] INDEX_PREFIX = new byte[]{0x4b}; private static final String BLOCKS_COLUMN = @@ -264,16 +263,13 @@ public synchronized StateArchiveHotBatchDescriptor planCheckpoint(List blocks = new ArrayList<>(); for (BlockReverseDiff diff : admitted) { BlockReverseDiff block = Objects.requireNonNull(diff, "diff"); BlockSnapshotMeta meta = block.getMeta(); - byte[] viewDigest = block.getMutationViewDigest(); if (meta.getEpoch() != meta.getBlockNumber() || meta.getBlockNumber() != previousBlock + 1 - || !Arrays.equals(meta.getParentHash(), previousHash) - || viewDigest == null) { + || !Arrays.equals(meta.getParentHash(), previousHash)) { throw new IllegalArgumentException( "Hot Archive checkpoint identity is not contiguous"); } @@ -282,8 +278,7 @@ public synchronized StateArchiveHotBatchDescriptor planCheckpoint(List diffs, byte[] preparedTarget, @@ -925,8 +920,6 @@ private void requireExactDescriptor(StateArchiveIndexDatabase.Reader reader, } BlockReverseDiff diff = decodeRecord(record); if (!diff.getMeta().equals(block.getMeta()) - || diff.getMutationViewDigest() == null - || !Arrays.equals(diff.getMutationViewDigest(), block.getMutationViewDigest()) || !Arrays.equals(Hashing.sha256().hashBytes(record).asBytes(), block.getArchiveRecordDigest())) { throw new ArchivePersistenceException("Hot Archive descriptor body identity differs"); @@ -1023,16 +1016,11 @@ private static Catalog loadCatalog(Path path) throws IOException { private byte[] encodeRecord(BlockReverseDiff diff) { try { byte[] history = historyCodec.encode(diff); - byte[] viewDigest = diff.getMutationViewDigest(); - short flags = viewDigest == null ? 0 : RECORD_VIEW_DIGEST; - ByteArrayOutputStream bytes = new ByteArrayOutputStream(history.length + 48); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(history.length + 16); DataOutputStream output = new DataOutputStream(bytes); output.writeInt(RECORD_MAGIC); output.writeShort(RECORD_VERSION); - output.writeShort(flags); - if (viewDigest != null) { - output.write(viewDigest); - } + output.writeShort(0); output.writeInt(history.length); output.write(history); output.flush(); @@ -1059,14 +1047,9 @@ private BlockReverseDiff decodeRecord(byte[] encoded) throws IOException { throw new ArchivePersistenceException("Hot Archive record format is unsupported"); } short flags = input.readShort(); - if ((flags & ~RECORD_VIEW_DIGEST) != 0) { + if (flags != 0) { throw new ArchivePersistenceException("Hot Archive record flags are unsupported"); } - byte[] viewDigest = null; - if ((flags & RECORD_VIEW_DIGEST) != 0) { - viewDigest = new byte[HASH_LENGTH]; - input.readFully(viewDigest); - } int historyLength = input.readInt(); if (historyLength <= 0 || historyLength != input.available()) { throw new ArchivePersistenceException("Hot Archive history length is invalid"); @@ -1079,7 +1062,7 @@ private BlockReverseDiff decodeRecord(byte[] encoded) throws IOException { } catch (IllegalArgumentException invalid) { throw new ArchivePersistenceException("Hot Archive history is corrupt", invalid); } - return new BlockReverseDiff(decoded.getMeta(), decoded.getGroups(), viewDigest); + return decoded; } catch (EOFException truncated) { throw new ArchivePersistenceException("Hot Archive record is truncated", truncated); } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointCapture.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointCapture.java index 980940466a4..6d4c1171535 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointCapture.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointCapture.java @@ -1,7 +1,6 @@ package org.tron.core.db2.core; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -32,10 +31,7 @@ public final class CommonCheckpointCapture { BlockReverseDiff diff = Objects.requireNonNull(this.archiveDiffs.get(index), "archiveDiff"); StateArchiveHotBatchDescriptor.BlockDigest block = archiveBinding.getBlocks().get(index); - if (!diff.getMeta().equals(block.getMeta()) - || diff.getMutationViewDigest() == null - || !Arrays.equals(diff.getMutationViewDigest(), - block.getMutationViewDigest())) { + if (!diff.getMeta().equals(block.getMeta())) { throw new IllegalArgumentException("common checkpoint transient Archive diff differs"); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java index 51a22a30fc8..8b033ee233a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayload.java @@ -15,8 +15,8 @@ /** Versioned immutable redo or coordination input for one cross-authority checkpoint. */ public final class CommonCheckpointPayload { - public static final int FORMAT_VERSION = 1; - public static final int COORDINATION_FORMAT_VERSION = 2; + public static final int FORMAT_VERSION = 3; + public static final int COORDINATION_FORMAT_VERSION = 4; private static final int DIGEST_LENGTH = 32; private static final Comparator MUTATION_ORDER = (left, right) -> compareUnsigned(left.key, right.key); @@ -68,16 +68,12 @@ public static CommonCheckpointPayload create(byte[] formatIdentity, for (int index = 0; index < archives.size(); index++) { PathStateFlushTarget.BlockBinding binding = path.getBlocks().get(index); BlockReverseDiff archive = Objects.requireNonNull(archives.get(index), "archiveBlock"); - if (!binding.getMeta().equals(archive.getMeta()) - || archive.getMutationViewDigest() == null - || !Arrays.equals(binding.getMutationViewDigest(), - archive.getMutationViewDigest())) { + if (!binding.getMeta().equals(archive.getMeta())) { throw new IllegalArgumentException( "common checkpoint Archive and PathState block identity differs"); } blocks.add(new BlockPayload(binding.getMeta(), binding.getParentStateRoot(), - binding.getStateRoot(), binding.getTransitionPayloadDigest(), - binding.getMutationViewDigest(), archive)); + binding.getStateRoot(), binding.getTransitionPayloadDigest(), archive)); } List pathStores = new ArrayList<>(); for (PathStateFlushTarget.StoreTarget store : path.getStores()) { @@ -105,14 +101,13 @@ public static CommonCheckpointPayload createV2(byte[] formatIdentity, for (int index = 0; index < path.getBlocks().size(); index++) { PathStateFlushTarget.BlockBinding binding = path.getBlocks().get(index); StateArchiveHotBatchDescriptor.BlockDigest digest = archive.getBlocks().get(index); - if (!binding.getMeta().equals(digest.getMeta()) - || !Arrays.equals(binding.getMutationViewDigest(), digest.getMutationViewDigest())) { + if (!binding.getMeta().equals(digest.getMeta())) { throw new IllegalArgumentException( "common checkpoint Hot Archive and PathState block identity differs"); } blocks.add(BlockPayload.coordination(binding.getMeta(), binding.getParentStateRoot(), binding.getStateRoot(), binding.getTransitionPayloadDigest(), - binding.getMutationViewDigest(), digest.getArchiveRecordDigest())); + digest.getArchiveRecordDigest())); } List pathStores = new ArrayList<>(); for (PathStateFlushTarget.StoreTarget store : path.getStores()) { @@ -140,14 +135,12 @@ static CommonCheckpointPayload coordinateV2(CommonCheckpointPayload capturedV1, for (int index = 0; index < source.blocks.size(); index++) { BlockPayload block = source.blocks.get(index); StateArchiveHotBatchDescriptor.BlockDigest digest = archive.getBlocks().get(index); - if (!block.meta.equals(digest.getMeta()) - || !Arrays.equals(block.mutationViewDigest, digest.getMutationViewDigest())) { + if (!block.meta.equals(digest.getMeta())) { throw new IllegalArgumentException( "common checkpoint Hot Archive and captured block identity differs"); } blocks.add(BlockPayload.coordination(block.meta, block.parentStateRoot, block.stateRoot, - block.transitionPayloadDigest, block.mutationViewDigest, - digest.getArchiveRecordDigest())); + block.transitionPayloadDigest, digest.getArchiveRecordDigest())); } return new CommonCheckpointPayload(COORDINATION_FORMAT_VERSION, source.formatIdentity, blocks, source.parentStateRoot, source.stateRoot, source.chainbaseStores, @@ -224,10 +217,7 @@ private static void validateBlocks(int version, List blocks, for (BlockPayload block : blocks) { BlockPayload current = Objects.requireNonNull(block, "block"); if (version == FORMAT_VERSION) { - byte[] archiveView = current.requireArchiveDiff().getMutationViewDigest(); - if (archiveView == null || !Arrays.equals(archiveView, current.mutationViewDigest)) { - throw new IllegalArgumentException("checkpoint block mutation-view identity differs"); - } + current.requireArchiveDiff(); } else if (current.archiveDiff != null || current.archiveRecordDigest == null) { throw new IllegalArgumentException("coordination checkpoint contains Archive body"); } @@ -260,7 +250,6 @@ private static void validateBlocks(int version, List blocks, BlockPayload block = blocks.get(index); StateArchiveHotBatchDescriptor.BlockDigest archive = archiveBlocks.get(index); if (!block.meta.equals(archive.getMeta()) - || !Arrays.equals(block.mutationViewDigest, archive.getMutationViewDigest()) || !Arrays.equals(block.archiveRecordDigest, archive.getArchiveRecordDigest())) { throw new IllegalArgumentException("common checkpoint Archive block binding differs"); } @@ -335,19 +324,16 @@ public static final class BlockPayload { private final byte[] parentStateRoot; private final byte[] stateRoot; private final byte[] transitionPayloadDigest; - private final byte[] mutationViewDigest; private final BlockReverseDiff archiveDiff; private final byte[] archiveRecordDigest; BlockPayload(BlockSnapshotMeta meta, byte[] parentStateRoot, byte[] stateRoot, - byte[] transitionPayloadDigest, byte[] mutationViewDigest, - BlockReverseDiff archiveDiff) { + byte[] transitionPayloadDigest, BlockReverseDiff archiveDiff) { this.meta = Objects.requireNonNull(meta, "meta"); this.parentStateRoot = digest(parentStateRoot, "parentStateRoot"); this.stateRoot = digest(stateRoot, "stateRoot"); this.transitionPayloadDigest = digest(transitionPayloadDigest, "transitionPayloadDigest"); - this.mutationViewDigest = digest(mutationViewDigest, "mutationViewDigest"); this.archiveDiff = Objects.requireNonNull(archiveDiff, "archiveDiff"); this.archiveRecordDigest = null; if (!meta.equals(archiveDiff.getMeta())) { @@ -356,23 +342,20 @@ public static final class BlockPayload { } private BlockPayload(BlockSnapshotMeta meta, byte[] parentStateRoot, byte[] stateRoot, - byte[] transitionPayloadDigest, byte[] mutationViewDigest, - byte[] archiveRecordDigest) { + byte[] transitionPayloadDigest, byte[] archiveRecordDigest) { this.meta = Objects.requireNonNull(meta, "meta"); this.parentStateRoot = digest(parentStateRoot, "parentStateRoot"); this.stateRoot = digest(stateRoot, "stateRoot"); this.transitionPayloadDigest = digest(transitionPayloadDigest, "transitionPayloadDigest"); - this.mutationViewDigest = digest(mutationViewDigest, "mutationViewDigest"); this.archiveDiff = null; this.archiveRecordDigest = digest(archiveRecordDigest, "archiveRecordDigest"); } static BlockPayload coordination(BlockSnapshotMeta meta, byte[] parentStateRoot, - byte[] stateRoot, byte[] transitionPayloadDigest, byte[] mutationViewDigest, - byte[] archiveRecordDigest) { + byte[] stateRoot, byte[] transitionPayloadDigest, byte[] archiveRecordDigest) { return new BlockPayload(meta, parentStateRoot, stateRoot, transitionPayloadDigest, - mutationViewDigest, archiveRecordDigest); + archiveRecordDigest); } public BlockSnapshotMeta getMeta() { @@ -391,10 +374,6 @@ public byte[] getTransitionPayloadDigest() { return copy(transitionPayloadDigest); } - public byte[] getMutationViewDigest() { - return copy(mutationViewDigest); - } - public BlockReverseDiff getArchiveDiff() { return requireArchiveDiff(); } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java index cc139369633..ca00ea7959d 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadCodec.java @@ -22,12 +22,12 @@ import org.tron.core.db2.core.CommonCheckpointPayload.StoreMutations; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; -/** Deterministic, bounded codec for v1 redo bodies and v2 digest-only Archive coordination. */ +/** Deterministic, bounded codec for redo bodies and digest-only Archive coordination. */ public final class CommonCheckpointPayloadCodec { public static final int MAGIC = 0x54434350; // TCCP - public static final short VERSION = 1; - public static final short COORDINATION_VERSION = 2; + public static final short VERSION = 3; + public static final short COORDINATION_VERSION = 4; public static final int HEADER_LENGTH = 44; public static final int DEFAULT_MAX_ENCODED_LENGTH = 256 * 1024 * 1024; private static final int DIGEST_LENGTH = 32; @@ -121,7 +121,6 @@ private byte[] encodeBody(CommonCheckpointPayload payload) throws IOException { output.write(block.getParentStateRoot()); output.write(block.getStateRoot()); output.write(block.getTransitionPayloadDigest()); - output.write(block.getMutationViewDigest()); if (admitted.getVersion() == CommonCheckpointPayload.FORMAT_VERSION) { writeBytes(output, historyCodec.encode(block.getArchiveDiff())); } else { @@ -157,12 +156,8 @@ private CommonCheckpointPayload decodeBodyV1(byte[] body) throws IOException { byte[] parentRoot = readExact(input, DIGEST_LENGTH); byte[] blockRoot = readExact(input, DIGEST_LENGTH); byte[] transitionDigest = readExact(input, DIGEST_LENGTH); - byte[] viewDigest = readExact(input, DIGEST_LENGTH); BlockReverseDiff decoded = historyCodec.decode(readBytes(input)); - BlockReverseDiff archive = new BlockReverseDiff(decoded.getMeta(), decoded.getGroups(), - viewDigest); - blocks.add(new BlockPayload(meta, parentRoot, blockRoot, transitionDigest, viewDigest, - archive)); + blocks.add(new BlockPayload(meta, parentRoot, blockRoot, transitionDigest, decoded)); } List chainbase = readStores(input); int pathStoreCount = readCount(input, MAX_STORES, "path-state Store"); @@ -195,11 +190,10 @@ private CommonCheckpointPayload decodeBodyV2(byte[] body) throws IOException { byte[] parentRoot = readExact(input, DIGEST_LENGTH); byte[] blockRoot = readExact(input, DIGEST_LENGTH); byte[] transitionDigest = readExact(input, DIGEST_LENGTH); - byte[] viewDigest = readExact(input, DIGEST_LENGTH); byte[] recordDigest = readExact(input, DIGEST_LENGTH); blocks.add(BlockPayload.coordination(meta, parentRoot, blockRoot, transitionDigest, - viewDigest, recordDigest)); - archiveBlocks.add(BlockDigest.restore(meta, viewDigest, recordDigest)); + recordDigest)); + archiveBlocks.add(BlockDigest.restore(meta, recordDigest)); } StateArchiveHotBatchDescriptor archiveBinding = readArchiveBinding(input, archiveBlocks); List chainbase = readStores(input); @@ -233,7 +227,6 @@ private static void writeArchiveBinding(DataOutputStream output, output.write(binding.getParentContentDigest()); output.write(binding.getResultContentDigest()); output.write(binding.getOrderedRecordDigest()); - output.write(binding.getMutationViewRangeDigest()); } private static StateArchiveHotBatchDescriptor readArchiveBinding(DataInputStream input, @@ -251,12 +244,11 @@ private static StateArchiveHotBatchDescriptor readArchiveBinding(DataInputStream byte[] parentContent = readExact(input, DIGEST_LENGTH); byte[] resultContent = readExact(input, DIGEST_LENGTH); byte[] orderedRecords = readExact(input, DIGEST_LENGTH); - byte[] mutationViews = readExact(input, DIGEST_LENGTH); if (blockCount != blocks.size()) { throw new IllegalArgumentException("Hot Archive binding block count differs"); } return StateArchiveHotBatchDescriptor.restore(engine, parentBlock, parentHash, first, last, - encodedBytes, parentContent, resultContent, orderedRecords, mutationViews, blocks); + encodedBytes, parentContent, resultContent, orderedRecords, blocks); } private static int engineTag(Engine engine) { diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java index fd1dc20598c..a61e01426dc 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java @@ -2,7 +2,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -145,8 +144,7 @@ private static List metas(List layers, String d private static void requireArtifacts(BlockSnapshotMeta meta, BlockReverseDiff archive, PathStateSnapshotDelta path, String dbName) { if (archive == null || path == null || !meta.equals(archive.getMeta()) - || !meta.equals(path.getMeta()) || archive.getMutationViewDigest() == null - || !Arrays.equals(archive.getMutationViewDigest(), path.getMutationViewDigest())) { + || !meta.equals(path.getMeta())) { throw new IllegalStateException("common checkpoint Snapshot artifacts differ: " + dbName); } } @@ -154,15 +152,7 @@ private static void requireArtifacts(BlockSnapshotMeta meta, BlockReverseDiff ar private static void requireSameArtifacts(BlockReverseDiff expectedArchive, PathStateSnapshotDelta expectedPath, BlockReverseDiff archive, PathStateSnapshotDelta path, String dbName) { - if (!expectedArchive.getMeta().equals(archive.getMeta()) - || !Arrays.equals(expectedArchive.getMutationViewDigest(), - archive.getMutationViewDigest()) - || !expectedPath.getMeta().equals(path.getMeta()) - || !Arrays.equals(expectedPath.getParentStateRoot(), path.getParentStateRoot()) - || !Arrays.equals(expectedPath.getStateRoot(), path.getStateRoot()) - || !Arrays.equals(expectedPath.getTransitionPayloadDigest(), - path.getTransitionPayloadDigest()) - || !Arrays.equals(expectedPath.getMutationViewDigest(), path.getMutationViewDigest())) { + if (expectedArchive != archive || expectedPath != path) { throw new IllegalStateException("common checkpoint artifacts differ across state Stores: " + dbName); } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java index e6a2d2f6c91..cd5bcde1439 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotImpl.java @@ -68,14 +68,6 @@ void attachBlockArtifacts(BlockSnapshotMeta meta, BlockReverseDiff reverseDiff, if (pathStateDelta != null && !admitted.equals(pathStateDelta.getMeta())) { throw new IllegalArgumentException("path-state delta differs from Snapshot block identity"); } - if (reverseDiff != null && pathStateDelta != null) { - byte[] archiveView = reverseDiff.getMutationViewDigest(); - if (archiveView == null - || !Arrays.equals(archiveView, pathStateDelta.getMutationViewDigest())) { - throw new IllegalArgumentException( - "archive and path-state artifacts differ from mutation view identity"); - } - } blockSnapshotMeta = meta; preparedArchiveBlock = reverseDiff; preparedPathStateDelta = pathStateDelta; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java index d94dd0589d1..cdb36797578 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateBlockTransition.java @@ -48,16 +48,9 @@ public final class PathStateBlockTransition { private final P66Phase phase; private final List mutations; private final byte[] payloadDigest; - private final byte[] mutationViewDigest; public PathStateBlockTransition(long blockNumber, byte[] blockHash, byte[] parentHash, long timestamp, P66Phase phase, Collection mutations) { - this(blockNumber, blockHash, parentHash, timestamp, phase, mutations, null); - } - - public PathStateBlockTransition(long blockNumber, byte[] blockHash, byte[] parentHash, - long timestamp, P66Phase phase, Collection mutations, - byte[] mutationViewDigest) { if (blockNumber < 0) { throw new IllegalArgumentException("blockNumber must not be negative"); } @@ -73,9 +66,6 @@ public PathStateBlockTransition(long blockNumber, byte[] blockHash, byte[] paren } this.mutations = Collections.unmodifiableList(canonical); this.payloadDigest = sha256(encode(prepared)); - this.mutationViewDigest = mutationViewDigest == null - ? Arrays.copyOf(payloadDigest, payloadDigest.length) - : copyHash(mutationViewDigest, "mutationViewDigest"); } public long getBlockNumber() { @@ -110,10 +100,6 @@ public byte[] getPayloadDigest() { return Arrays.copyOf(payloadDigest, payloadDigest.length); } - public byte[] getMutationViewDigest() { - return Arrays.copyOf(mutationViewDigest, mutationViewDigest.length); - } - private List prepare(Collection supplied) { PathStateParticipantDescriptor descriptor = PathStateParticipantDescriptor.current(); List prepared = new ArrayList<>(); diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateFlushTarget.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateFlushTarget.java index 43cda5f0597..3036a3d25d9 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateFlushTarget.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateFlushTarget.java @@ -170,14 +170,12 @@ public static final class BlockBinding { private final byte[] parentStateRoot; private final byte[] stateRoot; private final byte[] transitionPayloadDigest; - private final byte[] mutationViewDigest; private BlockBinding(PathStateSnapshotDelta delta) { this.meta = delta.getMeta(); this.parentStateRoot = delta.getParentStateRoot(); this.stateRoot = delta.getStateRoot(); this.transitionPayloadDigest = delta.getTransitionPayloadDigest(); - this.mutationViewDigest = delta.getMutationViewDigest(); } public BlockSnapshotMeta getMeta() { @@ -196,9 +194,6 @@ public byte[] getTransitionPayloadDigest() { return copy(transitionPayloadDigest); } - public byte[] getMutationViewDigest() { - return copy(mutationViewDigest); - } } /** Final target for one participant changed anywhere in the coalesced range. */ diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java index bd0534bb3dc..264ad642e9c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java @@ -417,8 +417,7 @@ private static void validateSnapshotDelta(BlockSnapshotMeta meta, return; } if (!meta.equals(delta.getMeta()) - || !Arrays.equals(transition.getPayloadDigest(), delta.getTransitionPayloadDigest()) - || !Arrays.equals(transition.getMutationViewDigest(), delta.getMutationViewDigest())) { + || !Arrays.equals(transition.getPayloadDigest(), delta.getTransitionPayloadDigest())) { throw new IOException("path-state Snapshot delta identity mismatch"); } } diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java index 9fbbe12aae7..ec4a10e2e79 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateSnapshotDelta.java @@ -25,18 +25,16 @@ public final class PathStateSnapshotDelta { private final byte[] parentStateRoot; private final byte[] stateRoot; private final byte[] transitionPayloadDigest; - private final byte[] mutationViewDigest; private final List stores; private final List superNodeMutations; private PathStateSnapshotDelta(BlockSnapshotMeta meta, byte[] parentStateRoot, - byte[] stateRoot, byte[] transitionPayloadDigest, byte[] mutationViewDigest, + byte[] stateRoot, byte[] transitionPayloadDigest, List stores, List superNodeMutations) { this.meta = Objects.requireNonNull(meta, "meta"); this.parentStateRoot = root(parentStateRoot, "parentStateRoot"); this.stateRoot = root(stateRoot, "stateRoot"); this.transitionPayloadDigest = root(transitionPayloadDigest, "transitionPayloadDigest"); - this.mutationViewDigest = root(mutationViewDigest, "mutationViewDigest"); this.stores = Collections.unmodifiableList(new ArrayList<>(stores)); this.superNodeMutations = immutableMutations(superNodeMutations); } @@ -82,8 +80,7 @@ static PathStateSnapshotDelta from(BlockSnapshotMeta meta, } } return new PathStateSnapshotDelta(admittedMeta, candidate.getParent().getStateRoot(), - candidate.getStateRoot(), transition.getPayloadDigest(), - transition.getMutationViewDigest(), deltas, superMutations); + candidate.getStateRoot(), transition.getPayloadDigest(), deltas, superMutations); } static PathStateSnapshotDelta fromPhysical(BlockSnapshotMeta meta, @@ -97,8 +94,8 @@ static PathStateSnapshotDelta fromPhysical(BlockSnapshotMeta meta, PathStateRoot.Snapshot admittedSnapshot = Objects.requireNonNull(snapshot, "snapshot"); requireSameBlock(admittedMeta, admittedTransition); return new PathStateSnapshotDelta(admittedMeta, admittedParent.getStateRoot(), - admittedSnapshot.getStateRoot(), admittedTransition.getPayloadDigest(), - admittedTransition.getMutationViewDigest(), stores, superNodeMutations); + admittedSnapshot.getStateRoot(), admittedTransition.getPayloadDigest(), stores, + superNodeMutations); } public BlockSnapshotMeta getMeta() { @@ -113,10 +110,6 @@ public byte[] getStateRoot() { return Arrays.copyOf(stateRoot, stateRoot.length); } - public byte[] getMutationViewDigest() { - return Arrays.copyOf(mutationViewDigest, mutationViewDigest.length); - } - public byte[] getTransitionPayloadDigest() { return Arrays.copyOf(transitionPayloadDigest, transitionPayloadDigest.length); } diff --git a/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java b/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java index 760619e852e..be27be0c6c7 100644 --- a/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java +++ b/framework/src/test/java/org/tron/core/db2/SnapshotImplTest.java @@ -193,31 +193,6 @@ public void testAttachBlockArtifactsRequiresOneSnapshotIdentity() throws Excepti assertSame(delta, layer.getPreparedPathStateDelta()); } - @Test - public void testAttachBlockArtifactsRequiresOneMutationViewIdentity() throws Exception { - SnapshotRoot root = new SnapshotRoot(tronDatabase.getDb()); - SnapshotImpl layer = getSnapshotImplIns(root); - BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); - byte[] viewDigest = hash(7); - BlockReverseDiff reverseDiff = new BlockReverseDiff(meta, Collections.emptyList(), - viewDigest); - PathStateSnapshotDelta delta = mock(PathStateSnapshotDelta.class); - when(delta.getMeta()).thenReturn(meta); - when(delta.getMutationViewDigest()).thenReturn(viewDigest); - - attachBlockArtifacts(layer, meta, reverseDiff, delta); - assertSame(reverseDiff, layer.getPreparedArchiveBlock()); - assertSame(delta, layer.getPreparedPathStateDelta()); - - PathStateSnapshotDelta wrong = mock(PathStateSnapshotDelta.class); - when(wrong.getMeta()).thenReturn(meta); - when(wrong.getMutationViewDigest()).thenReturn(hash(8)); - InvocationTargetException failure = assertThrows(InvocationTargetException.class, - () -> attachBlockArtifacts(layer, meta, reverseDiff, wrong)); - assertEquals(IllegalArgumentException.class, failure.getCause().getClass()); - assertSame(delta, layer.getPreparedPathStateDelta()); - } - /** * The constructor of SnapshotImpl is not public * so reflection is used to construct the object here. diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index 28fbb1886c1..a3f2459a851 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -100,8 +100,7 @@ public void deferredPathStateCaptureDoesNotWaitForCollectorAndDrainsOnClose() return new PathStateBlockTransition(meta.getBlockNumber(), meta.getBlockHash(), meta.getParentHash(), meta.getTimestamp(), P66Phase.P66_ON, Collections.singletonList( - PathStateMutation.put("code", bytes("contract"), bytes("runtime"))), - view.getMutationViewDigest()); + PathStateMutation.put("code", bytes("contract"), bytes("runtime")))); }, published::set, (blockNumber, blockHash) -> { }, null, (meta, transition) -> null); attachment.synchronizeReadyHead(PathStateRootMetadata.base(0, hash(0), hash(9), 0, P66Phase.P66_ON, hash(7), hash(8), hash(6))); @@ -137,14 +136,10 @@ public void pathStateRuntimeCoexistsWithArchiveAtBlockFinalBoundary() throws Exc manager.enable(); manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); AtomicReference published = new AtomicReference<>(); - AtomicReference capturedViewDigest = new AtomicReference<>(); SnapshotPathStateTransitionCollector collector = new SnapshotPathStateTransitionCollector( key -> Collections.emptyMap()); PathStateRuntimeAttachment attachment = new PathStateRuntimeAttachment( - view -> { - capturedViewDigest.set(view.getMutationViewDigest()); - return collector.collect(view); - }, published::set, + collector::collect, published::set, (blockNumber, blockHash) -> { }, null, (meta, transition) -> null); manager.attachPathStateRuntime(attachment); @@ -159,9 +154,6 @@ public void pathStateRuntimeCoexistsWithArchiveAtBlockFinalBoundary() throws Exc assertEquals(1, published.get().getMutations().size()); assertEquals("code", published.get().getMutations().get(0).getDbName()); assertArrayEquals(key, published.get().getMutations().get(0).getCanonicalKey()); - assertArrayEquals(capturedViewDigest.get(), published.get().getMutationViewDigest()); - assertFalse(Arrays.equals(published.get().getPayloadDigest(), - published.get().getMutationViewDigest())); assertSame(attachment, manager.detachPathStateRuntime(attachment)); manager.shutdown(); } @@ -181,13 +173,11 @@ public void pathStateForwardDeltaIsOwnedByTheSameBlockSnapshotLayer() throws Exc return new PathStateBlockTransition(meta.getBlockNumber(), meta.getBlockHash(), meta.getParentHash(), meta.getTimestamp(), P66Phase.P66_ON, Collections.singletonList( - PathStateMutation.put("code", bytes("contract"), bytes("runtime"))), - view.getMutationViewDigest()); + PathStateMutation.put("code", bytes("contract"), bytes("runtime")))); }, published::set, (blockNumber, blockHash) -> { }, null, (meta, transition) -> { PathStateSnapshotDelta delta = mock(PathStateSnapshotDelta.class); when(delta.getMeta()).thenReturn(meta); when(delta.getTransitionPayloadDigest()).thenReturn(transition.getPayloadDigest()); - when(delta.getMutationViewDigest()).thenReturn(transition.getMutationViewDigest()); prepared.set(delta); return delta; }); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java index fdcd22e6fca..b95cb31bd74 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java @@ -54,8 +54,6 @@ public void preservesEveryBlockBoundaryBeforePublishingReadableAcrossReopen() for (int index = 0; index < payload.getBlocks().size(); index++) { BlockReverseDiff actual = materializer.loadBlock(target, index); assertEquals(payload.getBlocks().get(index).getMeta(), actual.getMeta()); - assertArrayEquals(payload.getBlocks().get(index).getMutationViewDigest(), - actual.getMutationViewDigest()); assertEquals("code", actual.getGroups().get(0).getDbName()); } @@ -251,7 +249,6 @@ private static CommonCheckpointPayload payload(byte[] format, long firstBlock, i long number = firstBlock + index; byte[] blockHash = hash((int) number); byte[] nextRoot = index == count - 1 ? stateRoot : hash(30 + (int) number); - byte[] view = hash(60 + (int) number); BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(number, blockHash, priorHash, number * 3_000L); PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); @@ -259,11 +256,11 @@ private static CommonCheckpointPayload payload(byte[] format, long firstBlock, i when(binding.getParentStateRoot()).thenReturn(priorRoot); when(binding.getStateRoot()).thenReturn(nextRoot); when(binding.getTransitionPayloadDigest()).thenReturn(hash(70 + (int) number)); - when(binding.getMutationViewDigest()).thenReturn(view); bindings.add(binding); - archives.add(new BlockReverseDiff(meta, Collections.singletonList(new DbGroup( - "code", Collections.singletonList(new Entry(new byte[]{(byte) number}, - OldValue.present(new byte[]{(byte) (number - 1)}))))), view)); + DbGroup group = new DbGroup("code", Collections.singletonList( + new Entry(new byte[]{(byte) number}, + OldValue.present(new byte[]{(byte) (number - 1)})))); + archives.add(new BlockReverseDiff(meta, Collections.singletonList(group))); priorHash = blockHash; priorRoot = nextRoot; } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshotTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshotTest.java index 7e661c638cf..b90c337daa8 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshotTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointReadSnapshotTest.java @@ -113,7 +113,6 @@ private static CommonCheckpointPayload payload(byte[] format, long firstBlock, i long number = firstBlock + index; byte[] blockHash = hash((int) number); byte[] nextRoot = hash(31 + index); - byte[] view = hash(60 + index); BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(number, blockHash, priorHash, number * 3_000L); PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); @@ -121,11 +120,11 @@ private static CommonCheckpointPayload payload(byte[] format, long firstBlock, i when(binding.getParentStateRoot()).thenReturn(priorRoot); when(binding.getStateRoot()).thenReturn(nextRoot); when(binding.getTransitionPayloadDigest()).thenReturn(hash(70 + index)); - when(binding.getMutationViewDigest()).thenReturn(view); bindings.add(binding); - archives.add(new BlockReverseDiff(meta, Collections.singletonList(new DbGroup( - "code", Collections.singletonList(new Entry(new byte[]{(byte) number}, - OldValue.present(new byte[]{(byte) (number - 1)}))))), view)); + DbGroup group = new DbGroup("code", Collections.singletonList( + new Entry(new byte[]{(byte) number}, + OldValue.present(new byte[]{(byte) (number - 1)})))); + archives.add(new BlockReverseDiff(meta, Collections.singletonList(group))); priorHash = blockHash; priorRoot = nextRoot; } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializerTest.java index 158df64cc0a..4f4676fcb38 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializerTest.java @@ -51,7 +51,7 @@ public void commonParticipantOnlyVerifiesPreparedHotBodiesThenPublishes() throws assertThrows(IllegalArgumentException.class, () -> materializer.prepare(target, Collections.singletonList( new BlockReverseDiff(BlockSnapshotMeta.forBlock(2, hash(2), hash(1), 6_000L), - Collections.emptyList(), hash(62))))); + Collections.emptyList())))); assertThrows(ArchivePersistenceException.class, () -> store.loadBlock(1)); assertThrows(java.io.IOException.class, () -> materializer.materialize(payload, target)); @@ -116,13 +116,11 @@ private StateArchiveHotStore open(Path root, byte[] format) throws java.io.IOExc private static CommonCheckpointPayload payload(byte[] format, StateArchiveHotBatchDescriptor descriptor) { BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); - byte[] viewDigest = hash(61); PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); when(binding.getMeta()).thenReturn(meta); when(binding.getParentStateRoot()).thenReturn(hash(5)); when(binding.getStateRoot()).thenReturn(hash(6)); when(binding.getTransitionPayloadDigest()).thenReturn(hash(71)); - when(binding.getMutationViewDigest()).thenReturn(viewDigest); PathStateFlushTarget pathState = mock(PathStateFlushTarget.class); when(pathState.getBlocks()).thenReturn(Collections.singletonList(binding)); when(pathState.getParentStateRoot()).thenReturn(hash(5)); @@ -137,7 +135,7 @@ private static BlockReverseDiff diff() { return new BlockReverseDiff(BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L), Collections.singletonList(new BlockReverseDiff.DbGroup("code", Collections.singletonList(new BlockReverseDiff.Entry(new byte[]{1}, - OldValue.absent())))), hash(61)); + OldValue.absent()))))); } private static byte[] hash(int marker) { diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java index 03f636b48e5..7630a796a72 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotProcessRecoveryTest.java @@ -121,7 +121,7 @@ private static void assertPreparedBatch(Path hotRoot, Engine engine) throws Exce private static BlockReverseDiff diff(long block, int parent) { return new BlockReverseDiff(meta(block, parent), Arrays.asList( - group("account"), group("code"), group("storage-row")), hash(60 + (int) block)); + group("account"), group("code"), group("storage-row"))); } private static DbGroup group(String store) { diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java index cfbb2938f08..654ef1da922 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveHotStoreTest.java @@ -57,7 +57,6 @@ public void appendsSealsQueriesAndReopensAcrossBothEngines() throws Exception { assertEquals(1, reopened.getCurrentGenerationId()); assertEquals(Collections.singletonList(0L), reopened.getFrozenGenerationIds()); assertEquals(1, reopened.loadBlock(1).getMeta().getBlockNumber()); - assertArrayEquals(hash(61), reopened.loadBlock(1).getMutationViewDigest()); assertLookup(reopened.findOldValueAfter("account", new byte[]{3}, 2), 3, OldValue.present(new byte[]{33})); assertFalse(reopened.findOldValueAfter("missing", new byte[]{1}, 0).isPresent()); @@ -80,7 +79,7 @@ public void rocksCheckpointWritesOneExactCrossColumnFamilyBatch() throws Excepti new DbGroup("code", Collections.singletonList( new Entry(new byte[]{2}, OldValue.present(new byte[]{22})))), new DbGroup("storage-row", Collections.singletonList( - new Entry(new byte[]{3}, OldValue.present(new byte[0]))))), hash(122)); + new Entry(new byte[]{3}, OldValue.present(new byte[0])))))); Path database = root.resolve(StateArchiveHotStore.GENERATIONS) .resolve("00000000000000000000").resolve(StateArchiveHotStore.DATABASE); @@ -124,6 +123,30 @@ public void rocksCheckpointWritesOneExactCrossColumnFamilyBatch() throws Excepti } } + @Test + public void rejectsDescriptorLayoutBeforeMutationDigestRemoval() throws Exception { + Path root = temporaryFolder.newFolder("hot-descriptor-version").toPath(); + byte[] format = hash(123); + try (StateArchiveHotStore store = open(root, format, Engine.LEVELDB, + 0, hash(0), 3, 10)) { + StateArchiveHotBatchDescriptor descriptor = store.planCheckpoint( + Collections.singletonList(diff(1, 0, "code", new byte[]{1}, OldValue.absent()))); + StateArchiveHotBatchDescriptorCodec codec = new StateArchiveHotBatchDescriptorCodec(); + byte[] encoded = codec.encode(descriptor); + + byte[] oldEnvelopeVersion = Arrays.copyOf(encoded, encoded.length); + ByteBuffer.wrap(oldEnvelopeVersion).putShort(Integer.BYTES, (short) 1); + assertThrows(ArchivePersistenceException.class, () -> codec.decode(oldEnvelopeVersion)); + + byte[] oldBodyVersion = Arrays.copyOf(encoded, encoded.length); + ByteBuffer.wrap(oldBodyVersion).putShort(44, (short) 1); + byte[] body = Arrays.copyOfRange(oldBodyVersion, 44, oldBodyVersion.length); + byte[] checksum = com.google.common.hash.Hashing.sha256().hashBytes(body).asBytes(); + System.arraycopy(checksum, 0, oldBodyVersion, 12, checksum.length); + assertThrows(ArchivePersistenceException.class, () -> codec.decode(oldBodyVersion)); + } + } + @Test public void recoversEveryRotationPublicationBoundary() throws Exception { for (StateArchiveHotStore.Stage failedStage : Arrays.asList( @@ -452,7 +475,7 @@ private static BlockReverseDiff diff(long block, int parent, String dbName, byte OldValue oldValue) { return new BlockReverseDiff(BlockSnapshotMeta.forBlock(block, hash((int) block), hash(parent), block * 3_000), Collections.singletonList(new DbGroup(dbName, - Collections.singletonList(new Entry(key, oldValue)))), hash(60 + (int) block)); + Collections.singletonList(new Entry(key, oldValue))))); } private static BlockSnapshotMeta meta(long block, int parent, int hashMarker) { diff --git a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java index 093720f05a0..12233182cce 100644 --- a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java @@ -175,16 +175,14 @@ public void payloadFactoryCoalescesSnapshotMutationsWithoutDurableReads() throws number * 3_000L); byte[] parentRoot = hash(10 + number - 1); byte[] stateRoot = hash(10 + number); - byte[] view = hash(40 + number); PathStateSnapshotDelta path = mock(PathStateSnapshotDelta.class); when(path.getMeta()).thenReturn(meta); when(path.getParentStateRoot()).thenReturn(parentRoot); when(path.getStateRoot()).thenReturn(stateRoot); when(path.getTransitionPayloadDigest()).thenReturn(hash(50 + number)); - when(path.getMutationViewDigest()).thenReturn(view); when(path.getStores()).thenReturn(Collections.emptyList()); when(path.getSuperNodeMutations()).thenReturn(Collections.emptyList()); - BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList(), view); + BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList()); SnapshotImpl codeLayer = append(codeChainbase, meta, archive, path); SnapshotImpl storageLayer = append(storageChainbase, meta, archive, path); @@ -315,10 +313,9 @@ public void snapshotRebaserDropsOnlyMaterializedPrefixWithoutSecondStoreWrite() for (int number = 1; number <= 3; number++) { BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(number, hash(number), hash(number - 1), number * 3_000L); - byte[] view = hash(40 + number); PathStateSnapshotDelta path = pathDelta(meta, hash(10 + number - 1), hash(10 + number), - view); - BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList(), view); + hash(40 + number)); + BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList()); append(codeChainbase, meta, archive, path).put(new byte[]{1}, new byte[]{(byte) number}); append(storageChainbase, meta, archive, path).put(new byte[]{3}, @@ -379,7 +376,7 @@ public void memoryRebaseFailureLeavesEverySnapshotPointerUnchanged() throws Exce BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); byte[] view = hash(41); PathStateSnapshotDelta path = pathDelta(meta, hash(10), hash(11), view); - BlockReverseDiff archiveBlock = new BlockReverseDiff(meta, Collections.emptyList(), view); + BlockReverseDiff archiveBlock = new BlockReverseDiff(meta, Collections.emptyList()); SnapshotImpl layer = append(database, meta, archiveBlock, path); layer.put(new byte[]{1}, new byte[]{2}); @@ -420,7 +417,7 @@ public void runtimeComposesStartupCheckpointRebaseAndPointQuery() throws Excepti PathStateSnapshotDelta path = pathDelta(meta, hash(10), hash(11), view); BlockReverseDiff archiveBlock = new BlockReverseDiff(meta, Collections.singletonList(new DbGroup("code", Collections.singletonList( - new Entry(new byte[]{1}, OldValue.present(new byte[]{0}))))), view); + new Entry(new byte[]{1}, OldValue.present(new byte[]{0})))))); append(database, meta, archiveBlock, path).put(new byte[]{1}, new byte[]{2}); ChainbaseCheckpointMaterializer chainbase = new ChainbaseCheckpointMaterializer( @@ -598,13 +595,11 @@ private static CommonCheckpointPayload payload(byte[] format, long blockNumber, String... storeOverride) { BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(blockNumber, blockHash, parentHash, blockNumber * 3_000L); - byte[] view = hash(40 + (int) blockNumber); PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); when(binding.getMeta()).thenReturn(meta); when(binding.getParentStateRoot()).thenReturn(parentRoot); when(binding.getStateRoot()).thenReturn(stateRoot); when(binding.getTransitionPayloadDigest()).thenReturn(hash(50 + (int) blockNumber)); - when(binding.getMutationViewDigest()).thenReturn(view); PathStateFlushTarget pathState = mock(PathStateFlushTarget.class); when(pathState.getBlocks()).thenReturn(Collections.singletonList(binding)); when(pathState.getParentStateRoot()).thenReturn(parentRoot); @@ -622,7 +617,7 @@ private static CommonCheckpointPayload payload(byte[] format, long blockNumber, Collections.singletonList( new CommonCheckpointPayload.Mutation(new byte[]{3}, new byte[]{4})))); } - BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList(), view); + BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList()); return CommonCheckpointPayload.create(format, pathState, Collections.singletonList(archive), stores); } @@ -639,13 +634,11 @@ private static CommonCheckpointPayload integratedPayload(byte[] format, PathStateParticipantScope scope) { long blockNumber = 1; BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(blockNumber, hash(1), hash(0), 3_000L); - byte[] view = hash(41); PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); when(binding.getMeta()).thenReturn(meta); when(binding.getParentStateRoot()).thenReturn(hash(10)); when(binding.getStateRoot()).thenReturn(hash(11)); when(binding.getTransitionPayloadDigest()).thenReturn(hash(51)); - when(binding.getMutationViewDigest()).thenReturn(view); PathStateFlushTarget.StoreTarget account = mock(PathStateFlushTarget.StoreTarget.class); when(account.getStoreId()).thenReturn(scope.require("account").getStoreId()); when(account.getDbName()).thenReturn("account"); @@ -665,7 +658,7 @@ private static CommonCheckpointPayload integratedPayload(byte[] format, new CommonCheckpointPayload.StoreMutations("code", Collections.singletonList( new CommonCheckpointPayload.Mutation(new byte[]{1}, new byte[]{2})))); return CommonCheckpointPayload.create(format, pathState, - Collections.singletonList(new BlockReverseDiff(meta, Collections.emptyList(), view)), + Collections.singletonList(new BlockReverseDiff(meta, Collections.emptyList())), chainbase); } @@ -691,7 +684,6 @@ private static PathStateSnapshotDelta pathDelta(BlockSnapshotMeta meta, byte[] p when(path.getParentStateRoot()).thenReturn(parentRoot); when(path.getStateRoot()).thenReturn(stateRoot); when(path.getTransitionPayloadDigest()).thenReturn(hash(50 + (int) meta.getBlockNumber())); - when(path.getMutationViewDigest()).thenReturn(view); when(path.getStores()).thenReturn(Collections.emptyList()); when(path.getSuperNodeMutations()).thenReturn(Collections.emptyList()); return path; @@ -738,7 +730,7 @@ private V2Snapshots() { PathStateSnapshotDelta path = pathDelta(meta, hash(10), hash(11), view); BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.singletonList(new DbGroup("code", Collections.singletonList( - new Entry(new byte[]{1}, OldValue.present(new byte[]{0}))))), view); + new Entry(new byte[]{1}, OldValue.present(new byte[]{0})))))); codeLayer = append(codeDatabase, meta, archive, path); SnapshotImpl propertiesLayer = append(propertiesDatabase, meta, archive, path); codeLayer.put(new byte[]{1}, new byte[]{2}); diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointFileTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointFileTest.java index 03201e732e0..affa8fd0357 100644 --- a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointFileTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointFileTest.java @@ -107,20 +107,18 @@ private static CommonCheckpointFile.FaultHook failAt(CommonCheckpointFile.Stage private static CommonCheckpointPayload payload(int seed) { BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); - byte[] viewDigest = hash(4); PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); when(binding.getMeta()).thenReturn(meta); when(binding.getParentStateRoot()).thenReturn(hash(5)); when(binding.getStateRoot()).thenReturn(hash(6)); when(binding.getTransitionPayloadDigest()).thenReturn(hash(3)); - when(binding.getMutationViewDigest()).thenReturn(viewDigest); PathStateFlushTarget target = mock(PathStateFlushTarget.class); when(target.getBlocks()).thenReturn(Collections.singletonList(binding)); when(target.getParentStateRoot()).thenReturn(hash(5)); when(target.getStateRoot()).thenReturn(hash(6)); when(target.getStores()).thenReturn(Collections.emptyList()); when(target.getSuperNodeMutations()).thenReturn(Collections.emptyList()); - BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList(), viewDigest); + BlockReverseDiff archive = new BlockReverseDiff(meta, Collections.emptyList()); return CommonCheckpointPayload.create(hash(seed), target, Collections.singletonList(archive), Collections.singletonList( new CommonCheckpointPayload.StoreMutations("code", Collections.singletonList( diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointHotRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointHotRecoveryTest.java index 10d9646755b..cf777a78732 100644 --- a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointHotRecoveryTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointHotRecoveryTest.java @@ -95,7 +95,7 @@ public void absentWalUsesPersistedDynamicAndTruncatesOrphanIdempotently() throws Path root = temporaryFolder.newFolder("dynamic-authority").toPath(); byte[] format = hash(70); BlockSnapshotMeta block = meta(1); - BlockReverseDiff diff = new BlockReverseDiff(block, Collections.emptyList(), hash(40)); + BlockReverseDiff diff = new BlockReverseDiff(block, Collections.emptyList()); try (StateArchiveHotStore hot = StateArchiveHotStore.openOrCreate(root.resolve("hot"), format, Engine.LEVELDB, 0, hash(0), 3, 10, 1024 * 1024)) { StateArchiveHotBatchDescriptor descriptor = hot.planCheckpoint( @@ -165,11 +165,10 @@ public void absentWalRejectsMissingOrDriftedBlockStoreIdentity() throws Exceptio private CommonCheckpointPayload payload(Path hotPath, BlockSnapshotMeta meta, List stores, Engine engine) throws Exception { - byte[] viewDigest = hash(40); - BlockReverseDiff diff = new BlockReverseDiff(meta, Collections.emptyList(), viewDigest); + BlockReverseDiff diff = new BlockReverseDiff(meta, Collections.emptyList()); try (StateArchiveHotStore hot = StateArchiveHotStore.openOrCreate(hotPath, hash(70), engine, 0, hash(0), 3, 10, 1024 * 1024)) { - return CommonCheckpointPayload.createV2(hash(70), pathState(meta, viewDigest), + return CommonCheckpointPayload.createV2(hash(70), pathState(meta), hot.planCheckpoint(Collections.singletonList(diff)), stores); } } @@ -185,13 +184,12 @@ private static List dynamic(long number, new CommonCheckpointPayload.Mutation(HASH_KEY, hash)))); } - private static PathStateFlushTarget pathState(BlockSnapshotMeta meta, byte[] viewDigest) { + private static PathStateFlushTarget pathState(BlockSnapshotMeta meta) { PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); when(binding.getMeta()).thenReturn(meta); when(binding.getParentStateRoot()).thenReturn(hash(50)); when(binding.getStateRoot()).thenReturn(hash(51)); when(binding.getTransitionPayloadDigest()).thenReturn(hash(52)); - when(binding.getMutationViewDigest()).thenReturn(viewDigest); PathStateFlushTarget pathState = mock(PathStateFlushTarget.class); when(pathState.getBlocks()).thenReturn(Collections.singletonList(binding)); when(pathState.getParentStateRoot()).thenReturn(hash(50)); diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointPayloadV2Test.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointPayloadV2Test.java index 6f1279eb996..f3cc52b0ea5 100644 --- a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointPayloadV2Test.java +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointPayloadV2Test.java @@ -10,6 +10,7 @@ import java.nio.ByteBuffer; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; import org.junit.Rule; import org.junit.Test; @@ -38,22 +39,25 @@ public void roundTripsDigestOnlyArchiveBindingWithoutOldValueBody() throws Excep oldValueSentinel[index] = (byte) (0xa0 + index % 31); } BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); - byte[] viewDigest = hash(61); BlockReverseDiff diff = new BlockReverseDiff(meta, Collections.singletonList(new DbGroup("code", Collections.singletonList( - new Entry(new byte[]{1}, OldValue.present(oldValueSentinel))))), viewDigest); + new Entry(new byte[]{1}, OldValue.present(oldValueSentinel)))))); Path root = temporaryFolder.newFolder("payload-v2").toPath(); try (StateArchiveHotStore hotStore = StateArchiveHotStore.openOrCreate(root, format, Engine.LEVELDB, 0, hash(0), 3, 10, 1024 * 1024)) { StateArchiveHotBatchDescriptor descriptor = hotStore.planCheckpoint( Collections.singletonList(diff)); CommonCheckpointPayload payload = CommonCheckpointPayload.createV2(format, - pathState(meta, viewDigest), descriptor, Collections.emptyList()); + pathState(meta), descriptor, Collections.emptyList()); CommonCheckpointPayloadCodec codec = new CommonCheckpointPayloadCodec(); byte[] encoded = codec.encode(payload); assertEquals(CommonCheckpointPayload.COORDINATION_FORMAT_VERSION, ByteBuffer.wrap(encoded, Integer.BYTES, Short.BYTES).getShort()); + byte[] oldCoordinationVersion = Arrays.copyOf(encoded, encoded.length); + ByteBuffer.wrap(oldCoordinationVersion).putShort(Integer.BYTES, (short) 2); + assertThrows(IllegalArgumentException.class, + () -> codec.decode(oldCoordinationVersion)); assertFalse(contains(encoded, oldValueSentinel)); CommonCheckpointPayload decoded = codec.decode(encoded); assertEquals(CommonCheckpointPayload.COORDINATION_FORMAT_VERSION, @@ -71,13 +75,13 @@ public void roundTripsDigestOnlyArchiveBindingWithoutOldValueBody() throws Excep BlockReverseDiff changedBody = new BlockReverseDiff(meta, Collections.singletonList(new DbGroup("code", Collections.singletonList( - new Entry(new byte[]{1}, OldValue.absent())))), viewDigest); + new Entry(new byte[]{1}, OldValue.absent()))))); assertThrows(ArchivePersistenceException.class, () -> hotStore.prepareCheckpoint(hash(120), descriptor, Collections.singletonList(changedBody))); CommonCheckpointPayload v1 = CommonCheckpointPayload.create(format, - pathState(meta, viewDigest), Collections.singletonList(diff), Collections.emptyList()); + pathState(meta), Collections.singletonList(diff), Collections.emptyList()); byte[] encodedV1 = codec.encode(v1); assertEquals(CommonCheckpointPayload.FORMAT_VERSION, ByteBuffer.wrap(encodedV1, Integer.BYTES, Short.BYTES).getShort()); @@ -90,8 +94,7 @@ public void roundTripsDigestOnlyArchiveBindingWithoutOldValueBody() throws Excep public void roundTripsExplicitRocksEngineIdentity() throws Exception { byte[] format = hash(8); BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); - byte[] viewDigest = hash(62); - BlockReverseDiff diff = new BlockReverseDiff(meta, Collections.emptyList(), viewDigest); + BlockReverseDiff diff = new BlockReverseDiff(meta, Collections.emptyList()); Path root = temporaryFolder.newFolder("payload-v2-rocks").toPath(); try (StateArchiveHotStore hotStore = StateArchiveHotStore.openOrCreate(root, format, Engine.ROCKSDB, 0, hash(0), 3, 10, 1024 * 1024)) { @@ -99,18 +102,17 @@ public void roundTripsExplicitRocksEngineIdentity() throws Exception { Collections.singletonList(diff)); CommonCheckpointPayload decoded = new CommonCheckpointPayloadCodec().decode( new CommonCheckpointPayloadCodec().encode(CommonCheckpointPayload.createV2(format, - pathState(meta, viewDigest), descriptor, Collections.emptyList()))); + pathState(meta), descriptor, Collections.emptyList()))); assertEquals(Engine.ROCKSDB, decoded.getArchiveBinding().getEngine()); } } - private static PathStateFlushTarget pathState(BlockSnapshotMeta meta, byte[] viewDigest) { + private static PathStateFlushTarget pathState(BlockSnapshotMeta meta) { PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); when(binding.getMeta()).thenReturn(meta); when(binding.getParentStateRoot()).thenReturn(hash(5)); when(binding.getStateRoot()).thenReturn(hash(6)); when(binding.getTransitionPayloadDigest()).thenReturn(hash(71)); - when(binding.getMutationViewDigest()).thenReturn(viewDigest); PathStateFlushTarget pathState = mock(PathStateFlushTarget.class); when(pathState.getBlocks()).thenReturn(Collections.singletonList(binding)); when(pathState.getParentStateRoot()).thenReturn(hash(5)); diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java index bc91155d87d..9c3aaf0f147 100644 --- a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java @@ -327,13 +327,11 @@ private Fixture fixture(String name, CommonCheckpointRedoCoordinator.Stage failu private static CommonCheckpointPayload payload() { BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 3_000L); - byte[] viewDigest = hash(4); PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); when(binding.getMeta()).thenReturn(meta); when(binding.getParentStateRoot()).thenReturn(hash(5)); when(binding.getStateRoot()).thenReturn(hash(6)); when(binding.getTransitionPayloadDigest()).thenReturn(hash(3)); - when(binding.getMutationViewDigest()).thenReturn(viewDigest); PathStateFlushTarget target = mock(PathStateFlushTarget.class); when(target.getBlocks()).thenReturn(Collections.singletonList(binding)); when(target.getParentStateRoot()).thenReturn(hash(5)); @@ -341,7 +339,7 @@ private static CommonCheckpointPayload payload() { when(target.getStores()).thenReturn(Collections.emptyList()); when(target.getSuperNodeMutations()).thenReturn(Collections.emptyList()); return CommonCheckpointPayload.create(hash(7), target, - Collections.singletonList(new BlockReverseDiff(meta, Collections.emptyList(), viewDigest)), + Collections.singletonList(new BlockReverseDiff(meta, Collections.emptyList())), Collections.emptyList()); } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializerTest.java index fe147ae0f16..c9258aa9f4b 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateCheckpointMaterializerTest.java @@ -158,13 +158,11 @@ private static CommonCheckpointPayload payload(byte[] formatIdentity, long block byte[] parentHash, byte[] blockHash, byte[] parentRoot, byte[] stateRoot) { BlockSnapshotMeta meta = BlockSnapshotMeta.forBlock(blockNumber, blockHash, parentHash, blockNumber * 3_000L); - byte[] viewDigest = hash((int) blockNumber + 20); PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); when(binding.getMeta()).thenReturn(meta); when(binding.getParentStateRoot()).thenReturn(parentRoot); when(binding.getStateRoot()).thenReturn(stateRoot); when(binding.getTransitionPayloadDigest()).thenReturn(hash((int) blockNumber + 30)); - when(binding.getMutationViewDigest()).thenReturn(viewDigest); PathStateFlushTarget.StoreTarget store = mock(PathStateFlushTarget.StoreTarget.class); when(store.getStoreId()).thenReturn(4); @@ -183,7 +181,7 @@ private static CommonCheckpointPayload payload(byte[] formatIdentity, long block when(target.getSuperNodeMutations()).thenReturn(Collections.singletonList( new PathStateSnapshotDelta.Mutation(new byte[]{5}, new byte[]{6}))); return CommonCheckpointPayload.create(formatIdentity, target, - Collections.singletonList(new BlockReverseDiff(meta, Collections.emptyList(), viewDigest)), + Collections.singletonList(new BlockReverseDiff(meta, Collections.emptyList())), Collections.emptyList()); } diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index c774b4ec1ad..358a2827d59 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -514,8 +514,8 @@ pathDirectory, Engine.LEVELDB, new PathStateLayerLimits(8, 1L << 20), 1L << 20, PathStateFlushTarget target = PathStateFlushTarget.coalesce( Collections.singletonList(delta)); payload = CommonCheckpointPayload.create(formatIdentity, target, - Collections.singletonList(new BlockReverseDiff(targetMeta, Collections.emptyList(), - delta.getMutationViewDigest())), Collections.emptyList()); + Collections.singletonList(new BlockReverseDiff(targetMeta, Collections.emptyList())), + Collections.emptyList()); } CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); CommonCheckpointMaterializedStore materializedStore = diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index 370dfad8e95..e5cdeb189ee 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -834,8 +834,8 @@ Engine.ROCKSDB, new PathStateLayerLimits(4, 1L << 20))) { PathStateFlushTarget target = PathStateFlushTarget.coalesce( Collections.singletonList(delta)); CommonCheckpointPayload payload = CommonCheckpointPayload.create(formatIdentity, target, - Collections.singletonList(new BlockReverseDiff(block, Collections.emptyList(), - delta.getMutationViewDigest())), Collections.emptyList()); + Collections.singletonList(new BlockReverseDiff(block, Collections.emptyList())), + Collections.emptyList()); CommonCheckpointTarget checkpointTarget = CommonCheckpointTarget.from(payload); PathStateCheckpointMaterializer materializer = head.checkpointMaterializer(formatIdentity, baseline); @@ -1769,8 +1769,7 @@ private static CommonCheckpointPayload commonPayload(byte[] formatIdentity, PathStateFlushTarget target = PathStateFlushTarget.coalesce(deltas); List archive = new ArrayList<>(); for (PathStateSnapshotDelta delta : deltas) { - archive.add(new BlockReverseDiff(delta.getMeta(), Collections.emptyList(), - delta.getMutationViewDigest())); + archive.add(new BlockReverseDiff(delta.getMeta(), Collections.emptyList())); } return CommonCheckpointPayload.create(formatIdentity, target, archive, Collections.emptyList()); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java index 836f1fbf6a2..d4092096bac 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateSnapshotHeadTest.java @@ -106,7 +106,6 @@ public void preparedTransitionFreezesAnImmutableSnapshotForwardDelta() throws Ex assertArrayEquals(fixture.base.getStateRoot(), delta.getParentStateRoot()); assertArrayEquals(prepared.getStateRoot(), delta.getStateRoot()); assertArrayEquals(transition.getPayloadDigest(), delta.getTransitionPayloadDigest()); - assertArrayEquals(transition.getMutationViewDigest(), delta.getMutationViewDigest()); assertEquals(1, delta.getStores().size()); PathStateSnapshotDelta.StoreDelta store = delta.getStores().get(0); assertEquals("proposal", store.getDbName()); @@ -178,10 +177,10 @@ public void coalescesConsecutiveSnapshotDeltasAndRetainsEveryBlockBinding() thro java.util.List archiveBlocks = new ArrayList<>(); for (PathStateFlushTarget.BlockBinding block : target.getBlocks()) { - archiveBlocks.add(new BlockReverseDiff(block.getMeta(), Collections.singletonList( - new BlockReverseDiff.DbGroup("proposal", Collections.singletonList( - new BlockReverseDiff.Entry(new byte[]{1}, OldValue.present(new byte[]{2}))))), - block.getMutationViewDigest())); + BlockReverseDiff.DbGroup group = new BlockReverseDiff.DbGroup("proposal", + Collections.singletonList( + new BlockReverseDiff.Entry(new byte[]{1}, OldValue.present(new byte[]{2})))); + archiveBlocks.add(new BlockReverseDiff(block.getMeta(), Collections.singletonList(group))); } CommonCheckpointPayload payload = CommonCheckpointPayload.create(bytes(77), target, archiveBlocks, Arrays.asList( @@ -199,8 +198,6 @@ public void coalescesConsecutiveSnapshotDeltasAndRetainsEveryBlockBinding() thro assertEquals(3, decoded.getBlocks().size()); assertEquals(2, decoded.getChainbaseStores().size()); assertEquals(2, decoded.getPathStores().size()); - assertArrayEquals(target.getBlocks().get(1).getMutationViewDigest(), - decoded.getBlocks().get(1).getArchiveDiff().getMutationViewDigest()); assertArrayEquals(codec.digest(payload), codec.digest(decoded)); byte[] corrupt = Arrays.copyOf(encoded, encoded.length); corrupt[corrupt.length - 1] ^= 1; @@ -214,8 +211,10 @@ public void coalescesConsecutiveSnapshotDeltasAndRetainsEveryBlockBinding() thro () -> new CommonCheckpointPayloadCodec(encoded.length - 1).decode(encoded)); java.util.List mismatchedArchive = new ArrayList<>(archiveBlocks); PathStateFlushTarget.BlockBinding firstBlock = target.getBlocks().get(0); - mismatchedArchive.set(0, new BlockReverseDiff(firstBlock.getMeta(), - Collections.emptyList(), bytes(88))); + BlockSnapshotMeta wrongBlock = BlockSnapshotMeta.forBlock( + firstBlock.getMeta().getBlockNumber(), bytes(88), + firstBlock.getMeta().getParentHash(), firstBlock.getMeta().getTimestamp()); + mismatchedArchive.set(0, new BlockReverseDiff(wrongBlock, Collections.emptyList())); assertThrows(IllegalArgumentException.class, () -> CommonCheckpointPayload.create( bytes(77), target, mismatchedArchive, Collections.emptyList())); From 3f4fd4636cbe7c954261fc8301930d75b93364a4 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Tue, 8 Sep 2026 22:48:02 +0800 Subject: [PATCH 127/161] fix(chainbase): bound path-state snapshot retention Compact volatile snapshot ancestry after the rewind window is trimmed. Preserve retained rewind states without replaying blocks or marking transient nodes durable. --- .../core/db2/stateroot/PathMerkleTrie.java | 29 ++++++++++++++ .../PathStatePhysicalOverlayHead.java | 39 +++++++++++++++++++ .../core/db2/stateroot/PathStateRoot.java | 24 ++++++++++++ .../PathStateNativeNodeStoreTest.java | 38 ++++++++++++++++++ 4 files changed, 130 insertions(+) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index c6d3d339f35..a3fe81c8a7c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -1756,6 +1756,35 @@ byte[] rootHash() { return Arrays.copyOf(rootHash, rootHash.length); } + Snapshot detach() { + Map effective = new TreeMap<>(UNSIGNED_KEY_COMPARATOR); + populateLeaves(effective); + IdentityHashMap indexed = new IdentityHashMap<>(); + indexReachableMaterializedNodes(rootNode, EMPTY_PATH, indexed); + return new Snapshot(null, effective, indexed, rootNode, rootHash, leafCount); + } + + private void indexReachableMaterializedNodes(Node node, byte[] path, + IdentityHashMap indexed) { + if (node == null) { + return; + } + BytesKey materialized = materializedPath(node); + if (materialized != null) { + if (!Arrays.equals(materialized.bytes, path)) { + throw new IllegalStateException("materialized path trie node moved from its durable path"); + } + indexed.put(node, materialized); + } + visitChildren(node, path, + (child, childPath) -> indexReachableMaterializedNodes(child, childPath, indexed)); + } + + Snapshot reparent(Snapshot newParent) { + return new Snapshot(Objects.requireNonNull(newParent, "newParent"), leaves, + materializedNodes, rootNode, rootHash, leafCount); + } + int depth() { int depth = 0; Snapshot cursor = this; diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java index 99116b8a25d..bc3517f9b9c 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalOverlayHead.java @@ -277,6 +277,7 @@ public synchronized PathStateRootMetadata advance(PathStateBlockTransition trans } head = pending.metadata; snapshot = pending.snapshot; + compactRetainedSnapshotsIfNeeded(); logger.info("Path-state volatile overlay advanced: head={}, mutations={}, " + "nodeMutations={}, nativeNodeReads={}, cacheBytes={}, cacheEntries={}, " + "cacheEvictions={}, changedParticipants={}, maxParticipantMutations={}, " @@ -313,6 +314,44 @@ public synchronized PathStateRootMetadata advance(PathStateBlockTransition trans return copy(head); } + private void compactRetainedSnapshotsIfNeeded() throws IOException { + int previousDepth = snapshot.maxTrieDepth(); + if (history.isEmpty() || previousDepth <= (long) maxHistory * 2) { + return; + } + HeadState first = history.get(0); + PathStateRootMetadata candidateHead = first.metadata; + PathStateRoot.Snapshot candidateSnapshot = first.snapshot.detach(); + List candidateHistory = new ArrayList<>(history.size()); + BlockSnapshotMeta replayParent = BlockSnapshotMeta.forBlock( + candidateHead.getBlockNumber(), candidateHead.getBlockHash(), + candidateHead.getParentHash(), candidateHead.getTimestamp()); + for (int index = 0; index < history.size(); index++) { + HeadState retained = history.get(index); + PathStateRootMetadata child = index + 1 < history.size() + ? history.get(index + 1).metadata : head; + validateReplayStep(candidateHead, replayParent, retained.deltaToChild, child); + candidateHistory.add(new HeadState(candidateHead, candidateSnapshot, + retained.deltaToChild)); + PathStateRoot.Snapshot childSnapshot = index + 1 < history.size() + ? history.get(index + 1).snapshot : snapshot; + candidateSnapshot = childSnapshot.reparent(candidateSnapshot); + candidateHead = copy(child); + replayParent = retained.deltaToChild.getMeta(); + } + if (!Arrays.equals(candidateSnapshot.getStateRoot(), snapshot.getStateRoot()) + || candidateHead.getBlockNumber() != head.getBlockNumber() + || !Arrays.equals(candidateHead.getBlockHash(), head.getBlockHash())) { + throw new IOException("path-state retained snapshot compaction differs from live head"); + } + snapshot = candidateSnapshot; + history.clear(); + history.addAll(candidateHistory); + logger.info("Path-state volatile snapshot parents compacted: head={}, suffixBlocks={}, " + + "previousDepth={}, retainedDepth={}", head.getBlockNumber(), history.size(), + previousDepth, snapshot.maxTrieDepth()); + } + @Override public synchronized PathStateRootMetadata rewindTo(long blockNumber, byte[] blockHash) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java index b017f99585f..e2880c1b403 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRoot.java @@ -705,6 +705,30 @@ public byte[] getStateRoot() { return Arrays.copyOf(stateRoot, stateRoot.length); } + Snapshot detach() { + Map detached = new LinkedHashMap<>(); + for (Map.Entry entry : participants.entrySet()) { + detached.put(entry.getKey(), entry.getValue().detach()); + } + return new Snapshot(detached, superTrie.detach(), stateRoot); + } + + Snapshot reparent(Snapshot newParent) { + Snapshot parent = Objects.requireNonNull(newParent, "newParent"); + Map reparented = new LinkedHashMap<>(); + for (Map.Entry entry : participants.entrySet()) { + PathMerkleTrie.Snapshot parentTrie = parent.participants.get(entry.getKey()); + if (parentTrie == null) { + throw new IllegalArgumentException("snapshot parent participant set differs"); + } + reparented.put(entry.getKey(), entry.getValue().reparent(parentTrie)); + } + if (reparented.size() != parent.participants.size()) { + throw new IllegalArgumentException("snapshot parent participant set differs"); + } + return new Snapshot(reparented, superTrie.reparent(parent.superTrie), stateRoot); + } + byte[] participantRoot(String dbName) { PathMerkleTrie.Snapshot participant = participants.get( Objects.requireNonNull(dbName, "dbName")); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java index e5cdeb189ee..b82da8f8c65 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateNativeNodeStoreTest.java @@ -808,6 +808,44 @@ Engine.ROCKSDB, new PathStateLayerLimits(4, 1L << 20))) { } } + @Test + public void volatileOverlayBoundsSnapshotParentDepthAfterHistoryTrim() throws Exception { + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + Path root = temporaryFolder.newFolder("physical-volatile-retention").toPath(); + preparePublishedPhysicalTarget(root, scope); + + int maxHistory = 4; + try (PathStatePhysicalOverlayHead head = PathStatePhysicalOverlayHead.open(root, + Engine.ROCKSDB, new PathStateLayerLimits(maxHistory, 1L << 20))) { + byte[] parentHash = head.getHead().getBlockHash(); + for (int number = 1; number <= 20; number++) { + byte[] blockHash = bytes(140 + number); + PathStateBlockTransition transition = new PathStateBlockTransition(number, blockHash, + parentHash, number * 3L, P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("code", new byte[]{1}, new byte[]{(byte) number}))); + head.prepareSnapshotDelta( + BlockSnapshotMeta.forBlock(number, blockHash, parentHash, number * 3L), transition); + head.advance(transition); + parentHash = blockHash; + } + + PathStatePhysicalOverlayHead.RetentionCensus census = head.retentionCensus(); + assertEquals(maxHistory, census.getSuffixBlocks()); + assertTrue("snapshot parent depth must stay bounded: " + census.getMaxSnapshotDepth(), + census.getMaxSnapshotDepth() <= maxHistory * 2); + + PathStateRootMetadata rewound = head.rewindTo(18, bytes(158)); + assertEquals(18, rewound.getBlockNumber()); + PathStateBlockTransition sibling = new PathStateBlockTransition(19, bytes(180), bytes(158), + 60, P66Phase.P66_ON, Collections.singletonList( + PathStateMutation.put("proposal", new byte[]{2}, new byte[]{9}))); + head.prepareSnapshotDelta(BlockSnapshotMeta.forBlock(19, bytes(180), bytes(158), 60), + sibling); + assertEquals(19, head.advance(sibling).getBlockNumber()); + assertTrue(head.retentionCensus().getMaxSnapshotDepth() <= maxHistory * 2); + } + } + @Test public void commonOverlayReplacesInMemoryTrieAfterStartupRedo() throws Exception { PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); From 2eb2bbfc02f19d8baad287c029a2679e3788929f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 9 Sep 2026 09:41:26 +0800 Subject: [PATCH 128/161] perf(chainbase): retain archive serving writer keep the mutable State Archive serving-index writer open for the runtime lifetime so background compaction survives checkpoint publication. publish immutable read-only generations from the retained writer and verify the handle closes only with the runtime owner. --- .../PersistentServingKeyIndexGeneration.java | 132 +++++++++++++++++- .../archive/StateArchiveIndexDatabase.java | 79 ++++++++++- .../db2/archive/StateArchiveRuntimeOwner.java | 54 +++++-- ...eArchiveManagerStartupIntegrationTest.java | 9 ++ 4 files changed, 256 insertions(+), 18 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java index f921698ff28..d9543022f2a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java @@ -73,7 +73,7 @@ private PersistentServingKeyIndexGeneration(Path directory, Descriptor descripto } StateArchiveIndexDatabase.Reader opened = null; try { - opened = StateArchiveIndexDatabase.openReader(directory.resolve(DATABASE), engine); + opened = StateArchiveIndexDatabase.openImmutableReader(directory.resolve(DATABASE), engine); if (descriptor.formatVersion == EXACT_VERSION) { validateExactStoreCoverage(opened, descriptor); } @@ -248,7 +248,7 @@ synchronized PersistentServingKeyIndexGeneration extendExact(Path directory, } Files.createDirectories(directory); StateArchiveIndexEngineManifest.openOrCreate(directory, engine); - StateArchiveIndexDatabase.checkpoint(this.directory.resolve(DATABASE), + StateArchiveIndexDatabase.checkpointImmutable(this.directory.resolve(DATABASE), directory.resolve(DATABASE), engine); byte[] sourceDigest = rollSourceDigest(descriptor.sourceDigest, plan.getSourceStepDigests()); @@ -267,6 +267,18 @@ synchronized PersistentServingKeyIndexGeneration extendExact(Path directory, return open(directory, engine); } + /** + * Creates a process-owned mutable copy used to publish immutable generations without reopening + * the write database for every checkpoint target. + */ + synchronized RuntimeBuilder openRuntimeBuilder(Path builderDirectory) throws IOException { + ensureOpen(); + if (descriptor.formatVersion != EXACT_VERSION) { + throw new IllegalStateException("Serving runtime builder requires exact-only format"); + } + return RuntimeBuilder.open(builderDirectory, this); + } + @Override public synchronized OptionalLong firstChangeAfter(String dbName, byte[] rawKey, long targetBlock, long upperBound) throws IOException { @@ -668,6 +680,122 @@ private static long applyExactPlan(StateArchiveIndexDatabase.Writer target, return changes.values().stream().mapToLong(List::size).sum(); } + static final class RuntimeBuilder implements java.io.Closeable { + + private final Path directory; + private final Engine engine; + private final StateArchiveIndexDatabase.Writer writer; + private Descriptor descriptor; + private boolean closed; + + private RuntimeBuilder(Path directory, Engine engine, + StateArchiveIndexDatabase.Writer writer, Descriptor descriptor) { + this.directory = directory; + this.engine = engine; + this.writer = writer; + this.descriptor = descriptor; + } + + private static RuntimeBuilder open(Path directory, + PersistentServingKeyIndexGeneration source) throws IOException { + Objects.requireNonNull(directory, "directory"); + requireReplaceableBuilderDirectory(directory); + deleteBuilderDirectory(directory); + Files.createDirectories(directory); + StateArchiveIndexEngineManifest.openOrCreate(directory, source.engine); + StateArchiveIndexDatabase.Writer writer = null; + try { + StateArchiveIndexDatabase.checkpointImmutable(source.directory.resolve(DATABASE), + directory.resolve(DATABASE), source.engine); + writer = StateArchiveIndexDatabase.openWriter(directory.resolve(DATABASE), source.engine); + return new RuntimeBuilder(directory, source.engine, writer, source.descriptor); + } catch (IOException | RuntimeException failure) { + if (writer != null) { + try { + writer.close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + throw failure; + } + } + + synchronized PersistentServingKeyIndexGeneration extendExact(Path shadow, + String generationId, ServingIndexIncrementalPlan plan, + byte[] latestSourceIdentityDigest) throws IOException { + ensureOpen(); + validateExactIdentity(generationId, plan, latestSourceIdentityDigest); + if (plan.getIndexedFrom() != descriptor.indexedThrough + || !Arrays.equals(plan.getIndexedFromHash(), descriptor.headHash) + || !plan.getParticipatingDatabases().equals(descriptor.participants)) { + throw new IllegalArgumentException("Exact serving increment does not extend runtime I"); + } + if (Files.exists(shadow, LinkOption.NOFOLLOW_LINKS)) { + throw new IllegalArgumentException("Serving generation directory already exists"); + } + byte[] sourceDigest = rollSourceDigest(descriptor.sourceDigest, + plan.getSourceStepDigests()); + long added = applyExactPlan(writer, generationId, plan, descriptor.indexedFrom, + sourceDigest, () -> { }); + Descriptor replacement = new Descriptor(EXACT_VERSION, descriptor.scopeIdentity, + generationId, descriptor.indexedFrom, plan.getIndexedThrough(), plan.getHeadHash(), + sourceDigest, latestSourceIdentityDigest, descriptor.participants, + descriptor.keyChanges + added); + descriptor = replacement; + + Files.createDirectories(shadow); + StateArchiveIndexEngineManifest.openOrCreate(shadow, engine); + StateArchiveIndexDatabase.checkpoint(directory.resolve(DATABASE), + shadow.resolve(DATABASE), engine); + persistDescriptor(shadow, replacement); + HistorySegmentStore.syncDirectory(shadow); + return PersistentServingKeyIndexGeneration.open(shadow, engine); + } + + @Override + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + writer.close(); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Serving runtime builder is closed"); + } + } + + private static void requireReplaceableBuilderDirectory(Path directory) throws IOException { + if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + return; + } + if (Files.isSymbolicLink(directory) + || !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) { + throw new ArchivePersistenceException( + "Serving runtime builder path is not a direct directory"); + } + } + + private static void deleteBuilderDirectory(Path directory) throws IOException { + if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { + return; + } + List paths = new ArrayList<>(); + try (Stream walk = Files.walk(directory)) { + walk.sorted(java.util.Comparator.reverseOrder()).forEach(paths::add); + } + for (Path path : paths) { + if (Files.isSymbolicLink(path)) { + throw new ArchivePersistenceException( + "Serving runtime builder contains a symbolic link"); + } + Files.delete(path); + } + } + } + private static void appendExactChanges(StateArchiveIndexDatabase.Writer target, List batch, ExactKey key, List appended) throws IOException { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java index cb8c4c4ec40..5ad2df8b908 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveIndexDatabase.java @@ -39,6 +39,8 @@ final class StateArchiveIndexDatabase { static final String HOT_BLOCKS_COLUMN_FAMILY = "blocks"; private static final Map LEVEL_DATABASES = new HashMap<>(); private static final Map ROCKS_DATABASES = new HashMap<>(); + private static final Map IMMUTABLE_ROCKS_DATABASES = + new HashMap<>(); private static final Map HOT_ROCKS_DATABASES = new HashMap<>(); private static final List HOT_COLUMN_FAMILIES = createHotColumnFamilies(); private static final Set HOT_COLUMN_FAMILY_SET = @@ -59,6 +61,13 @@ static Reader openReader(Path directory, Engine engine, NativeDbConfig suppliedC : new RocksReader(acquireRocks(path, false, config)); } + static Reader openImmutableReader(Path directory, Engine engine) throws IOException { + Path path = normalize(directory); + NativeDbConfig config = configuredOptions(); + return engine == Engine.LEVELDB ? new LevelReader(acquireLevel(path, false, config)) + : new RocksReader(acquireImmutableRocks(path, config)); + } + static Writer openWriter(Path directory, Engine engine) throws IOException { return openWriter(directory, engine, configuredOptions()); } @@ -120,6 +129,16 @@ static void checkpoint(Path source, Path target, Engine engine) throws IOExcepti checkpointLevel(from, to); } + static void checkpointImmutable(Path source, Path target, Engine engine) throws IOException { + Path from = normalize(source); + Path to = normalize(target); + if (engine == Engine.ROCKSDB) { + checkpointFrozenRocks(from, to); + return; + } + checkpointLevel(from, to); + } + static synchronized int openReferenceCount(Path directory, Engine engine) { Path path = normalize(directory); if (engine == Engine.LEVELDB) { @@ -244,7 +263,7 @@ private static synchronized SharedRocksDatabase acquireRocks(Path directory, boo RocksResources resources = new RocksResources(config, create); try { shared = new SharedRocksDatabase(directory, - org.rocksdb.RocksDB.open(resources.options, directory.toString()), resources); + org.rocksdb.RocksDB.open(resources.options, directory.toString()), resources, false); } catch (org.rocksdb.RocksDBException | RuntimeException failure) { resources.close(); throw new IOException("Failed to open RocksDB Archive serving index", failure); @@ -259,6 +278,28 @@ private static synchronized SharedRocksDatabase acquireRocks(Path directory, boo return shared; } + private static synchronized SharedRocksDatabase acquireImmutableRocks(Path directory, + NativeDbConfig config) throws IOException { + SharedRocksDatabase shared = IMMUTABLE_ROCKS_DATABASES.get(directory); + if (shared == null) { + RocksResources resources = new RocksResources(config, false); + try { + shared = new SharedRocksDatabase(directory, + org.rocksdb.RocksDB.openReadOnly(resources.options, directory.toString()), resources, + true); + } catch (org.rocksdb.RocksDBException | RuntimeException failure) { + resources.close(); + throw new IOException("Failed to open immutable RocksDB Archive serving index", failure); + } + IMMUTABLE_ROCKS_DATABASES.put(directory, shared); + logger.info("Immutable Archive native database opened: directory={}, engine=ROCKSDB, " + + "blockBytes={}, cacheBytes={}, maxOpenFiles={}", directory, + config.getBlockSize(), config.getCacheSize(), config.getMaxOpenFiles()); + } + shared.references++; + return shared; + } + private static synchronized SharedHotRocksDatabase acquireHotRocks(Path directory, boolean create, NativeDbConfig config) throws IOException { SharedHotRocksDatabase shared = HOT_ROCKS_DATABASES.get(directory); @@ -285,7 +326,11 @@ private static synchronized void releaseRocks(SharedRocksDatabase shared) { if (--shared.references != 0) { return; } - ROCKS_DATABASES.remove(shared.directory); + if (shared.immutable) { + IMMUTABLE_ROCKS_DATABASES.remove(shared.directory); + } else { + ROCKS_DATABASES.remove(shared.directory); + } shared.database.close(); shared.resources.close(); } @@ -309,6 +354,32 @@ private static void checkpointRocks(Path source, Path target) throws IOException } } + private static void checkpointFrozenRocks(Path source, Path target) throws IOException { + Files.createDirectory(target); + try (java.util.stream.Stream entries = Files.list(source)) { + for (Path entry : (Iterable) entries::iterator) { + if (!Files.isRegularFile(entry, LinkOption.NOFOLLOW_LINKS)) { + continue; + } + String name = entry.getFileName().toString(); + if ("LOCK".equals(name) || "LOG".equals(name) || "LOG.old".equals(name)) { + continue; + } + Path destination = target.resolve(name); + if (name.endsWith(".sst")) { + Files.createLink(destination, entry); + } else { + Files.copy(entry, destination, StandardCopyOption.COPY_ATTRIBUTES); + try (java.nio.channels.FileChannel channel = java.nio.channels.FileChannel.open( + destination, java.nio.file.StandardOpenOption.WRITE)) { + channel.force(true); + } + } + } + } + HistorySegmentStore.syncDirectory(target); + } + private static NativeDbConfig configuredOptions() { org.tron.core.config.args.Storage storage = CommonParameter.getInstance().getStorage(); NativeDbConfig config = storage == null ? null @@ -408,13 +479,15 @@ private static final class SharedRocksDatabase { private final Path directory; private final org.rocksdb.RocksDB database; private final RocksResources resources; + private final boolean immutable; private int references; private SharedRocksDatabase(Path directory, org.rocksdb.RocksDB database, - RocksResources resources) { + RocksResources resources, boolean immutable) { this.directory = directory; this.database = database; this.resources = resources; + this.immutable = immutable; } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java index 3931f0f9ffe..996c33be333 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveRuntimeOwner.java @@ -23,6 +23,8 @@ /** Sole owner for exact-27 State Archive resources from recovered startup through shutdown. */ public final class StateArchiveRuntimeOwner implements Closeable { + static final String SERVING_INDEX_RUNTIME_DIRECTORY = ".serving-index-runtime"; + public enum ServingIndexStage { BEFORE_BUILD, GENERATION_INSTALLED, @@ -68,6 +70,8 @@ public enum State { private Closeable latestCoordinator; private Closeable servingCatalog; private PersistentServingKeyIndexCatalog servingIndexCatalog; + // Process-owned writer: checkpoint targets borrow it and only owner shutdown closes it. + private PersistentServingKeyIndexGeneration.RuntimeBuilder servingIndexBuilder; private LatestStateGenerationCoordinator latestStateCoordinator; private BlockSnapshotMeta latestAuthorityHead; private volatile BlockSnapshotMeta readableHead; @@ -90,6 +94,7 @@ public StateArchiveRuntimeOwner(SnapshotManager snapshotManager, this.latestCoordinator = Objects.requireNonNull(latestCoordinator, "latestCoordinator"); this.servingCatalog = Objects.requireNonNull(servingCatalog, "servingCatalog"); this.servingIndexCatalog = null; + this.servingIndexBuilder = null; this.latestStateCoordinator = null; this.latestAuthorityHead = null; this.readableHead = null; @@ -119,6 +124,7 @@ private StateArchiveRuntimeOwner(SnapshotManager snapshotManager, this.latestCoordinator = null; this.servingCatalog = null; this.servingIndexCatalog = null; + this.servingIndexBuilder = null; this.latestStateCoordinator = null; this.latestAuthorityHead = null; this.lastServingApply = null; @@ -259,6 +265,7 @@ public synchronized ArchiveHistoryWriter attachNormalWriter(OldValueCollector co ArchiveHistoryWriter writer = null; AsyncArchiveHistorySink asyncSink = null; PersistentServingKeyIndexCatalog catalog = null; + PersistentServingKeyIndexGeneration.RuntimeBuilder builder = null; LatestStateGenerationCoordinator latest = null; ArchiveRuntimeAttachment candidate = null; boolean attached = false; @@ -274,19 +281,25 @@ public synchronized ArchiveHistoryWriter attachNormalWriter(OldValueCollector co storeNames()); catalog = openOrCreateServingCatalog(writer); validateServingIndex(writer, catalog, canonicalHead); + try (PersistentServingKeyIndexGeneration current = catalog.pin()) { + builder = current.openRuntimeBuilder( + archiveDirectory.resolve(SERVING_INDEX_RUNTIME_DIRECTORY)); + } latest = LatestStateGenerationCoordinatorFactory.create(snapshotManager, supplementalStores, this::readLatestAuthority); - restoreLatestState(writer, catalog, latest, canonicalHead); + restoreLatestState(writer, catalog, builder, latest, canonicalHead); if (lastServingApply == null) { lastServingApply = ServingIndexApplyStatistics.zeroAction(canonicalHead); } asyncSink = new AsyncArchiveHistorySink(writer, queueCapacity); ArchiveHistoryWriter attachedWriter = writer; PersistentServingKeyIndexCatalog attachedCatalog = catalog; + PersistentServingKeyIndexGeneration.RuntimeBuilder attachedBuilder = builder; LatestStateGenerationCoordinator attachedLatest = latest; candidate = new ArchiveRuntimeAttachment(collector, asyncSink, target -> publishServingIndex(attachedWriter, attachedCatalog, target), - target -> publishReadableState(attachedWriter, attachedCatalog, attachedLatest, target)); + target -> publishReadableState(attachedWriter, attachedCatalog, attachedBuilder, + attachedLatest, target)); snapshotManager.attachArchiveRuntime(candidate); attached = true; snapshotManager.markArchiveReadableThrough(canonicalHead.getEpoch()); @@ -295,6 +308,7 @@ public synchronized ArchiveHistoryWriter attachNormalWriter(OldValueCollector co sink = asyncSink; historyWriter = writer; servingIndexCatalog = catalog; + servingIndexBuilder = builder; latestStateCoordinator = latest; latestCoordinator = latest; servingCatalog = catalog; @@ -320,6 +334,13 @@ public synchronized ArchiveHistoryWriter attachNormalWriter(OldValueCollector co failure.addSuppressed(closeFailure); } } + if (builder != null) { + try { + builder.close(); + } catch (IOException | RuntimeException closeFailure) { + failure.addSuppressed(closeFailure); + } + } if (catalog != null) { try { catalog.close(); @@ -402,8 +423,9 @@ private ArchiveProgressEnvelope readLatestAuthority() { } private synchronized void restoreLatestState(ArchiveHistoryWriter writer, - PersistentServingKeyIndexCatalog catalog, LatestStateGenerationCoordinator latest, - BlockSnapshotMeta target) throws IOException { + PersistentServingKeyIndexCatalog catalog, + PersistentServingKeyIndexGeneration.RuntimeBuilder builder, + LatestStateGenerationCoordinator latest, BlockSnapshotMeta target) throws IOException { try (PersistentServingKeyIndexGeneration serving = catalog.pin()) { validateServingGeneration(writer, serving, target); if (serving.isLatestSourceIdentityBound()) { @@ -411,18 +433,19 @@ private synchronized void restoreLatestState(ArchiveHistoryWriter writer, return; } } - bindAndPublishLatest(writer, catalog, latest, target); + bindAndPublishLatest(writer, catalog, builder, latest, target); } private synchronized void publishReadableState(ArchiveHistoryWriter writer, - PersistentServingKeyIndexCatalog catalog, LatestStateGenerationCoordinator latest, - BlockSnapshotMeta target) throws IOException { + PersistentServingKeyIndexCatalog catalog, + PersistentServingKeyIndexGeneration.RuntimeBuilder builder, + LatestStateGenerationCoordinator latest, BlockSnapshotMeta target) throws IOException { if (!target.equals(writer.committedHeadMeta())) { throw new ArchivePersistenceException( "Readable-state target differs from committed history head"); } readableStateFaultHook.afterStage(ReadableStateStage.CANONICAL_REFRESHED); - bindAndPublishLatest(writer, catalog, latest, target); + bindAndPublishLatest(writer, catalog, builder, latest, target); readableStateFaultHook.afterStage(ReadableStateStage.LATEST_PUBLISHED); long previousReadable = snapshotManager.getArchiveReadableEpoch(); BlockSnapshotMeta previousReadableHead = readableHead; @@ -452,8 +475,9 @@ private void publishExistingLatest(LatestStateGenerationCoordinator latest, } private void bindAndPublishLatest(ArchiveHistoryWriter writer, - PersistentServingKeyIndexCatalog catalog, LatestStateGenerationCoordinator latest, - BlockSnapshotMeta target) throws IOException { + PersistentServingKeyIndexCatalog catalog, + PersistentServingKeyIndexGeneration.RuntimeBuilder builder, + LatestStateGenerationCoordinator latest, BlockSnapshotMeta target) throws IOException { String expectedLatest = latest.getCurrentGenerationId(); long started = System.nanoTime(); latestAuthorityHead = target; @@ -478,8 +502,8 @@ private void bindAndPublishLatest(ArchiveHistoryWriter writer, } try (LatestStateGenerationCoordinator.Candidate candidate = latest.acquire(generationId(target))) { - publishServingAndLatest(catalog, latest, current, plan, candidate, expectedLatest, - target); + publishServingAndLatest(catalog, builder, latest, current, plan, candidate, + expectedLatest, target); lastServingApply = ServingIndexApplyStatistics.from(plan, true, System.nanoTime() - started); } @@ -489,12 +513,13 @@ private void bindAndPublishLatest(ArchiveHistoryWriter writer, } private void publishServingAndLatest(PersistentServingKeyIndexCatalog catalog, + PersistentServingKeyIndexGeneration.RuntimeBuilder builder, LatestStateGenerationCoordinator latest, PersistentServingKeyIndexGeneration current, ServingIndexIncrementalPlan plan, LatestStateGenerationCoordinator.Candidate candidate, String expectedLatest, BlockSnapshotMeta target) throws IOException { String generationId = candidate.getGenerationId(); Path shadow = archiveDirectory.resolve(".serving-index-build-" + UUID.randomUUID()); - try (PersistentServingKeyIndexGeneration built = current.extendExact(shadow, + try (PersistentServingKeyIndexGeneration built = builder.extendExact(shadow, generationId, plan, candidate.getSourceIdentityDigest())) { validateIncrementCandidate(current, plan, built, target); } @@ -908,6 +933,9 @@ public synchronized void close() throws IOException { if (latestCoordinator != null) { failure = closeOwned("latest coordinator", latestCoordinator, failure); } + if (servingIndexBuilder != null) { + failure = closeOwned("serving index runtime builder", servingIndexBuilder, failure); + } if (servingCatalog != null) { failure = closeOwned("serving catalog", servingCatalog, failure); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java index cfc80d6ec40..396ec5c3d5d 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveManagerStartupIntegrationTest.java @@ -926,8 +926,13 @@ public void managerRunsTwoNormalFlushTargetsThroughExact27FixedPoint() throws Ex Manager manager = manager(snapshots, head); withArchiveConfig(output, engine, true, () -> invoke(manager, "initStateArchive")); + Path runtimeBuilder = archive.resolve( + StateArchiveRuntimeOwner.SERVING_INDEX_RUNTIME_DIRECTORY).resolve("keys"); + Engine selectedEngine = Engine.valueOf(engine); assertEquals(State.RUNNING, manager.getStateArchiveRuntime().getState()); + assertEquals(1, + StateArchiveIndexDatabase.openReferenceCount(runtimeBuilder, selectedEngine)); assertEquals(0, manager.getStateArchiveRuntime().getStartupRecoveryActionCount()); assertEquals(head.getMeta(), manager.getStateArchiveRuntime().getRecoveredHead()); assertNotNull(manager.getArchiveHistoryWriter()); @@ -971,10 +976,14 @@ public void managerRunsTwoNormalFlushTargetsThroughExact27FixedPoint() throws Ex .getTotalSstBytes().isAvailable()); assertEquals("ROCKSDB".equals(engine), inspection.getGeneration().getEngine() .getPendingCompactionBytes().isAvailable()); + assertEquals(1, + StateArchiveIndexDatabase.openReferenceCount(runtimeBuilder, selectedEngine)); setField(snapshots, "size", 0); } invoke(manager, "closeStateArchive"); + assertEquals(0, + StateArchiveIndexDatabase.openReferenceCount(runtimeBuilder, selectedEngine)); assertEquals(-1, snapshots.getArchiveReadableEpoch()); assertNull(manager.getStateArchiveRuntime()); assertNull(manager.getArchiveHistoryWriter()); From 89ef2d22b5af8e9360b4fcad5680f46e8543a38b Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 9 Sep 2026 09:53:50 +0800 Subject: [PATCH 129/161] style(chainbase): clarify path-state prefixes rename private F/N/M keyspace constants to describe flat state, trie nodes, and metadata while preserving their persisted byte values. --- .../stateroot/PathStatePhysicalStoreSet.java | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java index 8e6ce9242e6..d77ba43dd05 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStatePhysicalStoreSet.java @@ -66,9 +66,10 @@ public final class PathStatePhysicalStoreSet implements Closeable { private static final String REVERSE_DIRECTORY = "reverse"; static final String INTENT_FILE = "INTENT"; static final String CURRENT_FILE = "CURRENT"; - private static final byte FLAT_PREFIX = 'F'; - private static final byte NODE_PREFIX = 'N'; - private static final byte META_PREFIX = 'M'; + // Stable persisted keyspace tags. The descriptive names carry semantics; byte values keep ABI. + private static final byte FLAT_STATE_PREFIX = 'F'; + private static final byte TRIE_NODE_PREFIX = 'N'; + private static final byte METADATA_PREFIX = 'M'; private static final byte[] FLAT_ROOT_METADATA = new byte[]{'f', 'l', 'a', 't', '-', 'r', 'o', 'o', 't'}; private static final byte[] FLAT_COMPLETE_METADATA = new byte[]{'f', 'l', 'a', 't', '-', 'c', @@ -301,7 +302,7 @@ void ingestFlat(String dbName, PathStateRebuildCoordinator.SnapshotSource source byte[] key = Arrays.copyOf(physicalKey, physicalKey.length); byte[] secureKey = PathStateCommitmentCodec.storeLeafKey(participant.getStoreId(), key); byte[] encodedLeaf = PathStateCommitmentCodec.presentLeafValue(physicalValue); - byte[] storedKey = prefixed(FLAT_PREFIX, secureKey, "secureKey"); + byte[] storedKey = prefixed(FLAT_STATE_PREFIX, secureKey, "secureKey"); long mutationBytes = storedKey.length + encodedLeaf.length; if (!pending.isEmpty() && pendingBytes[0] + mutationBytes > BOOTSTRAP_WRITE_BATCH_BYTES) { @@ -1655,7 +1656,8 @@ private static PathStateParticipantScope requireExactScope(PathStateParticipantS private static byte[] unprefixedFlatKey(byte[] storedKey) { byte[] key = Arrays.copyOf(Objects.requireNonNull(storedKey, "storedKey"), storedKey.length); - if (key.length != PathStateCommitmentCodec.ROOT_LENGTH + 1 || key[0] != FLAT_PREFIX) { + if (key.length != PathStateCommitmentCodec.ROOT_LENGTH + 1 + || key[0] != FLAT_STATE_PREFIX) { throw new IllegalStateException("path-state physical F key is corrupt"); } return Arrays.copyOfRange(key, 1, key.length); @@ -1690,7 +1692,7 @@ private PhysicalStore(Path directory, Engine engine, int storeId, } public void putFlat(byte[] secureKey, byte[] encodedLeaf) { - nativeStore.put(prefixed(FLAT_PREFIX, secureKey, "secureKey"), encodedLeaf); + nativeStore.put(prefixed(FLAT_STATE_PREFIX, secureKey, "secureKey"), encodedLeaf); } private void writeBatch(List mutations) { @@ -1698,15 +1700,16 @@ private void writeBatch(List mutations) } public byte[] getFlat(byte[] secureKey) { - return nativeStore.get(prefixed(FLAT_PREFIX, secureKey, "secureKey")); + return nativeStore.get(prefixed(FLAT_STATE_PREFIX, secureKey, "secureKey")); } public void deleteFlat(byte[] secureKey) { - nativeStore.delete(prefixed(FLAT_PREFIX, secureKey, "secureKey")); + nativeStore.delete(prefixed(FLAT_STATE_PREFIX, secureKey, "secureKey")); } void scanFlat(PathStateNativeNodeStore.EntryConsumer consumer) throws IOException { - nativeStore.scanPrefix(new byte[]{FLAT_PREFIX}, Objects.requireNonNull(consumer, "consumer")); + nativeStore.scanPrefix(new byte[]{FLAT_STATE_PREFIX}, + Objects.requireNonNull(consumer, "consumer")); } public PathNodeStore nodeStore() { @@ -1744,7 +1747,7 @@ long getUnsyncedWriteBatchCalls() { void clearNodes() throws IOException { nodeStore.clear(); List pending = new ArrayList<>(4096); - nativeStore.scanPrefix(new byte[]{NODE_PREFIX}, entry -> { + nativeStore.scanPrefix(new byte[]{TRIE_NODE_PREFIX}, entry -> { pending.add(PathStateNativeNodeStore.BatchMutation.delete(entry.getKey())); if (pending.size() == 4096) { nativeStore.writeBatch(new ArrayList<>(pending)); @@ -1760,7 +1763,7 @@ void applyParticipantDelete(byte[] secureKey, List nodeMutations, byte[] flatDigest, byte[] generation, byte[] storeRoot) { List mutations = new ArrayList<>(); mutations.add(PathStateNativeNodeStore.BatchMutation.delete( - prefixed(FLAT_PREFIX, secureKey, "secureKey"))); + prefixed(FLAT_STATE_PREFIX, secureKey, "secureKey"))); appendNodeMutations(mutations, nodeMutations); mutations.add(metadataMutation(FLAT_DIGEST_METADATA, flatDigest)); mutations.add(metadataMutation(STORE_GENERATION_METADATA, generation)); @@ -1774,7 +1777,7 @@ void applyParticipantTransition(List flatMutations, byte[] storeRoot) { List mutations = new ArrayList<>(); for (FlatMutation mutation : Objects.requireNonNull(flatMutations, "flatMutations")) { - byte[] key = prefixed(FLAT_PREFIX, mutation.secureKey, "secureKey"); + byte[] key = prefixed(FLAT_STATE_PREFIX, mutation.secureKey, "secureKey"); mutations.add(mutation.encodedValue == null ? PathStateNativeNodeStore.BatchMutation.delete(key) : PathStateNativeNodeStore.BatchMutation.put(key, mutation.encodedValue)); @@ -1801,7 +1804,7 @@ void applyCheckpointParticipant(CommonCheckpointPayload.PathStoreTarget target, byte[] marker) { List mutations = new ArrayList<>(); for (CommonCheckpointPayload.Mutation mutation : target.getFlatMutations()) { - byte[] key = prefixed(FLAT_PREFIX, mutation.getKey(), "secureKey"); + byte[] key = prefixed(FLAT_STATE_PREFIX, mutation.getKey(), "secureKey"); mutations.add(mutation.isDelete() ? PathStateNativeNodeStore.BatchMutation.delete(key) : PathStateNativeNodeStore.BatchMutation.put(key, mutation.getValue())); @@ -1811,8 +1814,10 @@ void applyCheckpointParticipant(CommonCheckpointPayload.PathStoreTarget target, byte[] path = mutation.getKey(); byte[] value = mutation.getValue(); mutations.add(mutation.isDelete() - ? PathStateNativeNodeStore.BatchMutation.delete(prefixed(NODE_PREFIX, path, "path")) - : PathStateNativeNodeStore.BatchMutation.put(prefixed(NODE_PREFIX, path, "path"), + ? PathStateNativeNodeStore.BatchMutation.delete( + prefixed(TRIE_NODE_PREFIX, path, "path")) + : PathStateNativeNodeStore.BatchMutation.put( + prefixed(TRIE_NODE_PREFIX, path, "path"), value)); cacheMutations.add(new NodeMutation(path, value)); } @@ -1828,8 +1833,10 @@ void applyCheckpointSuper(List supplied, byte[ byte[] path = mutation.getKey(); byte[] value = mutation.getValue(); mutations.add(mutation.isDelete() - ? PathStateNativeNodeStore.BatchMutation.delete(prefixed(NODE_PREFIX, path, "path")) - : PathStateNativeNodeStore.BatchMutation.put(prefixed(NODE_PREFIX, path, "path"), + ? PathStateNativeNodeStore.BatchMutation.delete( + prefixed(TRIE_NODE_PREFIX, path, "path")) + : PathStateNativeNodeStore.BatchMutation.put( + prefixed(TRIE_NODE_PREFIX, path, "path"), value)); cacheMutations.add(new NodeMutation(path, value)); } @@ -1846,7 +1853,7 @@ private static void appendNodeMutations( List target, List nodeMutations) { for (NodeMutation mutation : Objects.requireNonNull(nodeMutations, "nodeMutations")) { - byte[] key = prefixed(NODE_PREFIX, mutation.path, "path"); + byte[] key = prefixed(TRIE_NODE_PREFIX, mutation.path, "path"); target.add(mutation.encodedNode == null ? PathStateNativeNodeStore.BatchMutation.delete(key) : PathStateNativeNodeStore.BatchMutation.put(key, mutation.encodedNode)); @@ -1855,20 +1862,20 @@ private static void appendNodeMutations( private static PathStateNativeNodeStore.BatchMutation metadataMutation(byte[] name, byte[] value) { - return PathStateNativeNodeStore.BatchMutation.put(prefixed(META_PREFIX, name, + return PathStateNativeNodeStore.BatchMutation.put(prefixed(METADATA_PREFIX, name, "metadata name"), value); } public void putMetadata(byte[] name, byte[] value) { - nativeStore.put(prefixed(META_PREFIX, name, "metadata name"), value); + nativeStore.put(prefixed(METADATA_PREFIX, name, "metadata name"), value); } public byte[] getMetadata(byte[] name) { - return nativeStore.get(prefixed(META_PREFIX, name, "metadata name")); + return nativeStore.get(prefixed(METADATA_PREFIX, name, "metadata name")); } void deleteMetadata(byte[] name) { - nativeStore.delete(prefixed(META_PREFIX, name, "metadata name")); + nativeStore.delete(prefixed(METADATA_PREFIX, name, "metadata name")); } public Path getDirectory() { @@ -1892,17 +1899,17 @@ private PhysicalNodeStore(PathStateNativeNodeStore nativeStore) { @Override public byte[] get(byte[] path) { - return nativeStore.get(prefixed(NODE_PREFIX, path, "path")); + return nativeStore.get(prefixed(TRIE_NODE_PREFIX, path, "path")); } @Override public void put(byte[] path, byte[] encodedNode) { - nativeStore.put(prefixed(NODE_PREFIX, path, "path"), encodedNode); + nativeStore.put(prefixed(TRIE_NODE_PREFIX, path, "path"), encodedNode); } @Override public void delete(byte[] path) { - nativeStore.delete(prefixed(NODE_PREFIX, path, "path")); + nativeStore.delete(prefixed(TRIE_NODE_PREFIX, path, "path")); } } @@ -2120,12 +2127,12 @@ private PhysicalNodeBatchWriter(PathStateNativeNodeStore nativeStore) { @Override public byte[] get(byte[] path) { flush(); - return nativeStore.get(prefixed(NODE_PREFIX, path, "path")); + return nativeStore.get(prefixed(TRIE_NODE_PREFIX, path, "path")); } @Override public void put(byte[] path, byte[] encodedNode) { - byte[] key = prefixed(NODE_PREFIX, path, "path"); + byte[] key = prefixed(TRIE_NODE_PREFIX, path, "path"); byte[] value = Arrays.copyOf(Objects.requireNonNull(encodedNode, "encodedNode"), encodedNode.length); flushBeforeOversizedMutation(key.length + value.length); @@ -2136,7 +2143,7 @@ public void put(byte[] path, byte[] encodedNode) { @Override public void delete(byte[] path) { - byte[] key = prefixed(NODE_PREFIX, path, "path"); + byte[] key = prefixed(TRIE_NODE_PREFIX, path, "path"); flushBeforeOversizedMutation(key.length); pending.add(PathStateNativeNodeStore.BatchMutation.delete(key)); pendingBytes = Math.addExact(pendingBytes, key.length); From 72bafa0635c48ce26ec263fa31eae9990cbd3cff Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Wed, 9 Sep 2026 10:47:31 +0800 Subject: [PATCH 130/161] fix(chainbase): prevent path-state worker stalls Avoid rebuilding a full materialized-node index when compacting retained trie snapshots. Fail deferred capture on worker errors and bound producer waits so a dead worker cannot stall block synchronization. --- .../core/db2/stateroot/PathMerkleTrie.java | 26 ++----- .../stateroot/PathStateRuntimeAttachment.java | 40 +++++++++-- .../SnapshotOldValueCollectorTest.java | 71 +++++++++++++++++++ .../db2/stateroot/PathMerkleTrieTest.java | 32 +++++++++ 4 files changed, 145 insertions(+), 24 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java index a3fe81c8a7c..b4d14323aa9 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathMerkleTrie.java @@ -1759,25 +1759,9 @@ byte[] rootHash() { Snapshot detach() { Map effective = new TreeMap<>(UNSIGNED_KEY_COMPARATOR); populateLeaves(effective); - IdentityHashMap indexed = new IdentityHashMap<>(); - indexReachableMaterializedNodes(rootNode, EMPTY_PATH, indexed); - return new Snapshot(null, effective, indexed, rootNode, rootHash, leafCount); - } - - private void indexReachableMaterializedNodes(Node node, byte[] path, - IdentityHashMap indexed) { - if (node == null) { - return; - } - BytesKey materialized = materializedPath(node); - if (materialized != null) { - if (!Arrays.equals(materialized.bytes, path)) { - throw new IllegalStateException("materialized path trie node moved from its durable path"); - } - indexed.put(node, materialized); - } - visitChildren(node, path, - (child, childPath) -> indexReachableMaterializedNodes(child, childPath, indexed)); + // Materialized nodes bind their durable path on creation. Re-indexing the reachable graph + // duplicates the complete resolved trie and can exhaust the heap while compacting parents. + return new Snapshot(null, effective, new IdentityHashMap<>(), rootNode, rootHash, leafCount); } Snapshot reparent(Snapshot newParent) { @@ -1794,6 +1778,10 @@ int depth() { } return depth; } + + int materializedIndexSize() { + return materializedNodes.size(); + } } static final class LeafEntry { diff --git a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java index 264ad642e9c..2dca4aeba89 100644 --- a/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java +++ b/chainbase/src/main/java/org/tron/core/db2/stateroot/PathStateRuntimeAttachment.java @@ -191,10 +191,22 @@ private void publishDeferred(PathStateBlockTransition transition) { } long startedNanos = System.nanoTime(); try { - deferredQueue.put(admitted); - logger.info("Path-state deferred view enqueued: head={}, queueDepth={}, enqueueMicros={}", - admitted.getMeta().getBlockNumber(), deferredQueue.size(), - TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startedNanos)); + while (true) { + if (deferredWorkerUnavailable()) { + return; + } + if (!deferredQueue.offer(admitted, 100L, TimeUnit.MILLISECONDS)) { + continue; + } + if (deferredWorkerUnavailable()) { + deferredQueue.remove(admitted); + return; + } + logger.info("Path-state deferred view enqueued: head={}, queueDepth={}, enqueueMicros={}", + admitted.getMeta().getBlockNumber(), deferredQueue.size(), + TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startedNanos)); + return; + } } catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); fail(FailureStage.PUBLISH, interrupted); @@ -227,13 +239,31 @@ private void runDeferred() { fail(FailureStage.CAPTURE, interrupted); } return; - } catch (IOException | RuntimeException currentFailure) { + } catch (Throwable currentFailure) { fail(FailureStage.CAPTURE, currentFailure); + deferredQueue.clear(); return; } } } + private synchronized boolean deferredWorkerUnavailable() { + if (failure != null) { + return true; + } + if (closed) { + fail(FailureStage.PUBLISH, + new IOException("deferred PathState runtime closed while publishing")); + return true; + } + if (!deferredWorker.isAlive()) { + fail(FailureStage.CAPTURE, + new IllegalStateException("deferred PathState worker terminated unexpectedly")); + return true; + } + return false; + } + /** Drains the deferred benchmark worker before its Manager-owned head is closed. */ public void close() throws IOException { if (deferredWorker == null) { diff --git a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java index a3f2459a851..f112bbbc37a 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/SnapshotOldValueCollectorTest.java @@ -36,6 +36,9 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -122,6 +125,74 @@ public void deferredPathStateCaptureDoesNotWaitForCollectorAndDrainsOnClose() manager.shutdown(); } + @Test + public void deferredPathStateWorkerErrorFailsRuntimeWithoutLeavingAProducerBlocked() + throws Exception { + SnapshotManager manager = new SnapshotManager(""); + Chainbase code = new Chainbase(new SnapshotRoot(new MemoryDb("code"))); + manager.add(code); + manager.enable(); + CountDownLatch attempted = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AssertionError workerFailure = new AssertionError("injected deferred worker error"); + PathStateRuntimeAttachment attachment = PathStateRuntimeAttachment.deferred(view -> { + attempted.countDown(); + try { + if (!release.await(5, TimeUnit.SECONDS)) { + throw new AssertionError("timed out waiting to fail deferred worker"); + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new AssertionError("deferred worker interrupted", interrupted); + } + throw workerFailure; + }, transition -> { }, (blockNumber, blockHash) -> { }, null, + (meta, transition) -> null); + attachment.synchronizeReadyHead(PathStateRootMetadata.base(0, hash(0), hash(9), 0, + P66Phase.P66_ON, hash(7), hash(8), hash(6))); + + BlockChangeView first = captureView(manager, code, + BlockSnapshotMeta.forBlock(1, hash(1), hash(0), 1L)); + attachment.capture(first); + attachment.publish(null); + assertTrue(attempted.await(5, TimeUnit.SECONDS)); + + for (int number = 2; number <= 65; number++) { + BlockChangeView queued = captureView(manager, code, + BlockSnapshotMeta.forBlock(number, hash(number), hash(number - 1), number)); + attachment.capture(queued); + attachment.publish(null); + } + BlockChangeView blocked = captureView(manager, code, + BlockSnapshotMeta.forBlock(66, hash(66), hash(65), 66L)); + attachment.capture(blocked); + ExecutorService publisher = Executors.newSingleThreadExecutor(); + try { + Future blockedPublish = publisher.submit(() -> attachment.publish(null)); + release.countDown(); + blockedPublish.get(5, TimeUnit.SECONDS); + } finally { + release.countDown(); + publisher.shutdownNow(); + } + + assertSame(workerFailure, attachment.getFailure()); + IOException closeFailure = assertThrows(IOException.class, attachment::close); + assertSame(workerFailure, closeFailure.getCause()); + assertEquals(PathStateRuntimeAttachment.State.FAILED, attachment.status().getState()); + assertEquals(PathStateRuntimeAttachment.FailureStage.CAPTURE, + attachment.status().getFailureStage()); + assertEquals(PathStateRuntimeAttachment.FailureKind.RUNTIME, + attachment.status().getFailureKind()); + + BlockChangeView second = captureView(manager, code, + BlockSnapshotMeta.forBlock(67, hash(67), hash(66), 67L)); + attachment.capture(second); + attachment.publish(null); + assertSame(workerFailure, attachment.getFailure()); + manager.shutdown(); + } + @Test public void pathStateRuntimeCoexistsWithArchiveAtBlockFinalBoundary() throws Exception { MemoryDb propertiesDb = new MemoryDb("properties"); diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java index 57cd57b5089..2bbe4c0da82 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathMerkleTrieTest.java @@ -447,6 +447,38 @@ public void resolvedSubtreeSurvivesConsecutiveSnapshotSwitches() { assertArrayEquals(referenceRoot(keys, values), secondBlock.rootHash()); } + @Test + public void detachedSnapshotDoesNotDuplicateTheResolvedNodeIndex() { + int leafCount = 512; + byte[][] keys = new byte[leafCount][]; + byte[][] values = new byte[leafCount][]; + InMemoryPathNodeStore store = new InMemoryPathNodeStore(); + PathMerkleTrie source = new PathMerkleTrie(store); + for (int index = 0; index < leafCount; index++) { + keys[index] = Hash.sha3(value("detached-key-" + index)); + values[index] = value("detached-value-" + index); + source.put(keys[index], values[index]); + } + byte[] root = source.rootHash(); + + PathMerkleTrie restored = new PathMerkleTrie(store); + restored.restoreRoot(root); + for (int index = 0; index < leafCount; index++) { + assertArrayEquals(values[index], restored.get(keys[index])); + } + PathMerkleTrie.Snapshot resolved = restored.snapshot(); + assertTrue(resolved.materializedIndexSize() > leafCount); + + PathMerkleTrie.Snapshot detached = resolved.detach(); + assertEquals(1, detached.depth()); + assertEquals(0, detached.materializedIndexSize()); + + PathMerkleTrie next = PathMerkleTrie.fromSnapshot(store, detached); + values[17] = value("detached-updated-value"); + next.put(keys[17], values[17]); + assertArrayEquals(referenceRoot(keys, values), next.rootHash()); + } + private static byte[] referenceRoot(byte[][] keys, byte[][] values) { TrieImpl reference = new TrieImpl(); reference.setAsync(false); From b95b2575e25fe2c4ef253fa02ea85604d4a01d6a Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 14:17:37 +0800 Subject: [PATCH 131/161] feat(chainbase): add append-file state archive --- ...ArchiveAppendCheckpointMaterializerV3.java | 288 +++ .../StateArchiveBlockFrameCodecV3.java | 796 +++++++ .../StateArchiveCheckpointMaterializer.java | 13 + .../StateArchiveCheckpointPlanner.java | 16 + .../db2/archive/StateArchiveFileFormatV3.java | 464 ++++ .../StateArchiveFiveLaneBlockCodecV3.java | 1128 +++++++++ ...StateArchiveFiveLaneDurabilityProofV3.java | 250 ++ .../StateArchiveFiveLaneRecoveryIntentV3.java | 571 +++++ .../StateArchiveFiveLaneSegmentWriterV3.java | 2109 +++++++++++++++++ ...StateArchiveHotCheckpointMaterializer.java | 4 +- .../db2/archive/StateArchiveHotStore.java | 46 +- .../archive/StateArchiveSegmentFormatV3.java | 1136 +++++++++ .../core/CommonCheckpointPayloadFactory.java | 8 +- .../db2/core/CommonCheckpointRuntime.java | 35 +- .../tron/core/db2/core/SnapshotManager.java | 5 +- .../org/tron/core/config/args/Storage.java | 4 + .../tron/core/config/args/StorageConfig.java | 35 + common/src/main/resources/reference.conf | 9 + .../core/config/args/StorageConfigTest.java | 31 + .../java/org/tron/core/config/args/Args.java | 1 + .../main/java/org/tron/core/db/Manager.java | 74 +- framework/src/main/resources/config.conf | 7 + .../org/tron/core/config/args/ArgsTest.java | 3 + ...iveAppendCheckpointMaterializerV3Test.java | 319 +++ .../StateArchiveBlockFrameCodecV3Test.java | 216 ++ ...tateArchiveCheckpointMaterializerTest.java | 23 +- .../StateArchiveFiveLaneBlockCodecV3Test.java | 332 +++ ...eArchiveFiveLaneDurabilityProcessTest.java | 123 + ...eArchiveFiveLaneDurabilityProofV3Test.java | 173 ++ ...teArchiveFiveLaneRecoveryIntentV3Test.java | 151 ++ ...ateArchiveFiveLaneSegmentWriterV3Test.java | 509 ++++ .../StateArchiveSegmentFormatV3Test.java | 223 ++ .../db2/archive/StorageRowKeyCodecTest.java | 2 +- .../ChainbaseCheckpointMaterializerTest.java | 53 + 34 files changed, 9110 insertions(+), 47 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveBlockFrameCodecV3.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointPlanner.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFileFormatV3.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneBlockCodecV3.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProofV3.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneRecoveryIntentV3.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveBlockFrameCodecV3Test.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneBlockCodecV3Test.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProcessTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProofV3Test.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneRecoveryIntentV3Test.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3Test.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java new file mode 100644 index 00000000000..8619dc623ed --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java @@ -0,0 +1,288 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.tron.core.db2.archive.StateArchiveFiveLaneBlockCodecV3.EncodedBundle; +import org.tron.core.db2.archive.StateArchiveFiveLaneRecoveryIntentV3.RecoveryPoint; +import org.tron.core.db2.archive.StateArchiveFiveLaneSegmentWriterV3.ArchiveDurabilityProof; +import org.tron.core.db2.core.CommonCheckpointCapture; +import org.tron.core.db2.core.CommonCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Default-off Common participant backed by the five-lane append-file v3 authority. */ +public final class StateArchiveAppendCheckpointMaterializerV3 + implements CommonCheckpointMaterializer, StateArchiveCheckpointPlanner { + + private final Path directory; + private final byte[] commonFormatIdentity; + private final Engine bindingEngine; + private final short compressionId; + private final StateArchiveFiveLaneBlockCodecV3 codec = + new StateArchiveFiveLaneBlockCodecV3(); + private final StateArchiveFiveLaneSegmentWriterV3 writer; + private boolean closed; + + public StateArchiveAppendCheckpointMaterializerV3(Path directory, + byte[] commonFormatIdentity, Engine bindingEngine, byte[] baselineHistoryDigest, + short compressionId) throws IOException { + this(directory, commonFormatIdentity, bindingEngine, baselineHistoryDigest, + compressionId, StateArchiveFileFormatV3.SEGMENT_TARGET_BYTES); + } + + public StateArchiveAppendCheckpointMaterializerV3(Path directory, + byte[] commonFormatIdentity, Engine bindingEngine, byte[] baselineHistoryDigest, + short compressionId, long rotationTargetBytes) throws IOException { + this.directory = Objects.requireNonNull(directory, "directory"); + this.commonFormatIdentity = requireDigest(commonFormatIdentity, "Common format identity"); + this.bindingEngine = Objects.requireNonNull(bindingEngine, "bindingEngine"); + this.compressionId = compressionId; + this.writer = new StateArchiveFiveLaneSegmentWriterV3(directory, + baselineHistoryDigest, compressionId, rotationTargetBytes); + } + + @Override + public Authority authority() { + return Authority.STATE_ARCHIVE; + } + + /** Builds the existing v2 transient-body binding without writing append-file bytes. */ + @Override + public synchronized StateArchiveHotBatchDescriptor planCheckpoint( + List diffs) throws IOException { + requireOpen(); + List admitted = admittedDiffs(diffs); + BlockSnapshotMeta first = admitted.get(0).getMeta(); + long lastBlock = admitted.get(admitted.size() - 1).getMeta().getBlockNumber(); + BlockSnapshotMeta head = writer.getAppendHead(); + if (head != null && (head.getBlockNumber() < first.getBlockNumber() - 1 + || head.getBlockNumber() > lastBlock)) { + throw new IOException("Append-file Archive checkpoint is not the current successor or retry"); + } + return StateArchiveHotStore.planCheckpointDescriptor(bindingEngine, + first.getBlockNumber() - 1, first.getParentHash(), + new byte[StateArchiveFileFormatV3.HASH_LENGTH], admitted); + } + + /** Appends, group-forces, rereads and persists SAP3 before the Common WAL is published. */ + @Override + public synchronized CommonCheckpointTarget prepare(CommonCheckpointCapture capture) + throws IOException { + requireOpen(); + CommonCheckpointCapture admitted = Objects.requireNonNull(capture, "capture"); + CommonCheckpointTarget target = requireTarget( + CommonCheckpointTarget.from(admitted.getPayload())); + Status status = inspect(target); + if (status != Status.NEEDS_MATERIALIZATION) { + return target; + } + List diffs = admittedDiffs(admitted.getArchiveDiffs()); + if (!admitted.getArchiveBinding().equals(planCheckpoint(diffs))) { + throw new IOException("Append-file Archive checkpoint binding differs"); + } + ArchiveDurabilityProof recoveredProof = writer.getLastDurabilityProof(); + if (recoveredProof != null && matches(recoveredProof, target)) { + ArchiveDurabilityProof forced = writer.sync(recoveredProof.getCheckpointSequence(), + recoveredProof.getTarget(), target.getPayloadDigest()); + StateArchiveFiveLaneDurabilityProofV3.publish(directory, forced); + return target; + } + byte[] previousHistory = writer.getResultHistoryDigest(); + int firstMissing = firstMissing(diffs, writer.getAppendHead()); + for (int index = firstMissing; index < diffs.size(); index++) { + BlockReverseDiff diff = diffs.get(index); + EncodedBundle bundle = codec.encode(diff, previousHistory, + writerCompressionId()); + writer.appendForCheckpoint(bundle, target.getLastBlock().getBlockNumber(), + target.getPayloadDigest()); + previousHistory = bundle.getResultHistoryDigest(); + } + BlockSnapshotMeta meta = target.getLastBlock(); + RecoveryPoint point = new RecoveryPoint(meta.getEpoch(), meta.getBlockNumber(), + meta.getTimestamp(), meta.getBlockHash(), meta.getParentHash(), + writer.getResultHistoryDigest()); + ArchiveDurabilityProof proof = writer.sync(target.getLastBlock().getBlockNumber(), + point, target.getPayloadDigest()); + StateArchiveFiveLaneDurabilityProofV3.publish(directory, proof); + if (inspect(target) != Status.MATERIALIZED) { + throw new IOException("Append-file Archive SAP3 materialization is not exact"); + } + return target; + } + + @Override + public synchronized Status inspect(CommonCheckpointTarget target) throws IOException { + requireOpen(); + CommonCheckpointTarget admitted = requireTarget(target); + Optional readable = + StateArchiveCheckpointMaterializer.loadReadableTargetIfPresent(directory); + if (readable.isPresent() && readable.get().equals(admitted)) { + requireExactProof(admitted); + return Status.PUBLISHED; + } + if (readable.isPresent()) { + requireParent(readable.get(), admitted); + } + if (!Files.isRegularFile(directory.resolve(StateArchiveFiveLaneDurabilityProofV3.FILE_NAME), + LinkOption.NOFOLLOW_LINKS)) { + return Status.NEEDS_MATERIALIZATION; + } + ArchiveDurabilityProof proof = StateArchiveFiveLaneDurabilityProofV3.loadAndVerify( + directory, writer); + if (matches(proof, admitted)) { + return Status.MATERIALIZED; + } + if (!readable.isPresent()) { + throw new IOException("Append-file Archive has an uncommitted different SAP3 target"); + } + return Status.NEEDS_MATERIALIZATION; + } + + /** Loads and revalidates the append-file target made readable by the Common publish barrier. */ + public synchronized Optional loadPublishedTargetIfPresent() + throws IOException { + requireOpen(); + Optional target = + StateArchiveCheckpointMaterializer.loadReadableTargetIfPresent(directory); + if (target.isPresent() && inspect(target.get()) != Status.PUBLISHED) { + throw new IOException("Append-file Archive readable target is not fully published"); + } + return target; + } + + @Override + public synchronized void materialize(CommonCheckpointPayload payload, + CommonCheckpointTarget target) throws IOException { + CommonCheckpointPayload admittedPayload = Objects.requireNonNull(payload, "payload"); + CommonCheckpointTarget admittedTarget = requireTarget(target); + if (admittedPayload.getVersion() != CommonCheckpointPayload.COORDINATION_FORMAT_VERSION + || !admittedTarget.equals(CommonCheckpointTarget.from(admittedPayload))) { + throw new IOException("Append-file Archive requires its exact coordination payload"); + } + if (inspect(admittedTarget) == Status.NEEDS_MATERIALIZATION) { + throw new IOException("Append-file Archive must be prepared before Common WAL publication"); + } + } + + @Override + public synchronized void publish(CommonCheckpointTarget target) throws IOException { + CommonCheckpointTarget admitted = requireTarget(target); + Status status = inspect(admitted); + if (status == Status.PUBLISHED) { + return; + } + if (status != Status.MATERIALIZED) { + throw new IOException("Append-file Archive target is not materialized"); + } + StateArchiveCheckpointMaterializer.publishReadableTarget(directory, admitted); + } + + @Override + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + writer.close(); + } + } + + private void requireExactProof(CommonCheckpointTarget target) throws IOException { + ArchiveDurabilityProof proof = StateArchiveFiveLaneDurabilityProofV3.loadAndVerify( + directory, writer); + if (!matches(proof, target)) { + throw new IOException("Append-file Archive proof differs from readable target"); + } + } + + private static boolean matches(ArchiveDurabilityProof proof, + CommonCheckpointTarget target) { + BlockSnapshotMeta meta = target.getLastBlock(); + RecoveryPoint point = proof.getTarget(); + return point.getEpoch() == meta.getEpoch() + && point.getBlockNumber() == meta.getBlockNumber() + && point.getTimestamp() == meta.getTimestamp() + && Arrays.equals(point.getBlockHash(), meta.getBlockHash()) + && Arrays.equals(point.getParentHash(), meta.getParentHash()) + && Arrays.equals(proof.getCommonTargetDigest(), target.getPayloadDigest()); + } + + private CommonCheckpointTarget requireTarget(CommonCheckpointTarget target) + throws IOException { + CommonCheckpointTarget admitted = Objects.requireNonNull(target, "target"); + if (!Arrays.equals(commonFormatIdentity, admitted.getFormatIdentity())) { + throw new IOException("Append-file Archive Common format identity differs"); + } + return admitted; + } + + private static void requireParent(CommonCheckpointTarget parent, + CommonCheckpointTarget target) throws IOException { + if (!Arrays.equals(parent.getFormatIdentity(), target.getFormatIdentity()) + || parent.getLastBlock().getBlockNumber() + 1 != target.getFirstBlock().getBlockNumber() + || !Arrays.equals(parent.getLastBlock().getBlockHash(), + target.getFirstBlock().getParentHash()) + || !Arrays.equals(parent.getStateRoot(), target.getParentStateRoot())) { + throw new IOException("Append-file Archive readable target is not the parent"); + } + } + + private static List admittedDiffs(List diffs) { + List admitted = new ArrayList<>(Objects.requireNonNull(diffs, "diffs")); + if (admitted.isEmpty() || admitted.contains(null)) { + throw new IllegalArgumentException("Append-file Archive checkpoint requires blocks"); + } + BlockSnapshotMeta previous = null; + for (BlockReverseDiff diff : admitted) { + BlockSnapshotMeta current = diff.getMeta(); + if (current.getEpoch() != current.getBlockNumber() + || previous != null && (current.getBlockNumber() != previous.getBlockNumber() + 1 + || !Arrays.equals(current.getParentHash(), previous.getBlockHash()))) { + throw new IllegalArgumentException("Append-file Archive block chain is not contiguous"); + } + previous = current; + } + return admitted; + } + + private static int firstMissing(List diffs, BlockSnapshotMeta head) + throws IOException { + if (head == null || head.getBlockNumber() < diffs.get(0).getMeta().getBlockNumber()) { + return 0; + } + for (int index = 0; index < diffs.size(); index++) { + BlockSnapshotMeta meta = diffs.get(index).getMeta(); + if (meta.getBlockNumber() == head.getBlockNumber()) { + if (!meta.equals(head)) { + throw new IOException("Append-file Archive retry prefix identity differs"); + } + return index + 1; + } + } + throw new IOException("Append-file Archive retry head is outside the checkpoint range"); + } + + private short writerCompressionId() { + return compressionId; + } + + private void requireOpen() throws IOException { + if (closed) { + throw new IOException("Append-file Archive materializer is closed"); + } + } + + private static byte[] requireDigest(byte[] value, String name) { + byte[] admitted = Arrays.copyOf(Objects.requireNonNull(value, name), value.length); + if (admitted.length != StateArchiveFileFormatV3.HASH_LENGTH) { + throw new IllegalArgumentException(name + " must contain exactly 32 bytes"); + } + return admitted; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveBlockFrameCodecV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveBlockFrameCodecV3.java new file mode 100644 index 00000000000..85e23a1d41f --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveBlockFrameCodecV3.java @@ -0,0 +1,796 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +/** Canonical codec for one default-off State Archive v3 mixed-stream block frame. */ +public final class StateArchiveBlockFrameCodecV3 { + + private static final long UNSIGNED_INT_MAX = 0xffff_ffffL; + private static final int TRAILER_CRC_AND_MAGIC_LENGTH = 8; + + /** + * Validates the exact capture boundary before collecting old values and encoding the frame. + */ + public EncodedBlock encode(BlockChangeView view, OldValueCollector collector, + byte[] previousHistoryDigest, short compressionId) { + Objects.requireNonNull(view, "view"); + Objects.requireNonNull(collector, "collector"); + List captured = new ArrayList<>(); + for (BlockChangeView.DatabaseChanges database : view.getDatabases()) { + captured.add(database.getDbName()); + } + StateArchiveFileFormatV3.requireExactCapture(captured); + BlockReverseDiff diff = Objects.requireNonNull(collector.collect(view), "collected diff"); + if (!view.getMeta().equals(diff.getMeta())) { + throw new IllegalArgumentException("Collected history block identity changed"); + } + return encode(diff, previousHistoryDigest, compressionId); + } + + /** Encodes an already collected reverse diff with exact-27 descriptor coverage. */ + EncodedBlock encode(BlockReverseDiff diff, byte[] previousHistoryDigest, + short compressionId) { + Objects.requireNonNull(diff, "diff"); + byte[] previousDigest = requireHash(previousHistoryDigest, "previousHistoryDigest"); + requireCompression(compressionId); + BlockSnapshotMeta meta = diff.getMeta(); + if (meta.getEpoch() != meta.getBlockNumber()) { + throw new IllegalArgumentException("v3 requires epoch to equal blockNumber"); + } + if (meta.getTimestamp() < 0) { + throw new IllegalArgumentException("v3 timestamp must not be negative"); + } + + CanonicalPayload canonical = encodePayload(diff.getGroups()); + byte[] payloadDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.PAYLOAD_DOMAIN, canonical.bytes); + byte[] descriptorDigest = StateArchiveFileFormatV3.storeDescriptorDigest(); + byte[] blockHistoryDigest = blockHistoryDigest(meta, descriptorDigest, canonical, + payloadDigest); + byte[] resultHistoryDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ROLLING_DOMAIN, previousDigest, blockHistoryDigest); + byte[] storedPayload = compressionId == StateArchiveFileFormatV3.COMPRESSION_NONE + ? canonical.bytes : deflate(canonical.bytes); + long totalLength = checkedAdd(StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH, + storedPayload.length, StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH); + if (totalLength > StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES) { + throw new IllegalArgumentException("State Archive block frame exceeds 64 MiB"); + } + + ByteBuffer frame = ByteBuffer.allocate((int) totalLength); + putEnvelope(frame, totalLength, storedPayload.length); + frame.putLong(meta.getEpoch()); + frame.putLong(meta.getBlockNumber()); + frame.putLong(meta.getTimestamp()); + frame.put(meta.getBlockHash()); + frame.put(meta.getParentHash()); + frame.put(descriptorDigest); + frame.put(previousDigest); + frame.put(payloadDigest); + frame.put(blockHistoryDigest); + frame.put(resultHistoryDigest); + frame.putLong(StateArchiveFileFormatV3.EXACT_COVERAGE_BITMAP); + frame.putLong(canonical.changedBitmap); + frame.putInt(canonical.groupCount); + frame.putInt(0); + frame.putLong(canonical.entryCount); + frame.putLong(canonical.bytes.length); + frame.putShort(StateArchiveFileFormatV3.BODY_CODEC_ID); + frame.putShort(compressionId); + frame.putShort(StateArchiveFileFormatV3.KEY_ORDER_ID); + frame.putShort(StateArchiveFileFormatV3.DIGEST_ID); + frame.putShort(StateArchiveFileFormatV3.CHECKSUM_ID); + frame.putShort(StateArchiveFileFormatV3.IDENTITY_KIND); + frame.putInt(0); + if (frame.position() != StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH) { + throw new IllegalStateException("Invalid State Archive block header length"); + } + frame.put(storedPayload); + byte[] encodedDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ENCODED_BLOCK_DOMAIN, + Arrays.copyOf(frame.array(), frame.position())); + frame.put(encodedDigest); + frame.putLong(totalLength); + int crcLength = frame.position(); + frame.putInt(crc32c(frame.array(), 0, crcLength)); + frame.putInt(StateArchiveFileFormatV3.FRAME_TRAILER_MAGIC); + if (frame.hasRemaining()) { + throw new IllegalStateException("Invalid State Archive frame length"); + } + return new EncodedBlock(frame.array(), canonical.bytes, payloadDigest, + blockHistoryDigest, resultHistoryDigest, encodedDigest, diff); + } + + /** Decodes and fully verifies one complete v3 block frame. */ + public DecodedBlock decode(byte[] frameBytes) { + Objects.requireNonNull(frameBytes, "frameBytes"); + if (frameBytes.length < StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH + || frameBytes.length > StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES) { + throw new IllegalArgumentException("State Archive block frame length is invalid"); + } + ByteBuffer frame = ByteBuffer.wrap(frameBytes); + requireInt(frame, StateArchiveFileFormatV3.FRAME_MAGIC, "frame magic"); + requireShort(frame, StateArchiveFileFormatV3.MAJOR_VERSION, "major version"); + requireShort(frame, StateArchiveFileFormatV3.MINOR_VERSION, "minor version"); + requireShort(frame, StateArchiveFileFormatV3.BLOCK_FRAME_TYPE, "frame type"); + requireShort(frame, (short) 0, "frame flags"); + requireInt(frame, StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH, "header length"); + long totalLength = frame.getLong(); + long payloadLength = frame.getLong(); + if (totalLength != frameBytes.length || payloadLength < 0 + || checkedAdd(StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH, payloadLength, + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH) != totalLength) { + throw new IllegalArgumentException("State Archive frame envelope length mismatch"); + } + + long epoch = requireNonNegative(frame.getLong(), "epoch"); + long blockNumber = requireNonNegative(frame.getLong(), "blockNumber"); + long timestamp = requireNonNegative(frame.getLong(), "timestamp"); + if (epoch != blockNumber) { + throw new IllegalArgumentException("v3 requires epoch to equal blockNumber"); + } + byte[] blockHash = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + byte[] parentHash = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + byte[] descriptorDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + requireArray(descriptorDigest, StateArchiveFileFormatV3.storeDescriptorDigest(), + "Store descriptor digest"); + byte[] previousHistoryDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + byte[] payloadDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + byte[] blockHistoryDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + byte[] resultHistoryDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + long coverageBitmap = frame.getLong(); + long changedBitmap = frame.getLong(); + int groupCount = requireNonNegative(frame.getInt(), "groupCount"); + requireInt(frame, 0, "block reserved field"); + long entryCount = requireNonNegative(frame.getLong(), "entryCount"); + long rawPayloadLength = requireNonNegative(frame.getLong(), "rawPayloadLength"); + requireShort(frame, StateArchiveFileFormatV3.BODY_CODEC_ID, "body codec"); + short compressionId = frame.getShort(); + requireCompression(compressionId); + requireShort(frame, StateArchiveFileFormatV3.KEY_ORDER_ID, "key order"); + requireShort(frame, StateArchiveFileFormatV3.DIGEST_ID, "digest algorithm"); + requireShort(frame, StateArchiveFileFormatV3.CHECKSUM_ID, "checksum algorithm"); + requireShort(frame, StateArchiveFileFormatV3.IDENTITY_KIND, "identity kind"); + requireInt(frame, 0, "block reserved tail"); + if (coverageBitmap != StateArchiveFileFormatV3.EXACT_COVERAGE_BITMAP + || (changedBitmap & ~coverageBitmap) != 0 + || groupCount != Long.bitCount(changedBitmap) + || rawPayloadLength > StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES) { + throw new IllegalArgumentException("State Archive block coverage is invalid"); + } + + byte[] storedPayload = getBytes(frame, checkedInt(payloadLength, "payloadLength")); + int trailerStart = frame.position(); + byte[] encodedFrameDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + if (frame.getLong() != totalLength) { + throw new IllegalArgumentException("State Archive repeated frame length mismatch"); + } + int expectedCrc = frame.getInt(); + requireInt(frame, StateArchiveFileFormatV3.FRAME_TRAILER_MAGIC, "trailer magic"); + int actualCrc = crc32c(frameBytes, 0, + frameBytes.length - TRAILER_CRC_AND_MAGIC_LENGTH); + if (expectedCrc != actualCrc) { + throw new IllegalArgumentException("State Archive frame checksum mismatch"); + } + byte[] actualEncodedDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ENCODED_BLOCK_DOMAIN, + Arrays.copyOf(frameBytes, trailerStart)); + requireArray(encodedFrameDigest, actualEncodedDigest, "encoded frame digest"); + + byte[] rawPayload = compressionId == StateArchiveFileFormatV3.COMPRESSION_NONE + ? storedPayload : inflate(storedPayload, checkedInt(rawPayloadLength, + "rawPayloadLength")); + if (rawPayload.length != rawPayloadLength) { + throw new IllegalArgumentException("State Archive raw payload length mismatch"); + } + requireArray(payloadDigest, StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.PAYLOAD_DOMAIN, rawPayload), "payload digest"); + DecodedPayload decodedPayload = decodePayload(rawPayload, groupCount, entryCount, + changedBitmap); + BlockSnapshotMeta meta = new BlockSnapshotMeta(epoch, blockNumber, blockHash, parentHash, + timestamp); + CanonicalPayload identityPayload = new CanonicalPayload(rawPayload, groupCount, entryCount, + changedBitmap); + byte[] actualBlockDigest = blockHistoryDigest(meta, descriptorDigest, identityPayload, + payloadDigest); + requireArray(blockHistoryDigest, actualBlockDigest, "block history digest"); + requireArray(resultHistoryDigest, StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ROLLING_DOMAIN, previousHistoryDigest, blockHistoryDigest), + "result history digest"); + BlockReverseDiff diff = new BlockReverseDiff(meta, decodedPayload.groups); + return new DecodedBlock(diff, rawPayload, previousHistoryDigest, payloadDigest, + blockHistoryDigest, resultHistoryDigest, encodedFrameDigest, compressionId); + } + + /** Returns the complete length described by a v3 common envelope. */ + public int frameLength(byte[] header) { + if (header == null || header.length < StateArchiveFileFormatV3.FRAME_ENVELOPE_LENGTH) { + throw new IllegalArgumentException("State Archive frame envelope is truncated"); + } + ByteBuffer buffer = ByteBuffer.wrap(header); + requireInt(buffer, StateArchiveFileFormatV3.FRAME_MAGIC, "frame magic"); + requireShort(buffer, StateArchiveFileFormatV3.MAJOR_VERSION, "major version"); + requireShort(buffer, StateArchiveFileFormatV3.MINOR_VERSION, "minor version"); + requireShort(buffer, StateArchiveFileFormatV3.BLOCK_FRAME_TYPE, "frame type"); + requireShort(buffer, (short) 0, "frame flags"); + requireInt(buffer, StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH, "header length"); + long totalLength = buffer.getLong(); + long payloadLength = buffer.getLong(); + if (totalLength > StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES + || totalLength != checkedAdd(StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH, + payloadLength, StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH)) { + throw new IllegalArgumentException("State Archive frame envelope length mismatch"); + } + return checkedInt(totalLength, "totalLength"); + } + + private CanonicalPayload encodePayload(Collection inputGroups) { + List groups = new ArrayList<>(); + Set storeIds = new HashSet<>(); + for (DbGroup group : inputGroups) { + int storeId = StateArchiveFileFormatV3.storeId(group.getDbName()); + if (!storeIds.add(storeId)) { + throw new IllegalArgumentException("Duplicate archive Store ID: " + storeId); + } + if (group.getEntries().isEmpty()) { + throw new IllegalArgumentException("Changed archive Store section must not be empty"); + } + groups.add(new StoreGroup(storeId, group.getEntries())); + } + groups.sort(Comparator.comparingInt(group -> group.storeId)); + List sections = new ArrayList<>(); + long entryCount = 0; + long sectionBytes = 0; + long changedBitmap = 0; + for (StoreGroup group : groups) { + byte[] section = encodeSection(group); + sections.add(section); + entryCount = checkedAdd(entryCount, group.entries.size()); + sectionBytes = checkedAdd(sectionBytes, section.length); + changedBitmap |= 1L << (group.storeId - 1); + } + long payloadLength = checkedAdd(StateArchiveFileFormatV3.PAYLOAD_HEADER_LENGTH, + sectionBytes); + if (payloadLength > StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES) { + throw new IllegalArgumentException("State Archive canonical payload is too large"); + } + ByteBuffer payload = ByteBuffer.allocate((int) payloadLength); + payload.putInt(StateArchiveFileFormatV3.PAYLOAD_MAGIC); + payload.putShort((short) 1); + payload.putShort((short) 0); + payload.putInt(StateArchiveFileFormatV3.PAYLOAD_HEADER_LENGTH); + payload.putInt(groups.size()); + payload.putLong(entryCount); + payload.putLong(sectionBytes); + for (byte[] section : sections) { + payload.put(section); + } + return new CanonicalPayload(payload.array(), groups.size(), entryCount, changedBitmap); + } + + private byte[] encodeSection(StoreGroup group) { + int entryCount = group.entries.size(); + long offsetVectorLength = checkedMultiply(entryCount + 1L, Integer.BYTES); + ByteArrayOutputStream records = new ByteArrayOutputStream(); + ByteArrayOutputStream values = new ByteArrayOutputStream(); + ByteBuffer offsets = ByteBuffer.allocate(checkedInt(offsetVectorLength, + "offsetVectorLength")); + byte[] previousKey = null; + try (DataOutputStream recordOutput = new DataOutputStream(records)) { + for (Entry entry : group.entries) { + byte[] key = entry.getKey(); + if (previousKey != null && BlockReverseDiff.compareUnsigned(previousKey, key) >= 0) { + throw new IllegalArgumentException("Archive keys are not strictly sorted"); + } + previousKey = key; + offsets.putInt(records.size()); + recordOutput.writeInt(key.length); + OldValue oldValue = entry.getOldValue(); + recordOutput.writeByte(oldValue.isPresent() ? 1 : 0); + recordOutput.write(new byte[3]); + if (oldValue.isPresent()) { + byte[] value = oldValue.getValue(); + recordOutput.writeInt(values.size()); + recordOutput.writeInt(value.length); + values.write(value); + } else { + recordOutput.writeInt(-1); + recordOutput.writeInt(-1); + } + recordOutput.write(key); + } + recordOutput.flush(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected in-memory section encoding failure", impossible); + } + offsets.putInt(records.size()); + byte[] offsetBytes = offsets.array(); + byte[] recordBytes = records.toByteArray(); + byte[] valueBytes = values.toByteArray(); + long sectionLength = checkedAdd(StateArchiveFileFormatV3.SECTION_HEADER_LENGTH, + offsetBytes.length, recordBytes.length, valueBytes.length); + byte[] sectionDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.SECTION_DOMAIN, + ByteBuffer.allocate(Short.BYTES + Integer.BYTES) + .putShort((short) group.storeId).putInt(entryCount).array(), + offsetBytes, recordBytes, valueBytes); + ByteBuffer section = ByteBuffer.allocate(checkedInt(sectionLength, "sectionLength")); + section.putShort((short) group.storeId); + section.putShort((short) 1); + section.putInt(StateArchiveFileFormatV3.SECTION_HEADER_LENGTH); + section.putInt(entryCount); + section.putInt(0); + section.putLong(offsetBytes.length); + section.putLong(recordBytes.length); + section.putLong(valueBytes.length); + section.putLong(sectionLength); + section.put(sectionDigest); + section.put(offsetBytes); + section.put(recordBytes); + section.put(valueBytes); + return section.array(); + } + + private DecodedPayload decodePayload(byte[] payloadBytes, int expectedGroupCount, + long expectedEntryCount, long expectedChangedBitmap) { + ByteBuffer payload = ByteBuffer.wrap(payloadBytes); + requireRemaining(payload, StateArchiveFileFormatV3.PAYLOAD_HEADER_LENGTH, + "payload header"); + requireInt(payload, StateArchiveFileFormatV3.PAYLOAD_MAGIC, "payload magic"); + requireShort(payload, (short) 1, "payload version"); + requireShort(payload, (short) 0, "payload flags"); + requireInt(payload, StateArchiveFileFormatV3.PAYLOAD_HEADER_LENGTH, + "payload header length"); + int groupCount = requireNonNegative(payload.getInt(), "payload section count"); + long entryCount = requireNonNegative(payload.getLong(), "payload entry count"); + long sectionBytes = requireNonNegative(payload.getLong(), "payload section bytes"); + if (groupCount != expectedGroupCount || entryCount != expectedEntryCount + || sectionBytes != payload.remaining()) { + throw new IllegalArgumentException("State Archive payload counts or length mismatch"); + } + List groups = new ArrayList<>(groupCount); + int previousStoreId = 0; + long actualEntryCount = 0; + long actualChangedBitmap = 0; + for (int index = 0; index < groupCount; index++) { + requireRemaining(payload, StateArchiveFileFormatV3.SECTION_HEADER_LENGTH, + "Store section header"); + int sectionStart = payload.position(); + int storeId = Short.toUnsignedInt(payload.getShort()); + if (storeId <= previousStoreId || storeId > StateArchiveFileFormatV3.STORE_COUNT) { + throw new IllegalArgumentException("State Archive Store sections are not ordered"); + } + previousStoreId = storeId; + requireShort(payload, (short) 1, "section version"); + requireInt(payload, StateArchiveFileFormatV3.SECTION_HEADER_LENGTH, + "section header length"); + int count = payload.getInt(); + if (count <= 0) { + throw new IllegalArgumentException("State Archive Store section is empty"); + } + requireInt(payload, 0, "section flags"); + long offsetVectorLength = payload.getLong(); + long indexRecordLength = payload.getLong(); + long valueDataLength = payload.getLong(); + long sectionLength = payload.getLong(); + byte[] sectionDigest = getBytes(payload, StateArchiveFileFormatV3.HASH_LENGTH); + long expectedOffsetLength = checkedMultiply(count + 1L, Integer.BYTES); + long expectedSectionLength = checkedAdd(StateArchiveFileFormatV3.SECTION_HEADER_LENGTH, + offsetVectorLength, indexRecordLength, valueDataLength); + if (offsetVectorLength != expectedOffsetLength || sectionLength != expectedSectionLength + || sectionLength > payloadBytes.length + || sectionLength > payloadBytes.length - sectionStart) { + throw new IllegalArgumentException("State Archive Store section length is invalid"); + } + byte[] offsetBytes = getBytes(payload, checkedInt(offsetVectorLength, + "offsetVectorLength")); + byte[] recordBytes = getBytes(payload, checkedInt(indexRecordLength, + "indexRecordLength")); + byte[] valueBytes = getBytes(payload, checkedInt(valueDataLength, + "valueDataLength")); + byte[] actualSectionDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.SECTION_DOMAIN, + ByteBuffer.allocate(Short.BYTES + Integer.BYTES) + .putShort((short) storeId).putInt(count).array(), + offsetBytes, recordBytes, valueBytes); + requireArray(sectionDigest, actualSectionDigest, "section digest"); + List entries = decodeEntries(count, offsetBytes, recordBytes, valueBytes); + groups.add(new DbGroup(StateArchiveFileFormatV3.dbName(storeId), entries)); + actualEntryCount = checkedAdd(actualEntryCount, count); + actualChangedBitmap |= 1L << (storeId - 1); + } + if (payload.hasRemaining() || actualEntryCount != entryCount + || actualChangedBitmap != expectedChangedBitmap) { + throw new IllegalArgumentException("State Archive payload coverage mismatch"); + } + return new DecodedPayload(groups); + } + + private List decodeEntries(int count, byte[] offsetBytes, byte[] recordBytes, + byte[] valueBytes) { + ByteBuffer offsets = ByteBuffer.wrap(offsetBytes); + int[] positions = new int[count + 1]; + for (int index = 0; index <= count; index++) { + long offset = Integer.toUnsignedLong(offsets.getInt()); + if (offset > recordBytes.length || (index == 0 && offset != 0) + || (index > 0 && offset <= positions[index - 1])) { + throw new IllegalArgumentException("State Archive record offsets are invalid"); + } + positions[index] = (int) offset; + } + if (positions[count] != recordBytes.length) { + throw new IllegalArgumentException("State Archive final record offset is invalid"); + } + List entries = new ArrayList<>(count); + byte[] previousKey = null; + int valueCursor = 0; + for (int index = 0; index < count; index++) { + int recordLength = positions[index + 1] - positions[index]; + if (recordLength < StateArchiveFileFormatV3.VARIABLE_INDEX_RECORD_BASE_LENGTH) { + throw new IllegalArgumentException("State Archive index record is truncated"); + } + ByteBuffer record = ByteBuffer.wrap(recordBytes, positions[index], recordLength).slice(); + long keyLength = Integer.toUnsignedLong(record.getInt()); + int tag = Byte.toUnsignedInt(record.get()); + requireZero(record, 3, "index record reserved bytes"); + long valueOffset = Integer.toUnsignedLong(record.getInt()); + long valueLength = Integer.toUnsignedLong(record.getInt()); + if (keyLength != record.remaining()) { + throw new IllegalArgumentException("State Archive key length is invalid"); + } + byte[] key = getBytes(record, checkedInt(keyLength, "keyLength")); + if (previousKey != null && BlockReverseDiff.compareUnsigned(previousKey, key) >= 0) { + throw new IllegalArgumentException("State Archive decoded keys are not ordered"); + } + previousKey = key; + OldValue oldValue; + if (tag == 0) { + if (valueOffset != UNSIGNED_INT_MAX || valueLength != UNSIGNED_INT_MAX) { + throw new IllegalArgumentException("State Archive ABSENT value locator is invalid"); + } + oldValue = OldValue.absent(); + } else if (tag == 1) { + if (valueOffset != valueCursor || valueLength > valueBytes.length - valueCursor) { + throw new IllegalArgumentException("State Archive PRESENT value locator is invalid"); + } + byte[] value = Arrays.copyOfRange(valueBytes, valueCursor, + valueCursor + (int) valueLength); + valueCursor += (int) valueLength; + oldValue = OldValue.present(value); + } else { + throw new IllegalArgumentException("Unknown State Archive old-value tag: " + tag); + } + entries.add(new Entry(key, oldValue)); + } + if (valueCursor != valueBytes.length) { + throw new IllegalArgumentException("State Archive value data has an unreferenced tail"); + } + return entries; + } + + private byte[] blockHistoryDigest(BlockSnapshotMeta meta, byte[] descriptorDigest, + CanonicalPayload payload, byte[] payloadDigest) { + int identityLength = 3 * Long.BYTES + 3 * StateArchiveFileFormatV3.HASH_LENGTH + + 2 * Long.BYTES + Integer.BYTES + 2 * Long.BYTES + + StateArchiveFileFormatV3.HASH_LENGTH; + ByteBuffer identity = ByteBuffer.allocate(identityLength); + identity.putLong(meta.getEpoch()); + identity.putLong(meta.getBlockNumber()); + identity.putLong(meta.getTimestamp()); + identity.put(meta.getBlockHash()); + identity.put(meta.getParentHash()); + identity.put(descriptorDigest); + identity.putLong(StateArchiveFileFormatV3.EXACT_COVERAGE_BITMAP); + identity.putLong(payload.changedBitmap); + identity.putInt(payload.groupCount); + identity.putLong(payload.entryCount); + identity.putLong(payload.bytes.length); + identity.put(payloadDigest); + return StateArchiveFileFormatV3.sha256(StateArchiveFileFormatV3.BLOCK_DOMAIN, + identity.array()); + } + + private void putEnvelope(ByteBuffer frame, long totalLength, long payloadLength) { + frame.putInt(StateArchiveFileFormatV3.FRAME_MAGIC); + frame.putShort(StateArchiveFileFormatV3.MAJOR_VERSION); + frame.putShort(StateArchiveFileFormatV3.MINOR_VERSION); + frame.putShort(StateArchiveFileFormatV3.BLOCK_FRAME_TYPE); + frame.putShort((short) 0); + frame.putInt(StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH); + frame.putLong(totalLength); + frame.putLong(payloadLength); + } + + private byte[] deflate(byte[] input) { + Deflater deflater = new Deflater(1, true); + deflater.setStrategy(Deflater.DEFAULT_STRATEGY); + deflater.setInput(input); + deflater.finish(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); + if (count == 0 && deflater.needsInput()) { + throw new IllegalStateException("Unexpected raw DEFLATE termination"); + } + output.write(buffer, 0, count); + } + deflater.end(); + return output.toByteArray(); + } + + private byte[] inflate(byte[] input, int expectedLength) { + Inflater inflater = new Inflater(true); + inflater.setInput(input); + byte[] output = new byte[expectedLength]; + try { + int count = inflater.inflate(output); + if (count != expectedLength || !inflater.finished() || inflater.getRemaining() != 0) { + throw new IllegalArgumentException("State Archive compressed payload length mismatch"); + } + return output; + } catch (DataFormatException e) { + throw new IllegalArgumentException("Invalid State Archive compressed payload", e); + } finally { + inflater.end(); + } + } + + private static byte[] requireHash(byte[] value, String name) { + Objects.requireNonNull(value, name); + if (value.length != StateArchiveFileFormatV3.HASH_LENGTH) { + throw new IllegalArgumentException(name + " must be exactly 32 bytes"); + } + return Arrays.copyOf(value, value.length); + } + + private static void requireCompression(short compressionId) { + if (compressionId != StateArchiveFileFormatV3.COMPRESSION_NONE + && compressionId != StateArchiveFileFormatV3.COMPRESSION_RAW_DEFLATE_LEVEL_1) { + throw new IllegalArgumentException("Unsupported State Archive compression: " + + compressionId); + } + } + + private static long requireNonNegative(long value, String name) { + if (value < 0) { + throw new IllegalArgumentException("State Archive " + name + " is negative"); + } + return value; + } + + private static int requireNonNegative(int value, String name) { + if (value < 0) { + throw new IllegalArgumentException("State Archive " + name + " is negative"); + } + return value; + } + + private static void requireInt(ByteBuffer buffer, int expected, String name) { + requireRemaining(buffer, Integer.BYTES, name); + if (buffer.getInt() != expected) { + throw new IllegalArgumentException("Invalid State Archive " + name); + } + } + + private static void requireShort(ByteBuffer buffer, short expected, String name) { + requireRemaining(buffer, Short.BYTES, name); + if (buffer.getShort() != expected) { + throw new IllegalArgumentException("Invalid State Archive " + name); + } + } + + private static void requireZero(ByteBuffer buffer, int length, String name) { + requireRemaining(buffer, length, name); + for (int index = 0; index < length; index++) { + if (buffer.get() != 0) { + throw new IllegalArgumentException("Invalid State Archive " + name); + } + } + } + + private static void requireRemaining(ByteBuffer buffer, int length, String name) { + if (length < 0 || buffer.remaining() < length) { + throw new IllegalArgumentException("Truncated State Archive " + name); + } + } + + private static byte[] getBytes(ByteBuffer buffer, int length) { + requireRemaining(buffer, length, "byte field"); + byte[] bytes = new byte[length]; + buffer.get(bytes); + return bytes; + } + + private static void requireArray(byte[] actual, byte[] expected, String name) { + if (!Arrays.equals(actual, expected)) { + throw new IllegalArgumentException("State Archive " + name + " mismatch"); + } + } + + private static int crc32c(byte[] bytes, int offset, int length) { + return Hashing.crc32c().hashBytes(bytes, offset, length).asInt(); + } + + private static long checkedAdd(long... values) { + long result = 0; + for (long value : values) { + if (value < 0 || Long.MAX_VALUE - result < value) { + throw new IllegalArgumentException("State Archive length overflow"); + } + result += value; + } + return result; + } + + private static long checkedMultiply(long left, long right) { + if (left < 0 || right < 0 || (left != 0 && right > Long.MAX_VALUE / left)) { + throw new IllegalArgumentException("State Archive length overflow"); + } + return left * right; + } + + private static int checkedInt(long value, String name) { + if (value < 0 || value > Integer.MAX_VALUE) { + throw new IllegalArgumentException("State Archive " + name + " exceeds int range"); + } + return (int) value; + } + + private static final class StoreGroup { + private final int storeId; + private final List entries; + + private StoreGroup(int storeId, List entries) { + this.storeId = storeId; + this.entries = entries; + } + } + + private static final class CanonicalPayload { + private final byte[] bytes; + private final int groupCount; + private final long entryCount; + private final long changedBitmap; + + private CanonicalPayload(byte[] bytes, int groupCount, long entryCount, + long changedBitmap) { + this.bytes = bytes; + this.groupCount = groupCount; + this.entryCount = entryCount; + this.changedBitmap = changedBitmap; + } + } + + private static final class DecodedPayload { + private final List groups; + + private DecodedPayload(List groups) { + this.groups = groups; + } + } + + public static final class EncodedBlock { + private final byte[] frame; + private final byte[] canonicalPayload; + private final byte[] payloadDigest; + private final byte[] blockHistoryDigest; + private final byte[] resultHistoryDigest; + private final byte[] encodedFrameDigest; + private final BlockReverseDiff diff; + + private EncodedBlock(byte[] frame, byte[] canonicalPayload, byte[] payloadDigest, + byte[] blockHistoryDigest, byte[] resultHistoryDigest, byte[] encodedFrameDigest, + BlockReverseDiff diff) { + this.frame = frame; + this.canonicalPayload = canonicalPayload; + this.payloadDigest = payloadDigest; + this.blockHistoryDigest = blockHistoryDigest; + this.resultHistoryDigest = resultHistoryDigest; + this.encodedFrameDigest = encodedFrameDigest; + this.diff = diff; + } + + public byte[] getFrame() { + return Arrays.copyOf(frame, frame.length); + } + + public byte[] getCanonicalPayload() { + return Arrays.copyOf(canonicalPayload, canonicalPayload.length); + } + + public byte[] getPayloadDigest() { + return Arrays.copyOf(payloadDigest, payloadDigest.length); + } + + public byte[] getBlockHistoryDigest() { + return Arrays.copyOf(blockHistoryDigest, blockHistoryDigest.length); + } + + public byte[] getResultHistoryDigest() { + return Arrays.copyOf(resultHistoryDigest, resultHistoryDigest.length); + } + + public byte[] getEncodedFrameDigest() { + return Arrays.copyOf(encodedFrameDigest, encodedFrameDigest.length); + } + + public BlockReverseDiff getDiff() { + return diff; + } + } + + public static final class DecodedBlock { + private final BlockReverseDiff diff; + private final byte[] canonicalPayload; + private final byte[] previousHistoryDigest; + private final byte[] payloadDigest; + private final byte[] blockHistoryDigest; + private final byte[] resultHistoryDigest; + private final byte[] encodedFrameDigest; + private final short compressionId; + + private DecodedBlock(BlockReverseDiff diff, byte[] canonicalPayload, + byte[] previousHistoryDigest, byte[] payloadDigest, byte[] blockHistoryDigest, + byte[] resultHistoryDigest, byte[] encodedFrameDigest, short compressionId) { + this.diff = diff; + this.canonicalPayload = canonicalPayload; + this.previousHistoryDigest = previousHistoryDigest; + this.payloadDigest = payloadDigest; + this.blockHistoryDigest = blockHistoryDigest; + this.resultHistoryDigest = resultHistoryDigest; + this.encodedFrameDigest = encodedFrameDigest; + this.compressionId = compressionId; + } + + public BlockReverseDiff getDiff() { + return diff; + } + + public byte[] getCanonicalPayload() { + return Arrays.copyOf(canonicalPayload, canonicalPayload.length); + } + + public byte[] getPreviousHistoryDigest() { + return Arrays.copyOf(previousHistoryDigest, previousHistoryDigest.length); + } + + public byte[] getPayloadDigest() { + return Arrays.copyOf(payloadDigest, payloadDigest.length); + } + + public byte[] getBlockHistoryDigest() { + return Arrays.copyOf(blockHistoryDigest, blockHistoryDigest.length); + } + + public byte[] getResultHistoryDigest() { + return Arrays.copyOf(resultHistoryDigest, resultHistoryDigest.length); + } + + public byte[] getEncodedFrameDigest() { + return Arrays.copyOf(encodedFrameDigest, encodedFrameDigest.length); + } + + public short getCompressionId() { + return compressionId; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java index 96c01df5a25..c1a744ed7ce 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializer.java @@ -494,6 +494,19 @@ private static TargetMarker loadTarget(Path path) throws IOException { } } + static Optional loadReadableTargetIfPresent(Path directory) + throws IOException { + Path readable = Objects.requireNonNull(directory, "directory").resolve(READABLE_FILE); + return Files.exists(readable, LinkOption.NOFOLLOW_LINKS) + ? Optional.of(loadTarget(readable).target) : Optional.empty(); + } + + static void publishReadableTarget(Path directory, CommonCheckpointTarget target) + throws IOException { + replace(Objects.requireNonNull(directory, "directory").resolve(READABLE_FILE), + encodeTarget(Objects.requireNonNull(target, "target"))); + } + private static void requireExact(Path path, byte[] expected) throws IOException { if (!Arrays.equals(loadTarget(path).encoded, expected)) { throw new IOException("State Archive checkpoint target identity differs"); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointPlanner.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointPlanner.java new file mode 100644 index 00000000000..c6c1f89eaed --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveCheckpointPlanner.java @@ -0,0 +1,16 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.util.List; +import org.tron.core.db2.core.CommonCheckpointCapture; +import org.tron.core.db2.core.CommonCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointTarget; + +/** Computes the transient Archive binding embedded in a Common checkpoint v2 payload. */ +public interface StateArchiveCheckpointPlanner extends CommonCheckpointMaterializer { + + StateArchiveHotBatchDescriptor planCheckpoint(List diffs) + throws IOException; + + CommonCheckpointTarget prepare(CommonCheckpointCapture capture) throws IOException; +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFileFormatV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFileFormatV3.java new file mode 100644 index 00000000000..76ed4ed5ab0 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFileFormatV3.java @@ -0,0 +1,464 @@ +package org.tron.core.db2.archive; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collection; + +/** Single source of truth for the default-off State Archive append-file v3 format. */ +public final class StateArchiveFileFormatV3 { + + public static final String FORMAT_ID = "archive-state/mixed-frame-segment/v3"; + public static final String FIVE_LANE_FORMAT_ID = + "archive-state/five-lane-frame-segment/v3"; + + public static final int FRAME_MAGIC = 0x53414633; + public static final int FRAME_TRAILER_MAGIC = 0x33464153; + public static final int PAYLOAD_MAGIC = 0x53425031; + public static final int PART_MAGIC = 0x53415033; + public static final int SEGMENT_MAGIC = 0x53415333; + public static final int BLOCK_INDEX_MAGIC = 0x53424933; + public static final int RECOVERY_INTENT_MAGIC = 0x53524933; + public static final int RECOVERY_INTENT_TRAILER_MAGIC = 0x33495253; + + public static final short MAJOR_VERSION = 3; + public static final short MINOR_VERSION = 0; + public static final short BLOCK_FRAME_TYPE = 1; + public static final short DURABLE_MARKER_FRAME_TYPE = 2; + public static final short PART_SEAL_FRAME_TYPE = 3; + + public static final short BODY_CODEC_ID = 1; + public static final short LANE_VARIABLE_BODY_CODEC_ID = 2; + public static final short DEDICATED_FIXED_WIDTH_BODY_CODEC_ID = 3; + public static final short COMPRESSION_NONE = 0; + public static final short COMPRESSION_RAW_DEFLATE_LEVEL_1 = 1; + public static final short KEY_ORDER_ID = 1; + public static final short DIGEST_ID = 1; + public static final short CHECKSUM_ID = 1; + public static final short IDENTITY_KIND = 1; + public static final short LANE_KIND = 1; + public static final short LANE_ID = 0; + public static final short SIZE_ROLLED_AFTER_TARGET_ROTATION_ID = 1; + public static final short SEGMENT_LAYOUT_ID = 2; + public static final short SEGMENT_OVERSHOOT_POLICY_ID = 1; + public static final short RECOVERY_INTENT_ACTION_SCHEMA_ID = 1; + public static final short RECOVERY_POINT_SCHEMA_ID = 1; + + public static final int FORMAT_DESCRIPTOR_LENGTH = 96; + public static final int FRAME_ENVELOPE_LENGTH = 32; + public static final int BLOCK_HEADER_LENGTH = 336; + public static final int MARKER_HEADER_LENGTH = 288; + public static final int SEAL_HEADER_LENGTH = 320; + public static final int FRAME_TRAILER_LENGTH = 48; + public static final int PART_HEADER_LENGTH = 512; + public static final int BLOCK_INDEX_HEADER_LENGTH = 128; + public static final int BLOCK_INDEX_ENTRY_LENGTH = 32; + public static final int MANIFEST_HEADER_LENGTH = 256; + public static final int MANIFEST_PART_RECORD_LENGTH = 160; + public static final int MANIFEST_TRAILER_LENGTH = 48; + public static final int SEGMENT_LAYOUT_DESCRIPTOR_LENGTH = 64; + public static final int SEGMENT_MAP_ENTRY_LENGTH = 192; + public static final int RECOVERY_INTENT_LAYOUT_DESCRIPTOR_LENGTH = 32; + public static final int RECOVERY_INTENT_HEADER_LENGTH = 768; + public static final int RECOVERY_INTENT_LANE_RECORD_LENGTH = 160; + public static final int RECOVERY_INTENT_TRAILER_LENGTH = 48; + public static final int RECOVERY_INTENT_TOTAL_LENGTH = 1_616; + public static final int PAYLOAD_HEADER_LENGTH = 32; + public static final int SECTION_HEADER_LENGTH = 80; + public static final int VARIABLE_INDEX_RECORD_BASE_LENGTH = 16; + + // Legacy U0 descriptor fields. They are not five-lane rotation rules. + static final int U0_SEGMENT_BLOCK_SPAN = 16_384; + static final long U0_PART_MAX_BYTES = 2_000_000_000L; + public static final int MAX_BLOCK_FRAME_BYTES = 67_108_864; + public static final long SEGMENT_TARGET_BYTES = 2_000_000_000L; + public static final int SHARD_MAX_SEGMENTS = 1_024; + public static final long EXACT_COVERAGE_BITMAP = 0x0000000007ffffffL; + public static final long MIXED_LANE_COVERAGE_BITMAP = 0x0000000007dfefe7L; + private static final int[] FIVE_LANE_ID_VALUES = {0, 4, 5, 13, 22}; + public static final int STORE_COUNT = 27; + public static final int HASH_LENGTH = 32; + + static final byte[] FORMAT_DOMAIN = ascii("TRON-STATE-ARCHIVE-FORMAT-V3\0"); + static final byte[] STORE_DESCRIPTOR_DOMAIN = + ascii("TRON-STATE-ARCHIVE-STORE-DESCRIPTOR-V3\0"); + static final byte[] FIVE_LANE_DESCRIPTOR_DOMAIN = + ascii("TRON-STATE-ARCHIVE-FIVE-LANE-DESCRIPTOR-V3\0"); + static final byte[] SECTION_DOMAIN = ascii("TRON-STATE-ARCHIVE-SECTION-V3\0"); + static final byte[] PAYLOAD_DOMAIN = ascii("TRON-STATE-ARCHIVE-PAYLOAD-V3\0"); + static final byte[] BLOCK_DOMAIN = ascii("TRON-STATE-ARCHIVE-BLOCK-V3\0"); + static final byte[] ROLLING_DOMAIN = ascii("TRON-STATE-ARCHIVE-ROLLING-V3\0"); + static final byte[] ENCODED_BLOCK_DOMAIN = + ascii("TRON-STATE-ARCHIVE-ENCODED-BLOCK-V3\0"); + static final byte[] FIXED_SECTION_DOMAIN = + ascii("TRON-STATE-ARCHIVE-FIXED-SECTION-V3\0"); + static final byte[] LANE_ITEM_DOMAIN = + ascii("TRON-STATE-ARCHIVE-LANE-ITEM-V3\0"); + static final byte[] BUNDLE_BLOCK_DOMAIN = + ascii("TRON-STATE-ARCHIVE-BUNDLE-BLOCK-V3\0"); + static final byte[] PART_HEADER_DOMAIN = + ascii("TRON-STATE-ARCHIVE-PART-HEADER-V3\0"); + static final byte[] SEGMENT_LAYOUT_DOMAIN = + ascii("TRON-STATE-ARCHIVE-SEGMENT-LAYOUT-V3\0"); + static final byte[] COMPOSITE_FORMAT_DOMAIN = + ascii("TRON-STATE-ARCHIVE-COMPOSITE-FORMAT-V3\0"); + static final byte[] SEGMENT_HEADER_DOMAIN = + ascii("TRON-STATE-ARCHIVE-SEGMENT-HEADER-V3\0"); + static final byte[] SEGMENT_CONTENT_DOMAIN = + ascii("TRON-STATE-ARCHIVE-SEGMENT-CONTENT-V3\0"); + static final byte[] ENCODED_SEGMENT_SEAL_DOMAIN = + ascii("TRON-STATE-ARCHIVE-ENCODED-SEGMENT-SEAL-V3\0"); + static final byte[] ENCODED_MARKER_DOMAIN = + ascii("TRON-STATE-ARCHIVE-ENCODED-MARKER-V3\0"); + static final byte[] SEGMENT_CHAIN_DOMAIN = + ascii("TRON-STATE-ARCHIVE-SEGMENT-CHAIN-V3\0"); + static final byte[] LANE_SEGMENT_BASELINE_DOMAIN = + ascii("TRON-STATE-ARCHIVE-LANE-SEGMENT-BASELINE-V3\0"); + static final byte[] BLOCK_INDEX_HEADER_DOMAIN = + ascii("TRON-STATE-ARCHIVE-BLOCK-INDEX-HEADER-V3\0"); + static final byte[] RECOVERY_INTENT_LAYOUT_DOMAIN = + ascii("TRON-STATE-ARCHIVE-RECOVERY-INTENT-LAYOUT-V3\0"); + static final byte[] RECOVERY_INTENT_RECORDS_DOMAIN = + ascii("TRON-STATE-ARCHIVE-RECOVERY-INTENT-RECORDS-V3\0"); + static final byte[] RECOVERY_INTENT_HEADER_DOMAIN = + ascii("TRON-STATE-ARCHIVE-RECOVERY-INTENT-HEADER-V3\0"); + static final byte[] RECOVERY_INTENT_DOMAIN = + ascii("TRON-STATE-ARCHIVE-RECOVERY-INTENT-V3\0"); + static final byte[] RECOVERY_DATA_PREFIX_DOMAIN = + ascii("TRON-STATE-ARCHIVE-RECOVERY-DATA-PREFIX-V3\0"); + static final byte[] RECOVERY_INDEX_FILE_DOMAIN = + ascii("TRON-STATE-ARCHIVE-RECOVERY-INDEX-FILE-V3\0"); + + private static final byte[] FORMAT_DESCRIPTOR = buildFormatDescriptor(); + private static final byte[] FORMAT_DIGEST = sha256(FORMAT_DOMAIN, FORMAT_DESCRIPTOR); + private static final byte[] STORE_DESCRIPTOR_DIGEST = buildStoreDescriptorDigest(); + private static final byte[] FIVE_LANE_DESCRIPTOR_DIGEST = + buildFiveLaneDescriptorDigest(); + private static final byte[] SEGMENT_LAYOUT_DESCRIPTOR = buildSegmentLayoutDescriptor(); + private static final byte[] SEGMENT_LAYOUT_DIGEST = sha256(SEGMENT_LAYOUT_DOMAIN, + SEGMENT_LAYOUT_DESCRIPTOR); + private static final byte[] RECOVERY_INTENT_LAYOUT_DESCRIPTOR = + buildRecoveryIntentLayoutDescriptor(); + private static final byte[] RECOVERY_INTENT_LAYOUT_DIGEST = sha256( + RECOVERY_INTENT_LAYOUT_DOMAIN, RECOVERY_INTENT_LAYOUT_DESCRIPTOR); + private static final byte[] COMPOSITE_FORMAT_DIGEST = sha256(COMPOSITE_FORMAT_DOMAIN, + FORMAT_DIGEST, SEGMENT_LAYOUT_DIGEST, RECOVERY_INTENT_LAYOUT_DIGEST); + + private StateArchiveFileFormatV3() { + } + + public static byte[] formatDescriptor() { + return Arrays.copyOf(FORMAT_DESCRIPTOR, FORMAT_DESCRIPTOR.length); + } + + public static byte[] formatDigest() { + return Arrays.copyOf(FORMAT_DIGEST, FORMAT_DIGEST.length); + } + + public static byte[] storeDescriptorDigest() { + return Arrays.copyOf(STORE_DESCRIPTOR_DIGEST, STORE_DESCRIPTOR_DIGEST.length); + } + + public static byte[] fiveLaneDescriptorDigest() { + return Arrays.copyOf(FIVE_LANE_DESCRIPTOR_DIGEST, + FIVE_LANE_DESCRIPTOR_DIGEST.length); + } + + public static byte[] segmentLayoutDescriptor() { + return Arrays.copyOf(SEGMENT_LAYOUT_DESCRIPTOR, SEGMENT_LAYOUT_DESCRIPTOR.length); + } + + public static byte[] segmentLayoutDigest() { + return Arrays.copyOf(SEGMENT_LAYOUT_DIGEST, SEGMENT_LAYOUT_DIGEST.length); + } + + public static byte[] recoveryIntentLayoutDescriptor() { + return Arrays.copyOf(RECOVERY_INTENT_LAYOUT_DESCRIPTOR, + RECOVERY_INTENT_LAYOUT_DESCRIPTOR.length); + } + + public static byte[] recoveryIntentLayoutDigest() { + return Arrays.copyOf(RECOVERY_INTENT_LAYOUT_DIGEST, + RECOVERY_INTENT_LAYOUT_DIGEST.length); + } + + public static byte[] compositeFormatDigest() { + return Arrays.copyOf(COMPOSITE_FORMAT_DIGEST, COMPOSITE_FORMAT_DIGEST.length); + } + + static int[] fiveLaneIds() { + return Arrays.copyOf(FIVE_LANE_ID_VALUES, FIVE_LANE_ID_VALUES.length); + } + + static int laneId(int storeId) { + requireStoreId(storeId); + switch (storeId) { + case 4: + case 5: + case 13: + case 22: + return storeId; + default: + return 0; + } + } + + static long laneCoverage(int laneId) { + if (laneId == 0) { + return MIXED_LANE_COVERAGE_BITMAP; + } + requireDedicatedLane(laneId); + return 1L << (laneId - 1); + } + + static short laneKind(int laneId) { + if (laneId == 0) { + return 1; + } + requireDedicatedLane(laneId); + return 2; + } + + static short laneBodyCodec(int laneId) { + if (laneId == 4 || laneId == 22) { + return DEDICATED_FIXED_WIDTH_BODY_CODEC_ID; + } + if (laneId == 0 || laneId == 5 || laneId == 13) { + return LANE_VARIABLE_BODY_CODEC_ID; + } + throw new IllegalArgumentException("Unknown State Archive lane ID: " + laneId); + } + + static int fixedKeyWidth(int laneId) { + if (laneId == 4) { + return 21; + } + if (laneId == 22) { + return 32; + } + return 0; + } + + static int storeId(String dbName) { + int storeId = ArchiveParticipantDescriptor.current().getStoreId(dbName); + if (storeId < 1 || storeId > STORE_COUNT) { + throw new IllegalArgumentException("Inactive archive Store: " + dbName); + } + return storeId; + } + + static String dbName(int storeId) { + String dbName = ArchiveParticipantDescriptor.current().getActiveDatabases().stream() + .filter(candidate -> ArchiveParticipantDescriptor.current().getStoreId(candidate) + == storeId) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown archive Store ID: " + storeId)); + return dbName; + } + + static void requireExactCapture(Collection dbNames) { + ArchiveParticipantDescriptor.current().requireExactParticipants(dbNames); + } + + static byte[] sha256(byte[]... values) { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException impossible) { + throw new IllegalStateException("SHA-256 is unavailable", impossible); + } + for (byte[] value : values) { + digest.update(value); + } + return digest.digest(); + } + + private static byte[] buildFormatDescriptor() { + ByteBuffer buffer = ByteBuffer.allocate(FORMAT_DESCRIPTOR_LENGTH); + buffer.putInt(0x464d5433); + buffer.putShort(MAJOR_VERSION); + buffer.putShort(MINOR_VERSION); + buffer.putInt(FORMAT_DESCRIPTOR_LENGTH); + buffer.putShort((short) BLOCK_HEADER_LENGTH); + buffer.putShort((short) MARKER_HEADER_LENGTH); + buffer.putShort((short) SEAL_HEADER_LENGTH); + buffer.putShort((short) FRAME_TRAILER_LENGTH); + buffer.putShort((short) PART_HEADER_LENGTH); + buffer.putShort((short) BLOCK_INDEX_HEADER_LENGTH); + buffer.putShort((short) BLOCK_INDEX_ENTRY_LENGTH); + buffer.putShort((short) MANIFEST_HEADER_LENGTH); + buffer.putShort((short) MANIFEST_PART_RECORD_LENGTH); + buffer.putShort((short) MANIFEST_TRAILER_LENGTH); + buffer.putShort((short) PAYLOAD_HEADER_LENGTH); + buffer.putShort((short) SECTION_HEADER_LENGTH); + buffer.putShort((short) VARIABLE_INDEX_RECORD_BASE_LENGTH); + buffer.putShort(LANE_KIND); + buffer.putShort(LANE_ID); + buffer.putShort(BODY_CODEC_ID); + buffer.putShort((short) 0x0003); + buffer.putShort(KEY_ORDER_ID); + buffer.putShort(DIGEST_ID); + buffer.putShort(CHECKSUM_ID); + buffer.putShort(IDENTITY_KIND); + buffer.putInt(U0_SEGMENT_BLOCK_SPAN); + buffer.putLong(U0_PART_MAX_BYTES); + buffer.putLong(MAX_BLOCK_FRAME_BYTES); + buffer.putLong(EXACT_COVERAGE_BITMAP); + buffer.put(new byte[14]); + if (buffer.hasRemaining()) { + throw new IllegalStateException("Invalid State Archive format descriptor length"); + } + return buffer.array(); + } + + private static byte[] buildStoreDescriptorDigest() { + ArchiveParticipantDescriptor descriptor = ArchiveParticipantDescriptor.current(); + int encodedLength = STORE_DESCRIPTOR_DOMAIN.length + HASH_LENGTH + Short.BYTES; + for (int storeId = 1; storeId <= STORE_COUNT; storeId++) { + encodedLength += 2 + 1 + 1 + 2 + + descriptorName(descriptor, storeId).getBytes(StandardCharsets.UTF_8).length; + } + ByteBuffer buffer = ByteBuffer.allocate(encodedLength); + buffer.put(STORE_DESCRIPTOR_DOMAIN); + buffer.put(FORMAT_DIGEST); + buffer.putShort((short) STORE_COUNT); + for (int storeId = 1; storeId <= STORE_COUNT; storeId++) { + byte[] name = descriptorName(descriptor, storeId).getBytes(StandardCharsets.UTF_8); + if (name.length > 0xffff) { + throw new IllegalStateException("Archive Store name is too long"); + } + buffer.putShort((short) storeId); + buffer.put((byte) 1); + buffer.put((byte) 0); + buffer.putShort((short) name.length); + buffer.put(name); + } + return sha256(buffer.array()); + } + + private static byte[] buildSegmentLayoutDescriptor() { + ByteBuffer buffer = ByteBuffer.allocate(SEGMENT_LAYOUT_DESCRIPTOR_LENGTH); + buffer.putInt(0x534c4433); + buffer.putShort(MAJOR_VERSION); + buffer.putShort(MINOR_VERSION); + buffer.putInt(SEGMENT_LAYOUT_DESCRIPTOR_LENGTH); + buffer.putShort(SEGMENT_LAYOUT_ID); + buffer.putShort((short) PART_HEADER_LENGTH); + buffer.putShort((short) SEAL_HEADER_LENGTH); + buffer.putShort((short) BLOCK_INDEX_HEADER_LENGTH); + buffer.putShort((short) BLOCK_INDEX_ENTRY_LENGTH); + buffer.putShort((short) MANIFEST_HEADER_LENGTH); + buffer.putShort((short) MANIFEST_TRAILER_LENGTH); + buffer.putShort((short) SEGMENT_MAP_ENTRY_LENGTH); + buffer.putLong(SEGMENT_TARGET_BYTES); + buffer.putLong(MAX_BLOCK_FRAME_BYTES); + buffer.putInt(SHARD_MAX_SEGMENTS); + buffer.putShort(SIZE_ROLLED_AFTER_TARGET_ROTATION_ID); + buffer.putShort(SEGMENT_OVERSHOOT_POLICY_ID); + buffer.put(new byte[12]); + if (buffer.hasRemaining()) { + throw new IllegalStateException("Invalid State Archive segment layout length"); + } + return buffer.array(); + } + + private static byte[] buildRecoveryIntentLayoutDescriptor() { + ByteBuffer buffer = ByteBuffer.allocate(RECOVERY_INTENT_LAYOUT_DESCRIPTOR_LENGTH); + buffer.putInt(0x52494433); + buffer.putShort(MAJOR_VERSION); + buffer.putShort(MINOR_VERSION); + buffer.putInt(RECOVERY_INTENT_LAYOUT_DESCRIPTOR_LENGTH); + buffer.putShort((short) RECOVERY_INTENT_HEADER_LENGTH); + buffer.putShort((short) RECOVERY_INTENT_LANE_RECORD_LENGTH); + buffer.putShort((short) FIVE_LANE_ID_VALUES.length); + buffer.putShort((short) RECOVERY_INTENT_TRAILER_LENGTH); + buffer.putInt(RECOVERY_INTENT_TOTAL_LENGTH); + buffer.putShort(RECOVERY_INTENT_ACTION_SCHEMA_ID); + buffer.putShort(DIGEST_ID); + buffer.putShort(CHECKSUM_ID); + buffer.putShort(RECOVERY_POINT_SCHEMA_ID); + if (buffer.hasRemaining()) { + throw new IllegalStateException("Invalid State Archive recovery intent layout length"); + } + return buffer.array(); + } + + private static byte[] buildFiveLaneDescriptorDigest() { + ArchiveParticipantDescriptor descriptor = ArchiveParticipantDescriptor.current(); + int encodedLength = FIVE_LANE_DESCRIPTOR_DOMAIN.length + HASH_LENGTH + + 3 * Short.BYTES + FIVE_LANE_ID_VALUES.length * 24 + Short.BYTES; + for (int storeId = 1; storeId <= STORE_COUNT; storeId++) { + encodedLength += 14 + + descriptorName(descriptor, storeId).getBytes(StandardCharsets.UTF_8).length; + } + ByteBuffer buffer = ByteBuffer.allocate(encodedLength); + buffer.put(FIVE_LANE_DESCRIPTOR_DOMAIN); + buffer.put(FORMAT_DIGEST); + buffer.putShort((short) 1); + buffer.putShort(SIZE_ROLLED_AFTER_TARGET_ROTATION_ID); + buffer.putShort((short) FIVE_LANE_ID_VALUES.length); + long union = 0; + for (int laneId : FIVE_LANE_ID_VALUES) { + long coverage = laneCoverage(laneId); + if ((union & coverage) != 0) { + throw new IllegalStateException("Overlapping State Archive lane coverage"); + } + union |= coverage; + buffer.putShort((short) laneId); + buffer.putShort(laneKind(laneId)); + buffer.putShort(laneBodyCodec(laneId)); + buffer.putShort((short) 0); + buffer.putLong(coverage); + buffer.putInt(fixedKeyWidth(laneId)); + buffer.putInt(0); + } + if (union != EXACT_COVERAGE_BITMAP) { + throw new IllegalStateException("Incomplete State Archive lane coverage"); + } + buffer.putShort((short) STORE_COUNT); + for (int storeId = 1; storeId <= STORE_COUNT; storeId++) { + int laneId = laneId(storeId); + int keyWidth = fixedKeyWidth(laneId); + byte[] name = descriptorName(descriptor, storeId).getBytes(StandardCharsets.UTF_8); + buffer.putShort((short) storeId); + buffer.put((byte) 1); + buffer.put((byte) 0); + buffer.putShort((short) laneId); + buffer.putShort((short) (keyWidth == 0 ? 1 : 2)); + buffer.putInt(keyWidth); + buffer.putShort((short) name.length); + buffer.put(name); + } + if (buffer.hasRemaining()) { + throw new IllegalStateException("Invalid five-lane descriptor length"); + } + return sha256(buffer.array()); + } + + private static void requireStoreId(int storeId) { + if (storeId < 1 || storeId > STORE_COUNT) { + throw new IllegalArgumentException("Unknown archive Store ID: " + storeId); + } + } + + private static void requireDedicatedLane(int laneId) { + if (laneId != 4 && laneId != 5 && laneId != 13 && laneId != 22) { + throw new IllegalArgumentException("Unknown State Archive lane ID: " + laneId); + } + } + + private static String descriptorName(ArchiveParticipantDescriptor descriptor, int storeId) { + for (String dbName : descriptor.getActiveDatabases()) { + if (descriptor.getStoreId(dbName) == storeId) { + return dbName; + } + } + throw new IllegalStateException("Missing active archive Store ID: " + storeId); + } + + static byte[] ascii(String value) { + return value.getBytes(StandardCharsets.US_ASCII); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneBlockCodecV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneBlockCodecV3.java new file mode 100644 index 00000000000..53961dc6751 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneBlockCodecV3.java @@ -0,0 +1,1128 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +/** Canonical codec for one complete State Archive v3 five-lane block bundle. */ +public final class StateArchiveFiveLaneBlockCodecV3 { + + private static final long UNSIGNED_INT_MAX = 0xffff_ffffL; + private static final int FIXED_SECTION_HEADER_LENGTH = 96; + private static final int TRAILER_CRC_AND_MAGIC_LENGTH = 8; + + /** Validates one exact capture before emitting any lane frame. */ + public EncodedBundle encode(BlockChangeView view, OldValueCollector collector, + byte[] previousHistoryDigest, short compressionId) { + Objects.requireNonNull(view, "view"); + Objects.requireNonNull(collector, "collector"); + List captured = new ArrayList<>(); + for (BlockChangeView.DatabaseChanges database : view.getDatabases()) { + captured.add(database.getDbName()); + } + StateArchiveFileFormatV3.requireExactCapture(captured); + BlockReverseDiff diff = Objects.requireNonNull(collector.collect(view), "collected diff"); + if (!view.getMeta().equals(diff.getMeta())) { + throw new IllegalArgumentException("Collected history block identity changed"); + } + return encode(diff, previousHistoryDigest, compressionId); + } + + /** Encodes an already collected reverse diff into exactly five lane frames. */ + EncodedBundle encode(BlockReverseDiff diff, byte[] previousHistoryDigest, + short compressionId) { + Objects.requireNonNull(diff, "diff"); + byte[] previousDigest = requireHash(previousHistoryDigest, "previousHistoryDigest"); + requireCompression(compressionId); + BlockSnapshotMeta meta = requireMeta(diff.getMeta()); + List groups = admitAndCanonicalize(diff.getGroups()); + + int[] laneIds = StateArchiveFileFormatV3.fiveLaneIds(); + List payloads = new ArrayList<>(laneIds.length); + for (int laneId : laneIds) { + payloads.add(encodeLanePayload(laneId, groups)); + } + byte[] descriptorDigest = StateArchiveFileFormatV3.fiveLaneDescriptorDigest(); + byte[] blockHistoryDigest = bundleBlockHistoryDigest(meta, descriptorDigest, payloads); + byte[] resultHistoryDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ROLLING_DOMAIN, previousDigest, blockHistoryDigest); + + List lanes = new ArrayList<>(payloads.size()); + for (LanePayload payload : payloads) { + lanes.add(encodeLaneFrame(meta, previousDigest, descriptorDigest, + blockHistoryDigest, resultHistoryDigest, payload, compressionId)); + } + return new EncodedBundle(lanes, blockHistoryDigest, resultHistoryDigest, diff); + } + + /** Decodes five frames and only returns after verifying the complete bundle. */ + public DecodedBundle decode(List frameBytes) { + Objects.requireNonNull(frameBytes, "frameBytes"); + int[] laneIds = StateArchiveFileFormatV3.fiveLaneIds(); + if (frameBytes.size() != laneIds.length) { + throw new IllegalArgumentException("State Archive bundle must contain exactly five frames"); + } + Map byLane = new HashMap<>(); + for (byte[] frame : frameBytes) { + DecodedLane lane = decodeLane(frame); + if (byLane.put(lane.laneId, lane) != null) { + throw new IllegalArgumentException("Duplicate State Archive lane: " + lane.laneId); + } + } + List ordered = new ArrayList<>(frameBytes.size()); + for (int laneId : laneIds) { + DecodedLane lane = byLane.get(laneId); + if (lane == null) { + throw new IllegalArgumentException("Missing State Archive lane: " + laneId); + } + ordered.add(lane); + } + DecodedLane first = ordered.get(0); + long coverage = 0; + List payloads = new ArrayList<>(ordered.size()); + List groups = new ArrayList<>(); + for (DecodedLane lane : ordered) { + requireMetaEquals(first, lane); + if ((coverage & lane.coverageBitmap) != 0) { + throw new IllegalArgumentException("Overlapping State Archive lane coverage"); + } + coverage |= lane.coverageBitmap; + payloads.add(lane.payload); + groups.addAll(lane.groups); + } + if (coverage != StateArchiveFileFormatV3.EXACT_COVERAGE_BITMAP) { + throw new IllegalArgumentException("Incomplete State Archive bundle coverage"); + } + byte[] actualBlockDigest = bundleBlockHistoryDigest(first.meta, + StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), payloads); + requireArray(first.blockHistoryDigest, actualBlockDigest, "bundle block digest"); + byte[] actualResultDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ROLLING_DOMAIN, first.previousHistoryDigest, + actualBlockDigest); + requireArray(first.resultHistoryDigest, actualResultDigest, "bundle result digest"); + groups.sort(Comparator.comparingInt(group -> + StateArchiveFileFormatV3.storeId(group.getDbName()))); + BlockReverseDiff diff = new BlockReverseDiff(first.meta, groups); + return new DecodedBundle(diff, ordered, actualBlockDigest, actualResultDigest); + } + + private List admitAndCanonicalize(Collection inputGroups) { + List groups = new ArrayList<>(); + Set storeIds = new HashSet<>(); + for (DbGroup group : inputGroups) { + int storeId = StateArchiveFileFormatV3.storeId(group.getDbName()); + if (!storeIds.add(storeId)) { + throw new IllegalArgumentException("Duplicate archive Store ID: " + storeId); + } + if (group.getEntries().isEmpty()) { + throw new IllegalArgumentException("Changed archive Store section must not be empty"); + } + int laneId = StateArchiveFileFormatV3.laneId(storeId); + int keyWidth = StateArchiveFileFormatV3.fixedKeyWidth(laneId); + byte[] previousKey = null; + for (Entry entry : group.getEntries()) { + byte[] key = entry.getKey(); + if (keyWidth != 0 && key.length != keyWidth) { + throw new IllegalArgumentException("Invalid fixed-width key for Store " + storeId); + } + if (previousKey != null && BlockReverseDiff.compareUnsigned(previousKey, key) >= 0) { + throw new IllegalArgumentException("Archive keys are not strictly sorted"); + } + previousKey = key; + } + groups.add(new StoreGroup(storeId, laneId, group.getEntries())); + } + groups.sort(Comparator.comparingInt(group -> group.storeId)); + return groups; + } + + private LanePayload encodeLanePayload(int laneId, List allGroups) { + List groups = new ArrayList<>(); + for (StoreGroup group : allGroups) { + if (group.laneId == laneId) { + groups.add(group); + } + } + short bodyCodec = StateArchiveFileFormatV3.laneBodyCodec(laneId); + if (bodyCodec == StateArchiveFileFormatV3.DEDICATED_FIXED_WIDTH_BODY_CODEC_ID + && groups.size() > 1) { + throw new IllegalArgumentException("Fixed-width lane has multiple Store sections"); + } + List sections = new ArrayList<>(groups.size()); + long entryCount = 0; + long sectionBytes = 0; + long changedBitmap = 0; + for (StoreGroup group : groups) { + byte[] section = bodyCodec == StateArchiveFileFormatV3.LANE_VARIABLE_BODY_CODEC_ID + ? encodeVariableSection(group) : encodeFixedSection(group); + sections.add(section); + entryCount = checkedAdd(entryCount, group.entries.size()); + sectionBytes = checkedAdd(sectionBytes, section.length); + changedBitmap |= 1L << (group.storeId - 1); + } + long payloadLength = checkedAdd(StateArchiveFileFormatV3.PAYLOAD_HEADER_LENGTH, + sectionBytes); + if (payloadLength > StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES) { + throw new IllegalArgumentException("State Archive lane payload is too large"); + } + ByteBuffer payload = ByteBuffer.allocate(checkedInt(payloadLength, "payloadLength")); + payload.putInt(StateArchiveFileFormatV3.PAYLOAD_MAGIC); + payload.putShort((short) 1); + payload.putShort((short) 0); + payload.putInt(StateArchiveFileFormatV3.PAYLOAD_HEADER_LENGTH); + payload.putInt(groups.size()); + payload.putLong(entryCount); + payload.putLong(sectionBytes); + for (byte[] section : sections) { + payload.put(section); + } + byte[] bytes = payload.array(); + byte[] payloadDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.PAYLOAD_DOMAIN, bytes); + return new LanePayload(laneId, StateArchiveFileFormatV3.laneKind(laneId), + bodyCodec, StateArchiveFileFormatV3.laneCoverage(laneId), changedBitmap, + groups.size(), entryCount, bytes, payloadDigest); + } + + private byte[] encodeVariableSection(StoreGroup group) { + int entryCount = group.entries.size(); + ByteBuffer offsets = ByteBuffer.allocate(checkedInt( + checkedMultiply(entryCount + 1L, Integer.BYTES), "offsetVectorLength")); + ByteArrayOutputStream records = new ByteArrayOutputStream(); + ByteArrayOutputStream values = new ByteArrayOutputStream(); + try (DataOutputStream output = new DataOutputStream(records)) { + for (Entry entry : group.entries) { + offsets.putInt(records.size()); + byte[] key = entry.getKey(); + output.writeInt(key.length); + writeValueLocator(output, values, entry.getOldValue()); + output.write(key); + } + output.flush(); + } catch (IOException impossible) { + throw new IllegalStateException("Unexpected variable section encoding failure", impossible); + } + offsets.putInt(records.size()); + byte[] offsetBytes = offsets.array(); + byte[] recordBytes = records.toByteArray(); + byte[] valueBytes = values.toByteArray(); + long sectionLength = checkedAdd(StateArchiveFileFormatV3.SECTION_HEADER_LENGTH, + offsetBytes.length, recordBytes.length, valueBytes.length); + byte[] digest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.SECTION_DOMAIN, + ByteBuffer.allocate(Short.BYTES + Integer.BYTES) + .putShort((short) group.storeId).putInt(entryCount).array(), + offsetBytes, recordBytes, valueBytes); + ByteBuffer section = ByteBuffer.allocate(checkedInt(sectionLength, "sectionLength")); + section.putShort((short) group.storeId); + section.putShort((short) 1); + section.putInt(StateArchiveFileFormatV3.SECTION_HEADER_LENGTH); + section.putInt(entryCount); + section.putInt(0); + section.putLong(offsetBytes.length); + section.putLong(recordBytes.length); + section.putLong(valueBytes.length); + section.putLong(sectionLength); + section.put(digest); + section.put(offsetBytes); + section.put(recordBytes); + section.put(valueBytes); + return section.array(); + } + + private byte[] encodeFixedSection(StoreGroup group) { + int keyWidth = StateArchiveFileFormatV3.fixedKeyWidth(group.laneId); + int count = group.entries.size(); + byte[] keys = new byte[checkedInt(checkedMultiply(count, keyWidth), "keyDataLength")]; + byte[] presence = new byte[(count + 7) / 8]; + ByteBuffer ends = ByteBuffer.allocate(checkedInt( + checkedMultiply(count + 1L, Integer.BYTES), "valueEndVectorLength")); + ByteArrayOutputStream values = new ByteArrayOutputStream(); + ends.putInt(0); + for (int index = 0; index < count; index++) { + Entry entry = group.entries.get(index); + byte[] key = entry.getKey(); + System.arraycopy(key, 0, keys, index * keyWidth, keyWidth); + if (entry.getOldValue().isPresent()) { + presence[index / 8] |= (byte) (1 << (index % 8)); + byte[] value = entry.getOldValue().getValue(); + values.write(value, 0, value.length); + } + ends.putInt(values.size()); + } + byte[] endBytes = ends.array(); + byte[] valueBytes = values.toByteArray(); + long sectionLength = checkedAdd(FIXED_SECTION_HEADER_LENGTH, keys.length, + presence.length, endBytes.length, valueBytes.length); + byte[] digest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.FIXED_SECTION_DOMAIN, + ByteBuffer.allocate(Short.BYTES + 2 * Integer.BYTES) + .putShort((short) group.storeId).putInt(count).putInt(keyWidth).array(), + keys, presence, endBytes, valueBytes); + ByteBuffer section = ByteBuffer.allocate(checkedInt(sectionLength, "sectionLength")); + section.putShort((short) group.storeId); + section.putShort((short) 2); + section.putInt(FIXED_SECTION_HEADER_LENGTH); + section.putInt(count); + section.putInt(0); + section.putInt(keyWidth); + section.putInt(0); + section.putLong(keys.length); + section.putLong(presence.length); + section.putLong(endBytes.length); + section.putLong(valueBytes.length); + section.putLong(sectionLength); + section.put(digest); + section.put(keys); + section.put(presence); + section.put(endBytes); + section.put(valueBytes); + return section.array(); + } + + private void writeValueLocator(DataOutputStream output, ByteArrayOutputStream values, + OldValue oldValue) throws IOException { + output.writeByte(oldValue.isPresent() ? 1 : 0); + output.write(new byte[3]); + if (oldValue.isPresent()) { + byte[] value = oldValue.getValue(); + output.writeInt(values.size()); + output.writeInt(value.length); + values.write(value); + } else { + output.writeInt(-1); + output.writeInt(-1); + } + } + + private EncodedLane encodeLaneFrame(BlockSnapshotMeta meta, byte[] previousDigest, + byte[] descriptorDigest, byte[] blockHistoryDigest, byte[] resultHistoryDigest, + LanePayload payload, short compressionId) { + byte[] storedPayload = compressionId == StateArchiveFileFormatV3.COMPRESSION_NONE + ? payload.bytes : deflate(payload.bytes); + long totalLength = checkedAdd(StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH, + storedPayload.length, StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH); + if (totalLength > StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES) { + throw new IllegalArgumentException("State Archive lane frame exceeds 64 MiB"); + } + ByteBuffer frame = ByteBuffer.allocate((int) totalLength); + putEnvelope(frame, totalLength, storedPayload.length); + frame.putLong(meta.getEpoch()); + frame.putLong(meta.getBlockNumber()); + frame.putLong(meta.getTimestamp()); + frame.put(meta.getBlockHash()); + frame.put(meta.getParentHash()); + frame.put(descriptorDigest); + frame.put(previousDigest); + frame.put(payload.payloadDigest); + frame.put(blockHistoryDigest); + frame.put(resultHistoryDigest); + frame.putLong(payload.coverageBitmap); + frame.putLong(payload.changedBitmap); + frame.putInt(payload.groupCount); + frame.putInt(0); + frame.putLong(payload.entryCount); + frame.putLong(payload.bytes.length); + frame.putShort(payload.bodyCodec); + frame.putShort(compressionId); + frame.putShort(StateArchiveFileFormatV3.KEY_ORDER_ID); + frame.putShort(StateArchiveFileFormatV3.DIGEST_ID); + frame.putShort(StateArchiveFileFormatV3.CHECKSUM_ID); + frame.putShort(StateArchiveFileFormatV3.IDENTITY_KIND); + frame.putInt(0); + if (frame.position() != StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH) { + throw new IllegalStateException("Invalid State Archive block header length"); + } + frame.put(storedPayload); + byte[] encodedDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ENCODED_BLOCK_DOMAIN, + Arrays.copyOf(frame.array(), frame.position())); + frame.put(encodedDigest); + frame.putLong(totalLength); + int crcLength = frame.position(); + frame.putInt(crc32c(frame.array(), 0, crcLength)); + frame.putInt(StateArchiveFileFormatV3.FRAME_TRAILER_MAGIC); + return new EncodedLane(payload.laneId, payload.bodyCodec, payload.coverageBitmap, + payload.bytes, payload.payloadDigest, encodedDigest, frame.array()); + } + + private DecodedLane decodeLane(byte[] frameBytes) { + Objects.requireNonNull(frameBytes, "frameBytes"); + if (frameBytes.length < StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH + || frameBytes.length > StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES) { + throw new IllegalArgumentException("State Archive lane frame length is invalid"); + } + ByteBuffer frame = ByteBuffer.wrap(frameBytes); + requireInt(frame, StateArchiveFileFormatV3.FRAME_MAGIC, "frame magic"); + requireShort(frame, StateArchiveFileFormatV3.MAJOR_VERSION, "major version"); + requireShort(frame, StateArchiveFileFormatV3.MINOR_VERSION, "minor version"); + requireShort(frame, StateArchiveFileFormatV3.BLOCK_FRAME_TYPE, "frame type"); + requireShort(frame, (short) 0, "frame flags"); + requireInt(frame, StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH, "header length"); + long totalLength = frame.getLong(); + long payloadLength = frame.getLong(); + if (totalLength != frameBytes.length || payloadLength < 0 + || checkedAdd(StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH, payloadLength, + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH) != totalLength) { + throw new IllegalArgumentException("State Archive lane envelope length mismatch"); + } + long epoch = requireNonNegative(frame.getLong(), "epoch"); + long blockNumber = requireNonNegative(frame.getLong(), "blockNumber"); + long timestamp = requireNonNegative(frame.getLong(), "timestamp"); + if (epoch != blockNumber) { + throw new IllegalArgumentException("v3 requires epoch to equal blockNumber"); + } + byte[] blockHash = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + byte[] parentHash = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + byte[] descriptorDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + requireArray(descriptorDigest, StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + "five-lane descriptor digest"); + byte[] previousHistoryDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + byte[] payloadDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + byte[] blockHistoryDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + byte[] resultHistoryDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + long coverageBitmap = frame.getLong(); + int laneId = laneIdForCoverage(coverageBitmap); + long changedBitmap = frame.getLong(); + int groupCount = requireNonNegative(frame.getInt(), "groupCount"); + requireInt(frame, 0, "block reserved field"); + long entryCount = requireNonNegative(frame.getLong(), "entryCount"); + long rawPayloadLength = requireNonNegative(frame.getLong(), "rawPayloadLength"); + short bodyCodec = frame.getShort(); + requireShortValue(bodyCodec, StateArchiveFileFormatV3.laneBodyCodec(laneId), + "body codec"); + short compressionId = frame.getShort(); + requireCompression(compressionId); + requireShort(frame, StateArchiveFileFormatV3.KEY_ORDER_ID, "key order"); + requireShort(frame, StateArchiveFileFormatV3.DIGEST_ID, "digest algorithm"); + requireShort(frame, StateArchiveFileFormatV3.CHECKSUM_ID, "checksum algorithm"); + requireShort(frame, StateArchiveFileFormatV3.IDENTITY_KIND, "identity kind"); + requireInt(frame, 0, "block reserved tail"); + if ((changedBitmap & ~coverageBitmap) != 0 + || groupCount != Long.bitCount(changedBitmap) + || rawPayloadLength > StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES) { + throw new IllegalArgumentException("State Archive lane coverage is invalid"); + } + byte[] storedPayload = getBytes(frame, checkedInt(payloadLength, "payloadLength")); + int trailerStart = frame.position(); + byte[] encodedFrameDigest = getBytes(frame, StateArchiveFileFormatV3.HASH_LENGTH); + if (frame.getLong() != totalLength) { + throw new IllegalArgumentException("State Archive repeated frame length mismatch"); + } + int expectedCrc = frame.getInt(); + requireInt(frame, StateArchiveFileFormatV3.FRAME_TRAILER_MAGIC, "trailer magic"); + if (expectedCrc != crc32c(frameBytes, 0, + frameBytes.length - TRAILER_CRC_AND_MAGIC_LENGTH)) { + throw new IllegalArgumentException("State Archive lane frame checksum mismatch"); + } + byte[] actualEncodedDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ENCODED_BLOCK_DOMAIN, + Arrays.copyOf(frameBytes, trailerStart)); + requireArray(encodedFrameDigest, actualEncodedDigest, "encoded frame digest"); + byte[] rawPayload = compressionId == StateArchiveFileFormatV3.COMPRESSION_NONE + ? storedPayload : inflate(storedPayload, + checkedInt(rawPayloadLength, "rawPayloadLength")); + if (rawPayload.length != rawPayloadLength) { + throw new IllegalArgumentException("State Archive raw payload length mismatch"); + } + requireArray(payloadDigest, StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.PAYLOAD_DOMAIN, rawPayload), "payload digest"); + DecodedPayload decoded = decodePayload(laneId, bodyCodec, rawPayload, + groupCount, entryCount, changedBitmap); + LanePayload payload = new LanePayload(laneId, + StateArchiveFileFormatV3.laneKind(laneId), bodyCodec, coverageBitmap, + changedBitmap, groupCount, entryCount, rawPayload, payloadDigest); + return new DecodedLane(laneId, new BlockSnapshotMeta(epoch, blockNumber, + blockHash, parentHash, timestamp), previousHistoryDigest, blockHistoryDigest, + resultHistoryDigest, payload, decoded.groups, compressionId, + encodedFrameDigest); + } + + private DecodedPayload decodePayload(int laneId, short bodyCodec, byte[] payloadBytes, + int expectedGroupCount, long expectedEntryCount, long expectedChangedBitmap) { + ByteBuffer payload = ByteBuffer.wrap(payloadBytes); + requireRemaining(payload, StateArchiveFileFormatV3.PAYLOAD_HEADER_LENGTH, + "payload header"); + requireInt(payload, StateArchiveFileFormatV3.PAYLOAD_MAGIC, "payload magic"); + requireShort(payload, (short) 1, "payload version"); + requireShort(payload, (short) 0, "payload flags"); + requireInt(payload, StateArchiveFileFormatV3.PAYLOAD_HEADER_LENGTH, + "payload header length"); + int groupCount = requireNonNegative(payload.getInt(), "payload section count"); + long entryCount = requireNonNegative(payload.getLong(), "payload entry count"); + long sectionBytes = requireNonNegative(payload.getLong(), "payload section bytes"); + if (groupCount != expectedGroupCount || entryCount != expectedEntryCount + || sectionBytes != payload.remaining()) { + throw new IllegalArgumentException("State Archive payload counts or length mismatch"); + } + if (bodyCodec == StateArchiveFileFormatV3.DEDICATED_FIXED_WIDTH_BODY_CODEC_ID + && groupCount > 1) { + throw new IllegalArgumentException("Fixed-width lane has multiple sections"); + } + List groups = new ArrayList<>(groupCount); + int previousStoreId = 0; + long actualEntryCount = 0; + long actualChangedBitmap = 0; + for (int index = 0; index < groupCount; index++) { + int sectionStart = payload.position(); + requireRemaining(payload, Short.BYTES, "Store ID"); + int storeId = Short.toUnsignedInt(payload.getShort()); + if (storeId <= previousStoreId || StateArchiveFileFormatV3.laneId(storeId) != laneId) { + throw new IllegalArgumentException("State Archive Store section lane is invalid"); + } + previousStoreId = storeId; + List entries = bodyCodec == StateArchiveFileFormatV3.LANE_VARIABLE_BODY_CODEC_ID + ? decodeVariableSection(payloadBytes, payload, sectionStart, storeId) + : decodeFixedSection(payloadBytes, payload, sectionStart, storeId, laneId); + groups.add(new DbGroup(StateArchiveFileFormatV3.dbName(storeId), entries)); + actualEntryCount = checkedAdd(actualEntryCount, entries.size()); + actualChangedBitmap |= 1L << (storeId - 1); + } + if (payload.hasRemaining() || actualEntryCount != entryCount + || actualChangedBitmap != expectedChangedBitmap) { + throw new IllegalArgumentException("State Archive lane payload coverage mismatch"); + } + return new DecodedPayload(groups); + } + + private List decodeVariableSection(byte[] payloadBytes, ByteBuffer payload, + int sectionStart, int storeId) { + requireRemaining(payload, StateArchiveFileFormatV3.SECTION_HEADER_LENGTH - Short.BYTES, + "variable Store section header"); + requireShort(payload, (short) 1, "section version"); + requireInt(payload, StateArchiveFileFormatV3.SECTION_HEADER_LENGTH, + "section header length"); + int count = requirePositive(payload.getInt(), "section entry count"); + requireInt(payload, 0, "section flags"); + long offsetLength = payload.getLong(); + long recordLength = payload.getLong(); + long valueLength = payload.getLong(); + long sectionLength = payload.getLong(); + byte[] sectionDigest = getBytes(payload, StateArchiveFileFormatV3.HASH_LENGTH); + long expectedOffsetLength = checkedMultiply(count + 1L, Integer.BYTES); + requireSectionLength(payloadBytes, sectionStart, sectionLength, + StateArchiveFileFormatV3.SECTION_HEADER_LENGTH, offsetLength, recordLength, + valueLength); + if (offsetLength != expectedOffsetLength) { + throw new IllegalArgumentException("State Archive record offset length is invalid"); + } + byte[] offsets = getBytes(payload, checkedInt(offsetLength, "offsetLength")); + byte[] records = getBytes(payload, checkedInt(recordLength, "recordLength")); + byte[] values = getBytes(payload, checkedInt(valueLength, "valueLength")); + byte[] actualDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.SECTION_DOMAIN, + ByteBuffer.allocate(Short.BYTES + Integer.BYTES) + .putShort((short) storeId).putInt(count).array(), offsets, records, values); + requireArray(sectionDigest, actualDigest, "section digest"); + return decodeVariableEntries(count, offsets, records, values); + } + + private List decodeFixedSection(byte[] payloadBytes, ByteBuffer payload, + int sectionStart, int storeId, int laneId) { + requireRemaining(payload, FIXED_SECTION_HEADER_LENGTH - Short.BYTES, + "fixed Store section header"); + requireShort(payload, (short) 2, "fixed section version"); + requireInt(payload, FIXED_SECTION_HEADER_LENGTH, "fixed section header length"); + int count = requirePositive(payload.getInt(), "fixed section entry count"); + requireInt(payload, 0, "fixed section flags"); + int keyWidth = payload.getInt(); + requireInt(payload, 0, "fixed section reserved field"); + long keyLength = payload.getLong(); + long presenceLength = payload.getLong(); + long endsLength = payload.getLong(); + long valueLength = payload.getLong(); + long sectionLength = payload.getLong(); + byte[] sectionDigest = getBytes(payload, StateArchiveFileFormatV3.HASH_LENGTH); + int expectedKeyWidth = StateArchiveFileFormatV3.fixedKeyWidth(laneId); + if (storeId != laneId || keyWidth != expectedKeyWidth + || keyLength != checkedMultiply(count, keyWidth) + || presenceLength != (count + 7L) / 8 + || endsLength != checkedMultiply(count + 1L, Integer.BYTES)) { + throw new IllegalArgumentException("State Archive fixed section shape is invalid"); + } + requireSectionLength(payloadBytes, sectionStart, sectionLength, + FIXED_SECTION_HEADER_LENGTH, keyLength, presenceLength, endsLength, valueLength); + byte[] keys = getBytes(payload, checkedInt(keyLength, "keyLength")); + byte[] presence = getBytes(payload, checkedInt(presenceLength, "presenceLength")); + byte[] ends = getBytes(payload, checkedInt(endsLength, "endsLength")); + byte[] values = getBytes(payload, checkedInt(valueLength, "valueLength")); + byte[] actualDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.FIXED_SECTION_DOMAIN, + ByteBuffer.allocate(Short.BYTES + 2 * Integer.BYTES) + .putShort((short) storeId).putInt(count).putInt(keyWidth).array(), + keys, presence, ends, values); + requireArray(sectionDigest, actualDigest, "fixed section digest"); + return decodeFixedEntries(count, keyWidth, keys, presence, ends, values); + } + + private List decodeVariableEntries(int count, byte[] offsetBytes, + byte[] recordBytes, byte[] valueBytes) { + ByteBuffer offsets = ByteBuffer.wrap(offsetBytes); + int[] positions = new int[count + 1]; + for (int index = 0; index <= count; index++) { + long offset = Integer.toUnsignedLong(offsets.getInt()); + if (offset > recordBytes.length || (index == 0 && offset != 0) + || (index > 0 && offset <= positions[index - 1])) { + throw new IllegalArgumentException("State Archive record offsets are invalid"); + } + positions[index] = (int) offset; + } + if (positions[count] != recordBytes.length) { + throw new IllegalArgumentException("State Archive final record offset is invalid"); + } + List entries = new ArrayList<>(count); + byte[] previousKey = null; + int valueCursor = 0; + for (int index = 0; index < count; index++) { + int recordLength = positions[index + 1] - positions[index]; + if (recordLength < StateArchiveFileFormatV3.VARIABLE_INDEX_RECORD_BASE_LENGTH) { + throw new IllegalArgumentException("State Archive index record is truncated"); + } + ByteBuffer record = ByteBuffer.wrap(recordBytes, positions[index], recordLength).slice(); + long keyLength = Integer.toUnsignedLong(record.getInt()); + int tag = Byte.toUnsignedInt(record.get()); + requireZero(record, 3, "index record reserved bytes"); + long valueOffset = Integer.toUnsignedLong(record.getInt()); + long valueLength = Integer.toUnsignedLong(record.getInt()); + if (keyLength != record.remaining()) { + throw new IllegalArgumentException("State Archive key length is invalid"); + } + byte[] key = getBytes(record, checkedInt(keyLength, "keyLength")); + requireOrdered(previousKey, key); + previousKey = key; + ValueRead valueRead = readValue(tag, valueOffset, valueLength, + valueCursor, valueBytes); + valueCursor = valueRead.nextCursor; + entries.add(new Entry(key, valueRead.oldValue)); + } + if (valueCursor != valueBytes.length) { + throw new IllegalArgumentException("State Archive value data has an unreferenced tail"); + } + return entries; + } + + private List decodeFixedEntries(int count, int keyWidth, byte[] keys, + byte[] presence, byte[] endBytes, byte[] values) { + int usedBits = count % 8; + if (usedBits != 0 && (Byte.toUnsignedInt(presence[presence.length - 1]) + & ~((1 << usedBits) - 1)) != 0) { + throw new IllegalArgumentException("State Archive presence bitmap has non-zero tail bits"); + } + ByteBuffer ends = ByteBuffer.wrap(endBytes); + long first = Integer.toUnsignedLong(ends.getInt()); + if (first != 0) { + throw new IllegalArgumentException("State Archive first value end is invalid"); + } + List entries = new ArrayList<>(count); + byte[] previousKey = null; + int valueCursor = 0; + for (int index = 0; index < count; index++) { + long next = Integer.toUnsignedLong(ends.getInt()); + if (next < valueCursor || next > values.length) { + throw new IllegalArgumentException("State Archive value ends are invalid"); + } + byte[] key = Arrays.copyOfRange(keys, index * keyWidth, (index + 1) * keyWidth); + requireOrdered(previousKey, key); + previousKey = key; + boolean present = (presence[index / 8] & (1 << (index % 8))) != 0; + if (!present && next != valueCursor) { + throw new IllegalArgumentException("State Archive absent fixed value has bytes"); + } + OldValue oldValue = present + ? OldValue.present(Arrays.copyOfRange(values, valueCursor, (int) next)) + : OldValue.absent(); + valueCursor = (int) next; + entries.add(new Entry(key, oldValue)); + } + if (valueCursor != values.length) { + throw new IllegalArgumentException("State Archive fixed value data has a tail"); + } + return entries; + } + + private ValueRead readValue(int tag, long valueOffset, long valueLength, + int valueCursor, byte[] values) { + if (tag == 0) { + if (valueOffset != UNSIGNED_INT_MAX || valueLength != UNSIGNED_INT_MAX) { + throw new IllegalArgumentException("State Archive ABSENT value locator is invalid"); + } + return new ValueRead(OldValue.absent(), valueCursor); + } + if (tag != 1 || valueOffset != valueCursor + || valueLength > values.length - valueCursor) { + throw new IllegalArgumentException("State Archive PRESENT value locator is invalid"); + } + int next = valueCursor + (int) valueLength; + return new ValueRead(OldValue.present(Arrays.copyOfRange(values, valueCursor, next)), next); + } + + private byte[] bundleBlockHistoryDigest(BlockSnapshotMeta meta, + byte[] descriptorDigest, List payloads) { + ByteBuffer identity = ByteBuffer.allocate(3 * Long.BYTES + + 3 * StateArchiveFileFormatV3.HASH_LENGTH + Short.BYTES + + payloads.size() * StateArchiveFileFormatV3.HASH_LENGTH); + identity.putLong(meta.getEpoch()); + identity.putLong(meta.getBlockNumber()); + identity.putLong(meta.getTimestamp()); + identity.put(meta.getBlockHash()); + identity.put(meta.getParentHash()); + identity.put(descriptorDigest); + identity.putShort((short) payloads.size()); + int previousLane = -1; + for (LanePayload payload : payloads) { + if (payload.laneId <= previousLane) { + throw new IllegalArgumentException("State Archive lane items are not ordered"); + } + previousLane = payload.laneId; + identity.put(laneItemDigest(payload)); + } + return StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.BUNDLE_BLOCK_DOMAIN, identity.array()); + } + + private byte[] laneItemDigest(LanePayload payload) { + ByteBuffer identity = ByteBuffer.allocate(3 * Short.BYTES + 2 * Long.BYTES + + Integer.BYTES + 2 * Long.BYTES + StateArchiveFileFormatV3.HASH_LENGTH); + identity.putShort(payload.laneKind); + identity.putShort((short) payload.laneId); + identity.putShort(payload.bodyCodec); + identity.putLong(payload.coverageBitmap); + identity.putLong(payload.changedBitmap); + identity.putInt(payload.groupCount); + identity.putLong(payload.entryCount); + identity.putLong(payload.bytes.length); + identity.put(payload.payloadDigest); + return StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.LANE_ITEM_DOMAIN, identity.array()); + } + + private void requireMetaEquals(DecodedLane expected, DecodedLane actual) { + if (!expected.meta.equals(actual.meta) + || !Arrays.equals(expected.previousHistoryDigest, actual.previousHistoryDigest) + || !Arrays.equals(expected.blockHistoryDigest, actual.blockHistoryDigest) + || !Arrays.equals(expected.resultHistoryDigest, actual.resultHistoryDigest)) { + throw new IllegalArgumentException("State Archive lane bundle identity mismatch"); + } + } + + private int laneIdForCoverage(long coverage) { + for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { + if (coverage == StateArchiveFileFormatV3.laneCoverage(laneId)) { + return laneId; + } + } + throw new IllegalArgumentException("Unknown State Archive lane coverage"); + } + + private BlockSnapshotMeta requireMeta(BlockSnapshotMeta meta) { + if (meta.getEpoch() != meta.getBlockNumber()) { + throw new IllegalArgumentException("v3 requires epoch to equal blockNumber"); + } + if (meta.getTimestamp() < 0) { + throw new IllegalArgumentException("v3 timestamp must not be negative"); + } + return meta; + } + + private void putEnvelope(ByteBuffer frame, long totalLength, long payloadLength) { + frame.putInt(StateArchiveFileFormatV3.FRAME_MAGIC); + frame.putShort(StateArchiveFileFormatV3.MAJOR_VERSION); + frame.putShort(StateArchiveFileFormatV3.MINOR_VERSION); + frame.putShort(StateArchiveFileFormatV3.BLOCK_FRAME_TYPE); + frame.putShort((short) 0); + frame.putInt(StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH); + frame.putLong(totalLength); + frame.putLong(payloadLength); + } + + private byte[] deflate(byte[] input) { + Deflater deflater = new Deflater(1, true); + deflater.setStrategy(Deflater.DEFAULT_STRATEGY); + deflater.setInput(input); + deflater.finish(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); + if (count == 0 && deflater.needsInput()) { + throw new IllegalStateException("Unexpected raw DEFLATE termination"); + } + output.write(buffer, 0, count); + } + deflater.end(); + return output.toByteArray(); + } + + private byte[] inflate(byte[] input, int expectedLength) { + Inflater inflater = new Inflater(true); + inflater.setInput(input); + byte[] output = new byte[expectedLength]; + try { + int count = inflater.inflate(output); + if (count != expectedLength || !inflater.finished() || inflater.getRemaining() != 0) { + throw new IllegalArgumentException("State Archive compressed payload length mismatch"); + } + return output; + } catch (DataFormatException e) { + throw new IllegalArgumentException("Invalid State Archive compressed payload", e); + } finally { + inflater.end(); + } + } + + private void requireSectionLength(byte[] payload, int start, long sectionLength, + long... components) { + if (sectionLength != checkedAdd(components) || sectionLength > payload.length - start) { + throw new IllegalArgumentException("State Archive Store section length is invalid"); + } + } + + private static void requireOrdered(byte[] previous, byte[] key) { + if (previous != null && BlockReverseDiff.compareUnsigned(previous, key) >= 0) { + throw new IllegalArgumentException("State Archive decoded keys are not ordered"); + } + } + + private static byte[] requireHash(byte[] value, String name) { + Objects.requireNonNull(value, name); + if (value.length != StateArchiveFileFormatV3.HASH_LENGTH) { + throw new IllegalArgumentException(name + " must be exactly 32 bytes"); + } + return Arrays.copyOf(value, value.length); + } + + private static void requireCompression(short compressionId) { + if (compressionId != StateArchiveFileFormatV3.COMPRESSION_NONE + && compressionId != StateArchiveFileFormatV3.COMPRESSION_RAW_DEFLATE_LEVEL_1) { + throw new IllegalArgumentException("Unsupported State Archive compression: " + + compressionId); + } + } + + private static long requireNonNegative(long value, String name) { + if (value < 0) { + throw new IllegalArgumentException("State Archive " + name + " is negative"); + } + return value; + } + + private static int requireNonNegative(int value, String name) { + if (value < 0) { + throw new IllegalArgumentException("State Archive " + name + " is negative"); + } + return value; + } + + private static int requirePositive(int value, String name) { + if (value <= 0) { + throw new IllegalArgumentException("State Archive " + name + " is not positive"); + } + return value; + } + + private static void requireInt(ByteBuffer buffer, int expected, String name) { + requireRemaining(buffer, Integer.BYTES, name); + if (buffer.getInt() != expected) { + throw new IllegalArgumentException("Invalid State Archive " + name); + } + } + + private static void requireShort(ByteBuffer buffer, short expected, String name) { + requireRemaining(buffer, Short.BYTES, name); + requireShortValue(buffer.getShort(), expected, name); + } + + private static void requireShortValue(short actual, short expected, String name) { + if (actual != expected) { + throw new IllegalArgumentException("Invalid State Archive " + name); + } + } + + private static void requireZero(ByteBuffer buffer, int length, String name) { + requireRemaining(buffer, length, name); + for (int index = 0; index < length; index++) { + if (buffer.get() != 0) { + throw new IllegalArgumentException("Invalid State Archive " + name); + } + } + } + + private static void requireRemaining(ByteBuffer buffer, int length, String name) { + if (length < 0 || buffer.remaining() < length) { + throw new IllegalArgumentException("Truncated State Archive " + name); + } + } + + private static byte[] getBytes(ByteBuffer buffer, int length) { + requireRemaining(buffer, length, "byte field"); + byte[] bytes = new byte[length]; + buffer.get(bytes); + return bytes; + } + + private static void requireArray(byte[] actual, byte[] expected, String name) { + if (!Arrays.equals(actual, expected)) { + throw new IllegalArgumentException("State Archive " + name + " mismatch"); + } + } + + private static int crc32c(byte[] bytes, int offset, int length) { + return Hashing.crc32c().hashBytes(bytes, offset, length).asInt(); + } + + private static long checkedAdd(long... values) { + long result = 0; + for (long value : values) { + if (value < 0 || Long.MAX_VALUE - result < value) { + throw new IllegalArgumentException("State Archive length overflow"); + } + result += value; + } + return result; + } + + private static long checkedMultiply(long left, long right) { + if (left < 0 || right < 0 || (left != 0 && right > Long.MAX_VALUE / left)) { + throw new IllegalArgumentException("State Archive length overflow"); + } + return left * right; + } + + private static int checkedInt(long value, String name) { + if (value < 0 || value > Integer.MAX_VALUE) { + throw new IllegalArgumentException("State Archive " + name + " exceeds int range"); + } + return (int) value; + } + + private static final class StoreGroup { + private final int storeId; + private final int laneId; + private final List entries; + + private StoreGroup(int storeId, int laneId, List entries) { + this.storeId = storeId; + this.laneId = laneId; + this.entries = entries; + } + } + + private static final class LanePayload { + private final int laneId; + private final short laneKind; + private final short bodyCodec; + private final long coverageBitmap; + private final long changedBitmap; + private final int groupCount; + private final long entryCount; + private final byte[] bytes; + private final byte[] payloadDigest; + + private LanePayload(int laneId, short laneKind, short bodyCodec, + long coverageBitmap, long changedBitmap, int groupCount, long entryCount, + byte[] bytes, byte[] payloadDigest) { + this.laneId = laneId; + this.laneKind = laneKind; + this.bodyCodec = bodyCodec; + this.coverageBitmap = coverageBitmap; + this.changedBitmap = changedBitmap; + this.groupCount = groupCount; + this.entryCount = entryCount; + this.bytes = bytes; + this.payloadDigest = payloadDigest; + } + } + + private static final class DecodedPayload { + private final List groups; + + private DecodedPayload(List groups) { + this.groups = groups; + } + } + + private static final class ValueRead { + private final OldValue oldValue; + private final int nextCursor; + + private ValueRead(OldValue oldValue, int nextCursor) { + this.oldValue = oldValue; + this.nextCursor = nextCursor; + } + } + + public static final class EncodedBundle { + private final List lanes; + private final byte[] blockHistoryDigest; + private final byte[] resultHistoryDigest; + private final BlockReverseDiff diff; + + private EncodedBundle(List lanes, byte[] blockHistoryDigest, + byte[] resultHistoryDigest, BlockReverseDiff diff) { + this.lanes = java.util.Collections.unmodifiableList(new ArrayList<>(lanes)); + this.blockHistoryDigest = blockHistoryDigest; + this.resultHistoryDigest = resultHistoryDigest; + this.diff = diff; + } + + public List getLanes() { + return lanes; + } + + public byte[] getBlockHistoryDigest() { + return Arrays.copyOf(blockHistoryDigest, blockHistoryDigest.length); + } + + public byte[] getResultHistoryDigest() { + return Arrays.copyOf(resultHistoryDigest, resultHistoryDigest.length); + } + + public BlockReverseDiff getDiff() { + return diff; + } + } + + public static final class EncodedLane { + private final int laneId; + private final short bodyCodec; + private final long coverageBitmap; + private final byte[] canonicalPayload; + private final byte[] payloadDigest; + private final byte[] encodedFrameDigest; + private final byte[] frame; + + private EncodedLane(int laneId, short bodyCodec, long coverageBitmap, + byte[] canonicalPayload, byte[] payloadDigest, byte[] encodedFrameDigest, + byte[] frame) { + this.laneId = laneId; + this.bodyCodec = bodyCodec; + this.coverageBitmap = coverageBitmap; + this.canonicalPayload = canonicalPayload; + this.payloadDigest = payloadDigest; + this.encodedFrameDigest = encodedFrameDigest; + this.frame = frame; + } + + public int getLaneId() { + return laneId; + } + + public short getBodyCodec() { + return bodyCodec; + } + + public long getCoverageBitmap() { + return coverageBitmap; + } + + public byte[] getCanonicalPayload() { + return Arrays.copyOf(canonicalPayload, canonicalPayload.length); + } + + public byte[] getPayloadDigest() { + return Arrays.copyOf(payloadDigest, payloadDigest.length); + } + + public byte[] getEncodedFrameDigest() { + return Arrays.copyOf(encodedFrameDigest, encodedFrameDigest.length); + } + + public byte[] getFrame() { + return Arrays.copyOf(frame, frame.length); + } + } + + public static final class DecodedBundle { + private final BlockReverseDiff diff; + private final List lanes; + private final byte[] blockHistoryDigest; + private final byte[] resultHistoryDigest; + + private DecodedBundle(BlockReverseDiff diff, List lanes, + byte[] blockHistoryDigest, byte[] resultHistoryDigest) { + this.diff = diff; + this.lanes = java.util.Collections.unmodifiableList(new ArrayList<>(lanes)); + this.blockHistoryDigest = blockHistoryDigest; + this.resultHistoryDigest = resultHistoryDigest; + } + + public BlockReverseDiff getDiff() { + return diff; + } + + public List getLanes() { + return lanes; + } + + public byte[] getBlockHistoryDigest() { + return Arrays.copyOf(blockHistoryDigest, blockHistoryDigest.length); + } + + public byte[] getResultHistoryDigest() { + return Arrays.copyOf(resultHistoryDigest, resultHistoryDigest.length); + } + } + + public static final class DecodedLane { + private final int laneId; + private final BlockSnapshotMeta meta; + private final byte[] previousHistoryDigest; + private final byte[] blockHistoryDigest; + private final byte[] resultHistoryDigest; + private final LanePayload payload; + private final List groups; + private final short compressionId; + private final byte[] encodedFrameDigest; + private final long coverageBitmap; + + private DecodedLane(int laneId, BlockSnapshotMeta meta, + byte[] previousHistoryDigest, byte[] blockHistoryDigest, + byte[] resultHistoryDigest, LanePayload payload, List groups, + short compressionId, byte[] encodedFrameDigest) { + this.laneId = laneId; + this.meta = meta; + this.previousHistoryDigest = previousHistoryDigest; + this.blockHistoryDigest = blockHistoryDigest; + this.resultHistoryDigest = resultHistoryDigest; + this.payload = payload; + this.groups = java.util.Collections.unmodifiableList(new ArrayList<>(groups)); + this.compressionId = compressionId; + this.encodedFrameDigest = encodedFrameDigest; + this.coverageBitmap = payload.coverageBitmap; + } + + public int getLaneId() { + return laneId; + } + + public List getGroups() { + return groups; + } + + public short getCompressionId() { + return compressionId; + } + + public byte[] getEncodedFrameDigest() { + return Arrays.copyOf(encodedFrameDigest, encodedFrameDigest.length); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProofV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProofV3.java new file mode 100644 index 00000000000..e3a1244afe6 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProofV3.java @@ -0,0 +1,250 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.StateArchiveFiveLaneRecoveryIntentV3.RecoveryPoint; +import org.tron.core.db2.archive.StateArchiveFiveLaneSegmentWriterV3.ArchiveDurabilityProof; +import org.tron.core.db2.archive.StateArchiveFiveLaneSegmentWriterV3.FileTailProof; + +/** Byte-exact Common authority record for one append-file durability proof. */ +public final class StateArchiveFiveLaneDurabilityProofV3 { + + public static final String FILE_NAME = "append-file-proof.current"; + public static final String TEMP_FILE_NAME = "append-file-proof.current.tmp"; + public static final int MAGIC = 0x53415033; // SAP3 + public static final int TRAILER_MAGIC = 0x33504153; // 3PAS + public static final int HEADER_LENGTH = 352; + public static final int TAIL_LENGTH = 72; + public static final int TRAILER_LENGTH = 48; + + private static final byte[] HEADER_DOMAIN = StateArchiveFileFormatV3.ascii( + "TRON-STATE-ARCHIVE-PROOF-HEADER-V3\0"); + private static final byte[] TAILS_DOMAIN = StateArchiveFileFormatV3.ascii( + "TRON-STATE-ARCHIVE-PROOF-TAILS-V3\0"); + private static final byte[] PROOF_DOMAIN = StateArchiveFileFormatV3.ascii( + "TRON-STATE-ARCHIVE-PROOF-V3\0"); + private static final int HEADER_DIGEST_OFFSET = 316; + private static final int HEADER_CRC_OFFSET = 348; + + private StateArchiveFiveLaneDurabilityProofV3() { + } + + public static byte[] encode(ArchiveDurabilityProof proof) { + ArchiveDurabilityProof admitted = Objects.requireNonNull(proof, "proof"); + List tails = admitted.getFileTails(); + byte[] tailBytes = encodeTails(tails); + int totalLength = Math.addExact(HEADER_LENGTH + TRAILER_LENGTH, tailBytes.length); + ByteBuffer bytes = ByteBuffer.allocate(totalLength); + bytes.putInt(MAGIC); + bytes.putShort(StateArchiveFileFormatV3.MAJOR_VERSION); + bytes.putShort(StateArchiveFileFormatV3.MINOR_VERSION); + bytes.putInt(HEADER_LENGTH); + bytes.putShort((short) TAIL_LENGTH); + bytes.putShort((short) tails.size()); + bytes.putLong(totalLength); + bytes.putInt(0); + bytes.putInt(0); + bytes.put(admitted.getFormatIdentity()); + bytes.put(admitted.getDescriptorDigest()); + bytes.putLong(admitted.getCheckpointSequence()); + putPoint(bytes, admitted.getTarget()); + bytes.put(admitted.getCommonTargetDigest()); + bytes.put(StateArchiveFileFormatV3.sha256(TAILS_DOMAIN, tailBytes)); + bytes.put(new byte[28]); + if (bytes.position() != HEADER_DIGEST_OFFSET) { + throw new IllegalStateException("Invalid State Archive proof header layout"); + } + bytes.put(StateArchiveFileFormatV3.sha256(HEADER_DOMAIN, + Arrays.copyOf(bytes.array(), HEADER_DIGEST_OFFSET))); + bytes.putInt(crc32c(bytes.array(), 0, HEADER_CRC_OFFSET)); + bytes.put(tailBytes); + int proofDigestOffset = bytes.position(); + bytes.put(StateArchiveFileFormatV3.sha256(PROOF_DOMAIN, + Arrays.copyOf(bytes.array(), proofDigestOffset))); + bytes.putLong(totalLength); + bytes.putInt(crc32c(bytes.array(), 0, totalLength - 8)); + bytes.putInt(TRAILER_MAGIC); + return bytes.array(); + } + + public static ArchiveDurabilityProof decode(byte[] encoded) { + Objects.requireNonNull(encoded, "encoded"); + if (encoded.length < HEADER_LENGTH + TRAILER_LENGTH) { + throw new IllegalArgumentException("State Archive proof is truncated"); + } + ByteBuffer bytes = ByteBuffer.wrap(encoded); + require(bytes.getInt() == MAGIC, "proof magic"); + require(bytes.getShort() == StateArchiveFileFormatV3.MAJOR_VERSION, "proof major version"); + require(bytes.getShort() == StateArchiveFileFormatV3.MINOR_VERSION, "proof minor version"); + require(bytes.getInt() == HEADER_LENGTH, "proof header length"); + require(Short.toUnsignedInt(bytes.getShort()) == TAIL_LENGTH, "proof tail length"); + int tailCount = Short.toUnsignedInt(bytes.getShort()); + int expectedLength = Math.addExact(HEADER_LENGTH + TRAILER_LENGTH, + Math.multiplyExact(tailCount, TAIL_LENGTH)); + require(bytes.getLong() == expectedLength && encoded.length == expectedLength, + "proof total length"); + require(bytes.getInt() == 0 && bytes.getInt() == 0, "proof flags or reserved field"); + requireArray(read(bytes, 32), StateArchiveFileFormatV3.compositeFormatDigest(), + "proof composite format"); + requireArray(read(bytes, 32), StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + "proof placement descriptor"); + long checkpointSequence = bytes.getLong(); + RecoveryPoint target = readPoint(bytes); + byte[] commonTargetDigest = read(bytes, 32); + byte[] tailsDigest = read(bytes, 32); + requireZero(bytes, 28, "proof reserved bytes"); + requireArray(read(bytes, 32), StateArchiveFileFormatV3.sha256(HEADER_DOMAIN, + Arrays.copyOf(encoded, HEADER_DIGEST_OFFSET)), "proof header digest"); + require(bytes.getInt() == crc32c(encoded, 0, HEADER_CRC_OFFSET), + "proof header checksum"); + byte[] tailBytes = read(bytes, tailCount * TAIL_LENGTH); + requireArray(tailsDigest, StateArchiveFileFormatV3.sha256(TAILS_DOMAIN, tailBytes), + "proof tails digest"); + List tails = decodeTails(tailBytes); + int proofDigestOffset = HEADER_LENGTH + tailBytes.length; + requireArray(read(bytes, 32), StateArchiveFileFormatV3.sha256(PROOF_DOMAIN, + Arrays.copyOf(encoded, proofDigestOffset)), "proof digest"); + require(bytes.getLong() == expectedLength, "proof repeated length"); + require(bytes.getInt() == crc32c(encoded, 0, encoded.length - 8), + "proof checksum"); + require(bytes.getInt() == TRAILER_MAGIC, "proof trailer magic"); + return new ArchiveDurabilityProof(checkpointSequence, target, commonTargetDigest, tails); + } + + public static void publish(Path archiveRoot, ArchiveDurabilityProof proof) throws IOException { + Path root = Objects.requireNonNull(archiveRoot, "archiveRoot"); + Files.createDirectories(root); + byte[] encoded = encode(proof); + Path temporary = root.resolve(TEMP_FILE_NAME); + Path target = root.resolve(FILE_NAME); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer buffer = ByteBuffer.wrap(encoded); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + channel.force(true); + } + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("State Archive proof requires atomic publication", unsupported); + } + HistorySegmentStore.syncDirectory(root); + decode(Files.readAllBytes(target)); + } + + public static ArchiveDurabilityProof loadAndVerify(Path archiveRoot, + StateArchiveFiveLaneSegmentWriterV3 writer) throws IOException { + ArchiveDurabilityProof proof = decode(Files.readAllBytes( + Objects.requireNonNull(archiveRoot, "archiveRoot").resolve(FILE_NAME))); + Objects.requireNonNull(writer, "writer").verifyDurabilityProof(proof); + return proof; + } + + /** Proves the persisted Archive tail is the exact History authority referenced by Common W. */ + public static ArchiveDurabilityProof loadAndVerify(Path archiveRoot, + StateArchiveFiveLaneSegmentWriterV3 writer, RecoveryPoint commonCommitted, + byte[] commonTargetDigest) throws IOException { + ArchiveDurabilityProof proof = decode(Files.readAllBytes( + Objects.requireNonNull(archiveRoot, "archiveRoot").resolve(FILE_NAME))); + RecoveryPoint common = Objects.requireNonNull(commonCommitted, "commonCommitted"); + if (!samePoint(proof.getTarget(), common) + || !Arrays.equals(proof.getCommonTargetDigest(), commonTargetDigest)) { + throw new IllegalArgumentException("State Archive proof differs from Common target"); + } + Objects.requireNonNull(writer, "writer").verifyDurabilityProof(proof); + return proof; + } + + private static byte[] encodeTails(List tails) { + ByteBuffer bytes = ByteBuffer.allocate(Math.multiplyExact(tails.size(), TAIL_LENGTH)); + for (FileTailProof tail : tails) { + bytes.putShort((short) tail.getLaneId()); + bytes.putShort((short) 0); + bytes.putInt(0); + bytes.putLong(tail.getSegmentSeq()); + bytes.putLong(tail.getMarkerOffset()); + bytes.putLong(tail.getMarkerLength()); + bytes.putLong(tail.getMarkerEndOffset()); + bytes.put(tail.getMarkerDigest()); + } + return bytes.array(); + } + + private static List decodeTails(byte[] encoded) { + ByteBuffer bytes = ByteBuffer.wrap(encoded); + List tails = new ArrayList<>(); + while (bytes.hasRemaining()) { + int laneId = Short.toUnsignedInt(bytes.getShort()); + require(bytes.getShort() == 0 && bytes.getInt() == 0, "proof tail reserved field"); + long segmentSeq = bytes.getLong(); + long markerOffset = bytes.getLong(); + long markerLength = bytes.getLong(); + long markerEndOffset = bytes.getLong(); + require(markerLength <= Integer.MAX_VALUE, "proof marker length"); + tails.add(new FileTailProof(laneId, segmentSeq, markerOffset, (int) markerLength, + markerEndOffset, read(bytes, 32))); + } + return tails; + } + + private static void putPoint(ByteBuffer bytes, RecoveryPoint point) { + bytes.putLong(point.getEpoch()); + bytes.putLong(point.getBlockNumber()); + bytes.putLong(point.getTimestamp()); + bytes.put(point.getBlockHash()); + bytes.put(point.getParentHash()); + bytes.put(point.getResultHistoryDigest()); + } + + private static RecoveryPoint readPoint(ByteBuffer bytes) { + return new RecoveryPoint(bytes.getLong(), bytes.getLong(), bytes.getLong(), + read(bytes, 32), read(bytes, 32), read(bytes, 32)); + } + + private static boolean samePoint(RecoveryPoint left, RecoveryPoint right) { + return left.getEpoch() == right.getEpoch() + && left.getBlockNumber() == right.getBlockNumber() + && left.getTimestamp() == right.getTimestamp() + && Arrays.equals(left.getBlockHash(), right.getBlockHash()) + && Arrays.equals(left.getParentHash(), right.getParentHash()) + && Arrays.equals(left.getResultHistoryDigest(), right.getResultHistoryDigest()); + } + + private static byte[] read(ByteBuffer bytes, int length) { + byte[] result = new byte[length]; + bytes.get(result); + return result; + } + + private static void requireZero(ByteBuffer bytes, int length, String field) { + requireArray(read(bytes, length), new byte[length], field); + } + + private static void requireArray(byte[] actual, byte[] expected, String field) { + require(Arrays.equals(actual, expected), field); + } + + private static void require(boolean condition, String field) { + if (!condition) { + throw new IllegalArgumentException("State Archive " + field + " mismatch"); + } + } + + private static int crc32c(byte[] bytes, int offset, int length) { + return Hashing.crc32c().hashBytes(bytes, offset, length).asInt(); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneRecoveryIntentV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneRecoveryIntentV3.java new file mode 100644 index 00000000000..4b75d782178 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneRecoveryIntentV3.java @@ -0,0 +1,571 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** Byte-exact crash-recovery plan for five-lane open-tail repair. */ +public final class StateArchiveFiveLaneRecoveryIntentV3 { + + public static final String FILE_NAME = "five-lane-recovery.intent"; + public static final String TEMP_FILE_NAME = "five-lane-recovery.intent.tmp"; + public static final int DATA_TRUNCATE = 1; + public static final int DELETE_PAIR = 1 << 1; + public static final int INDEX_REPLACE = 1 << 2; + public static final int ORIGINAL_INDEX_MISSING = 1 << 3; + public static final int SOURCE_PAIR_MISSING = 1 << 4; + public static final long NO_TARGET_SEGMENT = -1L; + + private static final int KNOWN_FLAGS = DATA_TRUNCATE | DELETE_PAIR + | INDEX_REPLACE | ORIGINAL_INDEX_MISSING | SOURCE_PAIR_MISSING; + private static final int COMMON_POINT_PRESENT = 1; + private static final int TARGET_POINT_PRESENT = 1 << 1; + private static final int KNOWN_HEADER_FLAGS = COMMON_POINT_PRESENT | TARGET_POINT_PRESENT; + private static final int HEADER_DIGEST_OFFSET = 732; + private static final int HEADER_CRC_OFFSET = 764; + private static final int INTENT_DIGEST_OFFSET = 1_568; + private static final int INTENT_CRC_OFFSET = 1_608; + + private StateArchiveFiveLaneRecoveryIntentV3() { + } + + public static byte[] dataPrefixDigest(byte[] exactTargetPrefix) { + return StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.RECOVERY_DATA_PREFIX_DOMAIN, + Objects.requireNonNull(exactTargetPrefix, "exactTargetPrefix")); + } + + public static byte[] indexFileDigest(byte[] exactTargetIndex) { + return StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.RECOVERY_INDEX_FILE_DOMAIN, + Objects.requireNonNull(exactTargetIndex, "exactTargetIndex")); + } + + public static byte[] encode(Intent intent) { + Objects.requireNonNull(intent, "intent"); + intent.validate(); + byte[] records = encodeRecords(intent.lanes); + byte[] recordsDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.RECOVERY_INTENT_RECORDS_DOMAIN, records); + ByteBuffer bytes = ByteBuffer.allocate( + StateArchiveFileFormatV3.RECOVERY_INTENT_TOTAL_LENGTH); + bytes.putInt(StateArchiveFileFormatV3.RECOVERY_INTENT_MAGIC); + bytes.putShort(StateArchiveFileFormatV3.MAJOR_VERSION); + bytes.putShort(StateArchiveFileFormatV3.MINOR_VERSION); + bytes.putInt(StateArchiveFileFormatV3.RECOVERY_INTENT_HEADER_LENGTH); + bytes.putShort((short) StateArchiveFileFormatV3.RECOVERY_INTENT_LANE_RECORD_LENGTH); + bytes.putShort((short) StateArchiveFileFormatV3.fiveLaneIds().length); + bytes.putLong(StateArchiveFileFormatV3.RECOVERY_INTENT_TOTAL_LENGTH); + bytes.putShort(StateArchiveFileFormatV3.RECOVERY_INTENT_ACTION_SCHEMA_ID); + int flags = (intent.commonCommitted == null ? 0 : COMMON_POINT_PRESENT) + | (intent.target == null ? 0 : TARGET_POINT_PRESENT); + bytes.putShort((short) flags); + bytes.putInt(0); + bytes.put(StateArchiveFileFormatV3.compositeFormatDigest()); + bytes.put(StateArchiveFileFormatV3.fiveLaneDescriptorDigest()); + bytes.put(intent.baselineHistoryDigest); + putPoint(bytes, intent.authorizedCeiling); + putPointOrZero(bytes, intent.commonCommitted); + putPointOrZero(bytes, intent.target); + bytes.put(recordsDigest); + bytes.put(new byte[212]); + if (bytes.position() != HEADER_DIGEST_OFFSET) { + throw new IllegalStateException("Invalid State Archive recovery intent header layout"); + } + bytes.put(StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.RECOVERY_INTENT_HEADER_DOMAIN, + Arrays.copyOf(bytes.array(), HEADER_DIGEST_OFFSET))); + bytes.putInt(crc32c(bytes.array(), 0, HEADER_CRC_OFFSET)); + bytes.put(records); + if (bytes.position() != INTENT_DIGEST_OFFSET) { + throw new IllegalStateException("Invalid State Archive recovery intent record layout"); + } + bytes.put(StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.RECOVERY_INTENT_DOMAIN, + Arrays.copyOf(bytes.array(), INTENT_DIGEST_OFFSET))); + bytes.putLong(StateArchiveFileFormatV3.RECOVERY_INTENT_TOTAL_LENGTH); + bytes.putInt(crc32c(bytes.array(), 0, INTENT_CRC_OFFSET)); + bytes.putInt(StateArchiveFileFormatV3.RECOVERY_INTENT_TRAILER_MAGIC); + if (bytes.hasRemaining()) { + throw new IllegalStateException("Invalid State Archive recovery intent length"); + } + return bytes.array(); + } + + public static Intent decode(byte[] encoded) { + requireLength(encoded, StateArchiveFileFormatV3.RECOVERY_INTENT_TOTAL_LENGTH, + "recovery intent"); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + requireInt(bytes, StateArchiveFileFormatV3.RECOVERY_INTENT_MAGIC, "intent magic"); + requireShort(bytes, StateArchiveFileFormatV3.MAJOR_VERSION, "intent major version"); + requireShort(bytes, StateArchiveFileFormatV3.MINOR_VERSION, "intent minor version"); + requireInt(bytes, StateArchiveFileFormatV3.RECOVERY_INTENT_HEADER_LENGTH, + "intent header length"); + requireShort(bytes, (short) StateArchiveFileFormatV3.RECOVERY_INTENT_LANE_RECORD_LENGTH, + "intent record length"); + requireShort(bytes, (short) StateArchiveFileFormatV3.fiveLaneIds().length, + "intent lane count"); + requireLong(bytes, StateArchiveFileFormatV3.RECOVERY_INTENT_TOTAL_LENGTH, + "intent total length"); + requireShort(bytes, StateArchiveFileFormatV3.RECOVERY_INTENT_ACTION_SCHEMA_ID, + "intent action schema"); + int flags = Short.toUnsignedInt(bytes.getShort()); + if ((flags & ~KNOWN_HEADER_FLAGS) != 0 + || (flags & COMMON_POINT_PRESENT) != 0 && (flags & TARGET_POINT_PRESENT) == 0) { + throw new IllegalArgumentException("State Archive recovery intent flags mismatch"); + } + requireInt(bytes, 0, "intent reserved field"); + requireArray(getBytes(bytes, 32), StateArchiveFileFormatV3.compositeFormatDigest(), + "intent composite format"); + requireArray(getBytes(bytes, 32), StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + "intent placement descriptor"); + byte[] baselineHistoryDigest = getBytes(bytes, 32); + RecoveryPoint authorized = readPoint(bytes); + RecoveryPoint common = readPointOrZero(bytes, (flags & COMMON_POINT_PRESENT) != 0); + RecoveryPoint target = readPointOrZero(bytes, (flags & TARGET_POINT_PRESENT) != 0); + byte[] recordsDigest = getBytes(bytes, 32); + requireZero(bytes, 212, "intent header reserved bytes"); + byte[] headerDigest = getBytes(bytes, 32); + requireArray(headerDigest, StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.RECOVERY_INTENT_HEADER_DOMAIN, + Arrays.copyOf(encoded, HEADER_DIGEST_OFFSET)), "intent header digest"); + if (bytes.getInt() != crc32c(encoded, 0, HEADER_CRC_OFFSET)) { + throw new IllegalArgumentException("State Archive recovery intent header checksum mismatch"); + } + byte[] recordBytes = getBytes(bytes, StateArchiveFileFormatV3.fiveLaneIds().length + * StateArchiveFileFormatV3.RECOVERY_INTENT_LANE_RECORD_LENGTH); + requireArray(recordsDigest, StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.RECOVERY_INTENT_RECORDS_DOMAIN, recordBytes), + "intent records digest"); + List lanes = decodeRecords(recordBytes); + byte[] intentDigest = getBytes(bytes, 32); + requireArray(intentDigest, StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.RECOVERY_INTENT_DOMAIN, + Arrays.copyOf(encoded, INTENT_DIGEST_OFFSET)), "intent digest"); + requireLong(bytes, StateArchiveFileFormatV3.RECOVERY_INTENT_TOTAL_LENGTH, + "repeated intent total length"); + if (bytes.getInt() != crc32c(encoded, 0, INTENT_CRC_OFFSET)) { + throw new IllegalArgumentException("State Archive recovery intent checksum mismatch"); + } + requireInt(bytes, StateArchiveFileFormatV3.RECOVERY_INTENT_TRAILER_MAGIC, + "intent trailer magic"); + return new Intent(baselineHistoryDigest, authorized, common, target, lanes, + headerDigest, recordsDigest, intentDigest); + } + + private static byte[] encodeRecords(List lanes) { + ByteBuffer bytes = ByteBuffer.allocate(lanes.size() + * StateArchiveFileFormatV3.RECOVERY_INTENT_LANE_RECORD_LENGTH); + for (LaneTarget lane : lanes) { + bytes.putShort((short) lane.laneId); + bytes.putShort((short) lane.actionFlags); + bytes.putInt(0); + bytes.putLong(lane.sourceSegmentSeq); + bytes.putLong(lane.originalDataEnd); + bytes.putLong(lane.originalIndexEnd); + bytes.putLong(lane.targetSegmentSeq); + bytes.putLong(lane.targetDataEnd); + bytes.putLong(lane.targetIndexEnd); + bytes.put(lane.sourceSegmentHeaderDigest); + bytes.put(lane.targetDataPrefixDigest); + bytes.put(lane.targetIndexFileDigest); + bytes.putLong(0); + } + return bytes.array(); + } + + private static List decodeRecords(byte[] encoded) { + ByteBuffer bytes = ByteBuffer.wrap(encoded); + List lanes = new ArrayList<>(); + while (bytes.hasRemaining()) { + int laneId = Short.toUnsignedInt(bytes.getShort()); + int actionFlags = Short.toUnsignedInt(bytes.getShort()); + requireInt(bytes, 0, "lane intent reserved field"); + LaneTarget lane = new LaneTarget(laneId, actionFlags, + bytes.getLong(), bytes.getLong(), bytes.getLong(), bytes.getLong(), + bytes.getLong(), bytes.getLong(), getBytes(bytes, 32), getBytes(bytes, 32), + getBytes(bytes, 32)); + requireLong(bytes, 0, "lane intent reserved tail"); + lanes.add(lane); + } + return lanes; + } + + private static void putPoint(ByteBuffer bytes, RecoveryPoint point) { + bytes.putLong(point.epoch); + bytes.putLong(point.blockNumber); + bytes.putLong(point.timestamp); + bytes.put(point.blockHash); + bytes.put(point.parentHash); + bytes.put(point.resultHistoryDigest); + } + + private static void putPointOrZero(ByteBuffer bytes, RecoveryPoint point) { + if (point == null) { + bytes.put(new byte[120]); + } else { + putPoint(bytes, point); + } + } + + private static RecoveryPoint readPoint(ByteBuffer bytes) { + return new RecoveryPoint(bytes.getLong(), bytes.getLong(), bytes.getLong(), + getBytes(bytes, 32), getBytes(bytes, 32), getBytes(bytes, 32)); + } + + private static RecoveryPoint readPointOrZero(ByteBuffer bytes, boolean present) { + if (present) { + return readPoint(bytes); + } + requireZero(bytes, 120, "absent recovery point"); + return null; + } + + private static void requireSameIfEqual(RecoveryPoint first, RecoveryPoint second) { + if (first.blockNumber == second.blockNumber + && (first.epoch != second.epoch || first.timestamp != second.timestamp + || !Arrays.equals(first.blockHash, second.blockHash) + || !Arrays.equals(first.parentHash, second.parentHash) + || !Arrays.equals(first.resultHistoryDigest, second.resultHistoryDigest))) { + throw new IllegalArgumentException("State Archive recovery point identity mismatch"); + } + } + + private static byte[] requireHash(byte[] value, String name) { + requireLength(value, StateArchiveFileFormatV3.HASH_LENGTH, name); + return Arrays.copyOf(value, value.length); + } + + private static void requireLength(byte[] value, int expected, String name) { + if (value == null || value.length != expected) { + throw new IllegalArgumentException("Invalid State Archive " + name + " length"); + } + } + + private static byte[] getBytes(ByteBuffer bytes, int length) { + byte[] result = new byte[length]; + bytes.get(result); + return result; + } + + private static void requireZero(ByteBuffer bytes, int length, String name) { + for (int index = 0; index < length; index++) { + if (bytes.get() != 0) { + throw new IllegalArgumentException("Non-zero State Archive " + name); + } + } + } + + private static void requireInt(ByteBuffer bytes, int expected, String name) { + if (bytes.getInt() != expected) { + throw new IllegalArgumentException("State Archive " + name + " mismatch"); + } + } + + private static void requireShort(ByteBuffer bytes, short expected, String name) { + if (bytes.getShort() != expected) { + throw new IllegalArgumentException("State Archive " + name + " mismatch"); + } + } + + private static void requireLong(ByteBuffer bytes, long expected, String name) { + if (bytes.getLong() != expected) { + throw new IllegalArgumentException("State Archive " + name + " mismatch"); + } + } + + private static void requireArray(byte[] actual, byte[] expected, String name) { + if (!Arrays.equals(actual, expected)) { + throw new IllegalArgumentException("State Archive " + name + " mismatch"); + } + } + + private static boolean isZero(byte[] value) { + for (byte element : value) { + if (element != 0) { + return false; + } + } + return true; + } + + private static int crc32c(byte[] bytes, int offset, int length) { + return Hashing.crc32c().hashBytes(bytes, offset, length).asInt(); + } + + public static final class RecoveryPoint { + private final long epoch; + private final long blockNumber; + private final long timestamp; + private final byte[] blockHash; + private final byte[] parentHash; + private final byte[] resultHistoryDigest; + + public RecoveryPoint(long epoch, long blockNumber, long timestamp, + byte[] blockHash, byte[] parentHash, byte[] resultHistoryDigest) { + if (epoch < 0 || blockNumber < 0 || timestamp < 0 || epoch != blockNumber) { + throw new IllegalArgumentException("Invalid State Archive recovery block identity"); + } + this.epoch = epoch; + this.blockNumber = blockNumber; + this.timestamp = timestamp; + this.blockHash = requireHash(blockHash, "recovery block hash"); + this.parentHash = requireHash(parentHash, "recovery parent hash"); + this.resultHistoryDigest = requireHash(resultHistoryDigest, + "recovery history digest"); + } + + public long getEpoch() { + return epoch; + } + + public long getBlockNumber() { + return blockNumber; + } + + public long getTimestamp() { + return timestamp; + } + + public byte[] getBlockHash() { + return Arrays.copyOf(blockHash, blockHash.length); + } + + public byte[] getParentHash() { + return Arrays.copyOf(parentHash, parentHash.length); + } + + public byte[] getResultHistoryDigest() { + return Arrays.copyOf(resultHistoryDigest, resultHistoryDigest.length); + } + } + + public static final class LaneTarget { + private final int laneId; + private final int actionFlags; + private final long sourceSegmentSeq; + private final long originalDataEnd; + private final long originalIndexEnd; + private final long targetSegmentSeq; + private final long targetDataEnd; + private final long targetIndexEnd; + private final byte[] sourceSegmentHeaderDigest; + private final byte[] targetDataPrefixDigest; + private final byte[] targetIndexFileDigest; + + public LaneTarget(int laneId, int actionFlags, long sourceSegmentSeq, + long originalDataEnd, long originalIndexEnd, long targetSegmentSeq, + long targetDataEnd, long targetIndexEnd, byte[] sourceSegmentHeaderDigest, + byte[] targetDataPrefixDigest, byte[] targetIndexFileDigest) { + this.laneId = laneId; + this.actionFlags = actionFlags; + this.sourceSegmentSeq = sourceSegmentSeq; + this.originalDataEnd = originalDataEnd; + this.originalIndexEnd = originalIndexEnd; + this.targetSegmentSeq = targetSegmentSeq; + this.targetDataEnd = targetDataEnd; + this.targetIndexEnd = targetIndexEnd; + this.sourceSegmentHeaderDigest = requireHash(sourceSegmentHeaderDigest, + "intent segment header digest"); + this.targetDataPrefixDigest = requireHash(targetDataPrefixDigest, + "intent target data digest"); + this.targetIndexFileDigest = requireHash(targetIndexFileDigest, + "intent target index digest"); + validate(); + } + + private void validate() { + StateArchiveFileFormatV3.laneKind(laneId); + if ((actionFlags & ~KNOWN_FLAGS) != 0) { + throw new IllegalArgumentException("Invalid State Archive lane recovery flags"); + } + if ((actionFlags & SOURCE_PAIR_MISSING) != 0) { + if (actionFlags != SOURCE_PAIR_MISSING || sourceSegmentSeq != NO_TARGET_SEGMENT + || originalDataEnd != 0 || originalIndexEnd != 0 + || targetSegmentSeq != NO_TARGET_SEGMENT || targetDataEnd != 0 + || targetIndexEnd != 0 || !isZero(sourceSegmentHeaderDigest) + || !isZero(targetDataPrefixDigest) || !isZero(targetIndexFileDigest)) { + throw new IllegalArgumentException("Invalid State Archive missing lane source"); + } + return; + } + if (sourceSegmentSeq < 0 + || originalDataEnd < StateArchiveFileFormatV3.PART_HEADER_LENGTH + || originalIndexEnd < 0 + || ((actionFlags & ORIGINAL_INDEX_MISSING) != 0) != (originalIndexEnd == 0)) { + throw new IllegalArgumentException("Invalid State Archive lane recovery source"); + } + if ((actionFlags & DELETE_PAIR) != 0) { + if ((actionFlags & (DATA_TRUNCATE | INDEX_REPLACE)) != 0 + || targetSegmentSeq != NO_TARGET_SEGMENT || targetDataEnd != 0 + || targetIndexEnd != 0 || !isZero(targetDataPrefixDigest) + || !isZero(targetIndexFileDigest)) { + throw new IllegalArgumentException("Invalid State Archive lane delete target"); + } + return; + } + if (targetSegmentSeq != sourceSegmentSeq + || targetDataEnd < StateArchiveFileFormatV3.PART_HEADER_LENGTH + || targetDataEnd > originalDataEnd + || ((actionFlags & DATA_TRUNCATE) != 0) != (targetDataEnd < originalDataEnd) + || targetIndexEnd < StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + || (targetIndexEnd - StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH) + % StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH != 0 + || ((originalIndexEnd == 0 || originalIndexEnd != targetIndexEnd) + && (actionFlags & INDEX_REPLACE) == 0)) { + throw new IllegalArgumentException("Invalid State Archive lane recovery target"); + } + } + + public int getLaneId() { + return laneId; + } + + public int getActionFlags() { + return actionFlags; + } + + public long getSourceSegmentSeq() { + return sourceSegmentSeq; + } + + public long getOriginalDataEnd() { + return originalDataEnd; + } + + public long getOriginalIndexEnd() { + return originalIndexEnd; + } + + public long getTargetSegmentSeq() { + return targetSegmentSeq; + } + + public long getTargetDataEnd() { + return targetDataEnd; + } + + public long getTargetIndexEnd() { + return targetIndexEnd; + } + + public byte[] getSourceSegmentHeaderDigest() { + return Arrays.copyOf(sourceSegmentHeaderDigest, sourceSegmentHeaderDigest.length); + } + + public byte[] getTargetDataPrefixDigest() { + return Arrays.copyOf(targetDataPrefixDigest, targetDataPrefixDigest.length); + } + + public byte[] getTargetIndexFileDigest() { + return Arrays.copyOf(targetIndexFileDigest, targetIndexFileDigest.length); + } + } + + public static final class Intent { + private final byte[] baselineHistoryDigest; + private final RecoveryPoint authorizedCeiling; + private final RecoveryPoint commonCommitted; + private final RecoveryPoint target; + private final List lanes; + private final byte[] headerDigest; + private final byte[] recordsDigest; + private final byte[] intentDigest; + + public Intent(byte[] baselineHistoryDigest, RecoveryPoint authorizedCeiling, + RecoveryPoint commonCommitted, RecoveryPoint target, List lanes) { + this(baselineHistoryDigest, authorizedCeiling, commonCommitted, target, lanes, + null, null, null); + } + + private Intent(byte[] baselineHistoryDigest, RecoveryPoint authorizedCeiling, + RecoveryPoint commonCommitted, RecoveryPoint target, List lanes, + byte[] headerDigest, byte[] recordsDigest, byte[] intentDigest) { + this.baselineHistoryDigest = requireHash(baselineHistoryDigest, + "intent baseline history digest"); + this.authorizedCeiling = Objects.requireNonNull(authorizedCeiling, + "authorizedCeiling"); + this.commonCommitted = commonCommitted; + this.target = target; + this.lanes = Collections.unmodifiableList(new ArrayList<>( + Objects.requireNonNull(lanes, "lanes"))); + this.headerDigest = headerDigest == null ? null + : requireHash(headerDigest, "intent header digest"); + this.recordsDigest = recordsDigest == null ? null + : requireHash(recordsDigest, "intent records digest"); + this.intentDigest = intentDigest == null ? null + : requireHash(intentDigest, "intent digest"); + validate(); + } + + private void validate() { + if (commonCommitted != null && target == null) { + throw new IllegalArgumentException("Invalid State Archive recovery point order"); + } + if (target != null && target.blockNumber > authorizedCeiling.blockNumber) { + throw new IllegalArgumentException("Invalid State Archive recovery point order"); + } + if (commonCommitted != null && commonCommitted.blockNumber > target.blockNumber) { + throw new IllegalArgumentException("Invalid State Archive recovery point order"); + } + if (commonCommitted != null) { + requireSameIfEqual(commonCommitted, target); + requireSameIfEqual(commonCommitted, authorizedCeiling); + } + if (target != null) { + requireSameIfEqual(target, authorizedCeiling); + } + int[] laneIds = StateArchiveFileFormatV3.fiveLaneIds(); + if (lanes.size() != laneIds.length) { + throw new IllegalArgumentException("Incomplete State Archive recovery lane plan"); + } + boolean mutation = false; + for (int index = 0; index < laneIds.length; index++) { + LaneTarget lane = Objects.requireNonNull(lanes.get(index), "lane"); + if (lane.laneId != laneIds[index]) { + throw new IllegalArgumentException("Non-canonical State Archive recovery lane plan"); + } + mutation |= (lane.actionFlags & (DATA_TRUNCATE | DELETE_PAIR | INDEX_REPLACE)) != 0; + } + if (!mutation) { + throw new IllegalArgumentException("State Archive recovery intent has no mutation"); + } + } + + public byte[] getBaselineHistoryDigest() { + return Arrays.copyOf(baselineHistoryDigest, baselineHistoryDigest.length); + } + + public RecoveryPoint getAuthorizedCeiling() { + return authorizedCeiling; + } + + public RecoveryPoint getCommonCommitted() { + return commonCommitted; + } + + public RecoveryPoint getTarget() { + return target; + } + + public List getLanes() { + return lanes; + } + + public byte[] getHeaderDigest() { + return headerDigest == null ? null : Arrays.copyOf(headerDigest, headerDigest.length); + } + + public byte[] getRecordsDigest() { + return recordsDigest == null ? null : Arrays.copyOf(recordsDigest, recordsDigest.length); + } + + public byte[] getIntentDigest() { + return intentDigest == null ? null : Arrays.copyOf(intentDigest, intentDigest.length); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java new file mode 100644 index 00000000000..9bdb43aa6d2 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java @@ -0,0 +1,2109 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.tron.core.db2.archive.StateArchiveFiveLaneBlockCodecV3.DecodedBundle; +import org.tron.core.db2.archive.StateArchiveFiveLaneBlockCodecV3.EncodedBundle; +import org.tron.core.db2.archive.StateArchiveFiveLaneBlockCodecV3.EncodedLane; +import org.tron.core.db2.archive.StateArchiveFiveLaneRecoveryIntentV3.Intent; +import org.tron.core.db2.archive.StateArchiveFiveLaneRecoveryIntentV3.LaneTarget; +import org.tron.core.db2.archive.StateArchiveFiveLaneRecoveryIntentV3.RecoveryPoint; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.BlockIndexEntry; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.BlockIndexHeader; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.CurrentSegment; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.DurableMarker; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SealedSegment; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SegmentHeader; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SegmentSeal; + +/** Default-off five-lane append writer for State Archive v3 segment data and block indexes. */ +public final class StateArchiveFiveLaneSegmentWriterV3 implements AutoCloseable { + + private static final int APPEND_BUFFER_BYTES = 2 * 1024 * 1024; + private static final int BLOCK_NUMBER_OFFSET = 40; + private static final int PREVIOUS_HISTORY_DIGEST_OFFSET = 152; + private static final int RESULT_HISTORY_DIGEST_OFFSET = 248; + private static final int ENTRY_COUNT_OFFSET = 304; + private static final int RAW_PAYLOAD_LENGTH_OFFSET = 312; + private static final int COMPRESSION_ID_OFFSET = 322; + private static final int ENCODED_DIGEST_FROM_END = 48; + + private final Path segmentRoot; + private final Path archiveRoot; + private final byte[] baselineHistoryDigest; + private final short compressionId; + private final long rotationTargetBytes; + private final StateArchiveFiveLaneBlockCodecV3 codec = + new StateArchiveFiveLaneBlockCodecV3(); + private final Map lanes = new HashMap<>(); + private final List sealedSegments = new ArrayList<>(); + private BlockSnapshotMeta appendHead; + private byte[] resultHistoryDigest; + private boolean failed; + private RecoveryRequest activeRecoveryRequest; + private Intent activeRecoveryIntent; + private RecoveryFaultHook recoveryFaultHook = RecoveryFaultHook.NONE; + private ArchiveDurabilityProof lastDurabilityProof; + private boolean lastDurabilityProofRecovered; + private boolean rotationWithUnsyncedData; + private final List pendingRotationTails = new ArrayList<>(); + private long activeCheckpointSequence = -1; + private byte[] activeCommonTargetDigest; + + public StateArchiveFiveLaneSegmentWriterV3(Path archiveRoot, + byte[] baselineHistoryDigest, short compressionId) throws IOException { + this(archiveRoot, baselineHistoryDigest, compressionId, + StateArchiveFileFormatV3.SEGMENT_TARGET_BYTES, null, RecoveryFaultHook.NONE); + } + + StateArchiveFiveLaneSegmentWriterV3(Path archiveRoot, byte[] baselineHistoryDigest, + short compressionId, long rotationTargetBytes) throws IOException { + this(archiveRoot, baselineHistoryDigest, compressionId, rotationTargetBytes, null, + RecoveryFaultHook.NONE); + } + + /** Repairs open tails under complete caller and Common identities. */ + public static StateArchiveFiveLaneSegmentWriterV3 recover(Path archiveRoot, + byte[] baselineHistoryDigest, short compressionId, + RecoveryPoint authorizedCeiling, RecoveryPoint commonCommitted) throws IOException { + return new StateArchiveFiveLaneSegmentWriterV3(archiveRoot, baselineHistoryDigest, + compressionId, StateArchiveFileFormatV3.SEGMENT_TARGET_BYTES, + new RecoveryRequest(authorizedCeiling, commonCommitted), RecoveryFaultHook.NONE); + } + + /** Prototype-only numeric boundary retained for component tests, never a Common proof. */ + static StateArchiveFiveLaneSegmentWriterV3 recover(Path archiveRoot, + byte[] baselineHistoryDigest, short compressionId, long authorizedHead) + throws IOException { + if (authorizedHead < 0) { + throw new IllegalArgumentException("Invalid State Archive recovery boundary"); + } + return new StateArchiveFiveLaneSegmentWriterV3(archiveRoot, baselineHistoryDigest, + compressionId, StateArchiveFileFormatV3.SEGMENT_TARGET_BYTES, + RecoveryRequest.prototype(authorizedHead), RecoveryFaultHook.NONE); + } + + static StateArchiveFiveLaneSegmentWriterV3 recover(Path archiveRoot, + byte[] baselineHistoryDigest, short compressionId, long rotationTargetBytes, + long authorizedHead) throws IOException { + if (authorizedHead < 0) { + throw new IllegalArgumentException("Invalid State Archive recovery boundary"); + } + return new StateArchiveFiveLaneSegmentWriterV3(archiveRoot, baselineHistoryDigest, + compressionId, rotationTargetBytes, RecoveryRequest.prototype(authorizedHead), + RecoveryFaultHook.NONE); + } + + static StateArchiveFiveLaneSegmentWriterV3 recover(Path archiveRoot, + byte[] baselineHistoryDigest, short compressionId, long rotationTargetBytes, + RecoveryPoint authorizedCeiling, RecoveryPoint commonCommitted, + RecoveryFaultHook faultHook) throws IOException { + return new StateArchiveFiveLaneSegmentWriterV3(archiveRoot, baselineHistoryDigest, + compressionId, rotationTargetBytes, + new RecoveryRequest(authorizedCeiling, commonCommitted), faultHook); + } + + private StateArchiveFiveLaneSegmentWriterV3(Path archiveRoot, + byte[] baselineHistoryDigest, short compressionId, long rotationTargetBytes, + RecoveryRequest recoveryRequest, RecoveryFaultHook faultHook) throws IOException { + Objects.requireNonNull(archiveRoot, "archiveRoot"); + this.baselineHistoryDigest = requireHash(baselineHistoryDigest, + "baseline history digest"); + requireCompression(compressionId); + if (rotationTargetBytes <= StateArchiveFileFormatV3.PART_HEADER_LENGTH) { + throw new IllegalArgumentException("Invalid State Archive rotation target"); + } + this.compressionId = compressionId; + this.rotationTargetBytes = rotationTargetBytes; + this.archiveRoot = archiveRoot; + this.segmentRoot = archiveRoot.resolve("segments"); + Files.createDirectories(segmentRoot); + requireNoLegacyIntent(); + Intent existingIntent = loadIntent(); + if (existingIntent != null) { + if (!Arrays.equals(existingIntent.getBaselineHistoryDigest(), this.baselineHistoryDigest)) { + throw new IllegalArgumentException("State Archive recovery intent baseline mismatch"); + } + recoverWithIntent(existingIntent, faultHook); + } else if (recoveryRequest != null) { + planAndRecover(recoveryRequest, faultHook); + } else { + discardUnpublishedTemporaryIntent(); + reopen(null); + } + } + + private void planAndRecover(RecoveryRequest request, RecoveryFaultHook faultHook) + throws IOException { + discardUnpublishedTemporaryIntent(); + activeRecoveryRequest = request; + recoveryFaultHook = Objects.requireNonNull(faultHook, "faultHook"); + reopen(request.authorizedBlock()); + } + + private void recoverWithIntent(Intent intent, RecoveryFaultHook faultHook) + throws IOException { + activeRecoveryIntent = StateArchiveFiveLaneRecoveryIntentV3.decode( + StateArchiveFiveLaneRecoveryIntentV3.encode(intent)); + recoveryFaultHook = Objects.requireNonNull(faultHook, "faultHook"); + Long target = intent.getTarget() == null ? 0L : intent.getTarget().getBlockNumber(); + reopen(target); + } + + private void requireNoLegacyIntent() { + if (Files.exists(archiveRoot.resolve("truncation.intent"))) { + throw new IllegalArgumentException("Incompatible legacy State Archive truncation intent"); + } + } + + private Intent loadIntent() throws IOException { + Path path = archiveRoot.resolve(StateArchiveFiveLaneRecoveryIntentV3.FILE_NAME); + if (!Files.exists(path)) { + return null; + } + return StateArchiveFiveLaneRecoveryIntentV3.decode(Files.readAllBytes(path)); + } + + private void persistIntent(Intent intent) throws IOException { + byte[] encoded = StateArchiveFiveLaneRecoveryIntentV3.encode(intent); + Path temporary = archiveRoot.resolve(StateArchiveFiveLaneRecoveryIntentV3.TEMP_FILE_NAME); + Path target = archiveRoot.resolve(StateArchiveFiveLaneRecoveryIntentV3.FILE_NAME); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + writeFully(channel, ByteBuffer.wrap(encoded)); + channel.force(true); + } + recoveryFaultHook.after(RecoveryStage.TEMPORARY_FORCED, -1); + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("State Archive recovery intent requires atomic move", unsupported); + } + syncDirectory(archiveRoot); + recoveryFaultHook.after(RecoveryStage.INTENT_PUBLISHED, -1); + } + + private void clearIntent() throws IOException { + Files.deleteIfExists(archiveRoot.resolve(StateArchiveFiveLaneRecoveryIntentV3.FILE_NAME)); + syncDirectory(archiveRoot); + recoveryFaultHook.after(RecoveryStage.INTENT_DELETED, -1); + } + + private void discardUnpublishedTemporaryIntent() throws IOException { + if (Files.deleteIfExists( + archiveRoot.resolve(StateArchiveFiveLaneRecoveryIntentV3.TEMP_FILE_NAME))) { + syncDirectory(archiveRoot); + } + } + + /** Appends all five frames; the in-memory append head advances only after every lane succeeds. */ + public synchronized void append(EncodedBundle bundle) throws IOException { + if (activeCheckpointSequence >= 0) { + throw new IllegalStateException("State Archive checkpoint append is active"); + } + append(bundle, -1, null, SyncFaultHook.NONE); + } + + /** Appends one bundle under the Common identity needed to prove any intervening rotation. */ + public synchronized void appendForCheckpoint(EncodedBundle bundle, long checkpointSequence, + byte[] commonTargetDigest) throws IOException { + appendForCheckpoint(bundle, checkpointSequence, commonTargetDigest, SyncFaultHook.NONE); + } + + synchronized void appendForCheckpoint(EncodedBundle bundle, long checkpointSequence, + byte[] commonTargetDigest, SyncFaultHook faultHook) throws IOException { + if (checkpointSequence < 0) { + throw new IllegalArgumentException("Invalid State Archive checkpoint sequence"); + } + byte[] admittedDigest = requireHash(commonTargetDigest, "Common target digest"); + if (activeCheckpointSequence >= 0 + && (activeCheckpointSequence != checkpointSequence + || !Arrays.equals(activeCommonTargetDigest, admittedDigest))) { + throw new IllegalArgumentException("State Archive checkpoint append identity mismatch"); + } + append(bundle, checkpointSequence, admittedDigest, + Objects.requireNonNull(faultHook, "faultHook")); + } + + private void append(EncodedBundle bundle, long checkpointSequence, + byte[] commonTargetDigest, SyncFaultHook faultHook) throws IOException { + requireUsable(); + Objects.requireNonNull(bundle, "bundle"); + List frames = bundle.getLanes().stream().map(EncodedLane::getFrame) + .collect(Collectors.toList()); + DecodedBundle decoded = codec.decode(frames); + BlockSnapshotMeta meta = decoded.getDiff().getMeta(); + validateNext(meta, frames.get(0)); + if (checkpointSequence >= 0 && activeCheckpointSequence < 0) { + activeCheckpointSequence = checkpointSequence; + activeCommonTargetDigest = Arrays.copyOf(commonTargetDigest, commonTargetDigest.length); + } + try { + for (EncodedLane lane : bundle.getLanes()) { + if (ByteBuffer.wrap(lane.getFrame()).getShort(COMPRESSION_ID_OFFSET) + != compressionId) { + throw new IllegalArgumentException("State Archive writer compression mismatch"); + } + LaneState state = lanes.get(lane.getLaneId()); + if (state != null && StateArchiveSegmentFormatV3.shouldRotate( + state.blockFrameCount, state.dataEndOffset, rotationTargetBytes)) { + if (checkpointSequence >= 0) { + addPendingTail(markRotation(state, checkpointSequence, + commonTargetDigest, faultHook)); + } + seal(state); + state = null; + } + if (state == null) { + state = openNewSegment(lane.getLaneId(), meta.getBlockNumber(), + previousHistoryDigest(lane.getFrame())); + } + appendLaneFrame(state, meta, lane); + } + } catch (IOException | RuntimeException failure) { + failed = true; + throw failure; + } + appendHead = meta; + resultHistoryDigest = decoded.getResultHistoryDigest(); + } + + public synchronized BlockSnapshotMeta getAppendHead() { + return appendHead; + } + + public synchronized byte[] getResultHistoryDigest() { + return resultHistoryDigest == null ? Arrays.copyOf(baselineHistoryDigest, + baselineHistoryDigest.length) : Arrays.copyOf(resultHistoryDigest, + resultHistoryDigest.length); + } + + public synchronized List getCurrentSegments() { + return lanes.values().stream().sorted(Comparator.comparingInt(state -> state.laneId)) + .map(LaneState::currentMap).collect(Collectors.toList()); + } + + public synchronized List getSealedSegments() { + return Collections.unmodifiableList(new ArrayList<>(sealedSegments)); + } + + public synchronized ArchiveDurabilityProof getLastDurabilityProof() { + return lastDurabilityProof; + } + + /** Forces one complete five-lane marker barrier and returns proof only after reread. */ + public synchronized ArchiveDurabilityProof sync(long checkpointSequence, + RecoveryPoint target, byte[] commonTargetDigest) throws IOException { + return sync(checkpointSequence, target, commonTargetDigest, SyncFaultHook.NONE); + } + + synchronized ArchiveDurabilityProof sync(long checkpointSequence, + RecoveryPoint target, byte[] commonTargetDigest, SyncFaultHook faultHook) + throws IOException { + requireUsable(); + RecoveryPoint admittedTarget = Objects.requireNonNull(target, "target"); + byte[] admittedCommonDigest = requireHash(commonTargetDigest, "Common target digest"); + SyncFaultHook admittedFaultHook = Objects.requireNonNull(faultHook, "faultHook"); + if (appendHead == null) { + throw new IllegalStateException("State Archive has no bundle to sync"); + } + requireSamePoint(recoveryPoint(appendHead, resultHistoryDigest), admittedTarget, + "durability target"); + if (rotationWithUnsyncedData) { + throw new IllegalStateException( + "State Archive sync cannot prove a segment rotated before its marker"); + } + List ordered = lanes.values().stream() + .sorted(Comparator.comparingInt(state -> state.laneId)) + .collect(Collectors.toList()); + if (ordered.size() != StateArchiveFileFormatV3.fiveLaneIds().length) { + throw new IllegalStateException("State Archive sync requires five current lanes"); + } + boolean hasUnsynced = !pendingRotationTails.isEmpty() || ordered.stream() + .anyMatch(state -> state.blockFrameCount > state.markedBlockFrameCount); + if (!hasUnsynced) { + if (lastDurabilityProof == null + || !Arrays.equals(lastDurabilityProof.getCommonTargetDigest(), + admittedCommonDigest)) { + throw new IllegalStateException("State Archive sync has no new bundle range"); + } + requireSamePoint(lastDurabilityProof.getTarget(), admittedTarget, + "reused durability target"); + if (lastDurabilityProofRecovered) { + reforceRecoveredProof(lastDurabilityProof, admittedFaultHook); + lastDurabilityProofRecovered = false; + } + return lastDurabilityProof; + } + if (checkpointSequence < 0 || lastDurabilityProof != null + && checkpointSequence <= lastDurabilityProof.getCheckpointSequence() + || ordered.stream().anyMatch(state -> checkpointSequence < state.lastCheckpointSequence)) { + throw new IllegalArgumentException("Invalid State Archive checkpoint sequence"); + } + if (activeCheckpointSequence >= 0 + && (activeCheckpointSequence != checkpointSequence + || !Arrays.equals(activeCommonTargetDigest, admittedCommonDigest))) { + throw new IllegalArgumentException("State Archive checkpoint barrier identity mismatch"); + } + List markers = new ArrayList<>(); + try { + for (LaneState state : ordered) { + long blockCount = state.blockFrameCount - state.markedBlockFrameCount; + if (blockCount == 0 && state.lastCheckpointSequence == checkpointSequence + && state.lastBlock == admittedTarget.getBlockNumber() + && Arrays.equals(state.lastCommonTargetDigest, admittedCommonDigest)) { + addPendingTail(new FileTailProof(state.laneId, state.segmentSeq, + state.lastMarkerOffset, StateArchiveFileFormatV3.MARKER_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH, + state.lastMarkerEndOffset, state.previousMarkerDigest)); + continue; + } + if (blockCount <= 0 || state.lastBlock != admittedTarget.getBlockNumber()) { + throw new IllegalStateException("State Archive sync lane range mismatch"); + } + long firstBlock = state.lastBlock - blockCount + 1; + long encodedBytes = state.encodedBlockFrameBytes - state.markedEncodedBytes; + long markerOffset = state.dataEndOffset; + long markerEnd = markerOffset + StateArchiveFileFormatV3.MARKER_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH; + DurableMarker marker = new DurableMarker(checkpointSequence, firstBlock, + admittedTarget.getBlockNumber(), firstBlock, admittedTarget.getBlockNumber(), + admittedTarget.getBlockHash(), admittedTarget.getResultHistoryDigest(), + state.lastMarkerEndOffset, markerEnd, blockCount, + state.logicalPayloadBytes - state.markedLogicalBytes, encodedBytes, + state.previousMarkerDigest, admittedCommonDigest, state.laneId, + state.segmentSeq); + byte[] encoded = StateArchiveSegmentFormatV3.encodeDurableMarker(marker); + state.data.position(markerOffset); + writeFully(state.data, ByteBuffer.wrap(encoded)); + state.contentDigest.update(encoded); + state.dataEndOffset = markerEnd; + markers.add(new MarkerWrite(state, markerOffset, encoded, + StateArchiveSegmentFormatV3.decodeDurableMarker(encoded))); + admittedFaultHook.after(SyncStage.MARKER_WRITTEN, state.laneId); + } + for (LaneState state : ordered) { + state.data.force(false); + admittedFaultHook.after(SyncStage.DATA_FORCED, state.laneId); + } + List tails = new ArrayList<>(pendingRotationTails); + for (MarkerWrite write : markers) { + byte[] reread = readExact(write.state.data, write.offset, write.encoded.length); + DurableMarker decoded = StateArchiveSegmentFormatV3.decodeDurableMarker(reread); + if (!Arrays.equals(reread, write.encoded) + || write.state.data.size() != decoded.getMarkerEndOffset()) { + throw new IllegalArgumentException("State Archive durable marker reread mismatch"); + } + tails.add(new FileTailProof(decoded.getLaneId(), decoded.getSegmentSeq(), + write.offset, reread.length, decoded.getMarkerEndOffset(), + decoded.getEncodedFrameDigest())); + admittedFaultHook.after(SyncStage.MARKER_VERIFIED, write.state.laneId); + } + tails.sort(Comparator.comparingInt(FileTailProof::getLaneId) + .thenComparingLong(FileTailProof::getSegmentSeq)); + ArchiveDurabilityProof proof = new ArchiveDurabilityProof(checkpointSequence, + admittedTarget, admittedCommonDigest, tails); + verifyDurabilityProof(proof); + for (MarkerWrite write : markers) { + LaneState state = write.state; + state.markedBlockFrameCount = state.blockFrameCount; + state.markedLogicalBytes = state.logicalPayloadBytes; + state.markedEncodedBytes = state.encodedBlockFrameBytes; + state.lastMarkerEndOffset = write.marker.getMarkerEndOffset(); + state.previousMarkerDigest = write.marker.getEncodedFrameDigest(); + state.lastCheckpointSequence = checkpointSequence; + state.lastCommonTargetDigest = admittedCommonDigest; + } + lastDurabilityProof = proof; + pendingRotationTails.clear(); + activeCheckpointSequence = -1; + activeCommonTargetDigest = null; + admittedFaultHook.after(SyncStage.PROOF_READY, -1); + return proof; + } catch (IOException | RuntimeException failure) { + failed = true; + throw failure; + } + } + + /** Reopens proof bytes by exact lane/segment/offset; ordinary readable bytes are insufficient. */ + public synchronized void verifyDurabilityProof(ArchiveDurabilityProof proof) + throws IOException { + ArchiveDurabilityProof admitted = Objects.requireNonNull(proof, "proof"); + int activeLane = -1; + long previousBlock = -1; + DurableMarker lastMarker = null; + for (FileTailProof tail : admitted.getFileTails()) { + if (tail.getLaneId() != activeLane) { + requireFinalProofMarker(lastMarker, admitted); + activeLane = tail.getLaneId(); + previousBlock = -1; + } + Path path = dataPath(tail.getLaneId(), tail.getSegmentSeq()); + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { + byte[] encoded = readExact(channel, tail.getMarkerOffset(), tail.getMarkerLength()); + DurableMarker marker = StateArchiveSegmentFormatV3.decodeDurableMarker(encoded); + if (channel.size() < tail.getMarkerEndOffset() + || marker.getCheckpointSequence() != admitted.getCheckpointSequence() + || marker.getLaneId() != tail.getLaneId() + || marker.getSegmentSeq() != tail.getSegmentSeq() + || marker.getMarkerEndOffset() != tail.getMarkerEndOffset() + || marker.getLastBlock() <= previousBlock + || marker.getLastBlock() > admitted.getTarget().getBlockNumber() + || !Arrays.equals(marker.getCommonTargetDigest(), + admitted.getCommonTargetDigest()) + || !Arrays.equals(marker.getEncodedFrameDigest(), tail.getMarkerDigest())) { + throw new IllegalArgumentException("State Archive durability proof mismatch"); + } + previousBlock = marker.getLastBlock(); + lastMarker = marker; + } + } + requireFinalProofMarker(lastMarker, admitted); + } + + private static void requireFinalProofMarker(DurableMarker marker, + ArchiveDurabilityProof proof) { + if (marker != null && (marker.getLastBlock() != proof.getTarget().getBlockNumber() + || !Arrays.equals(marker.getLastBlockHash(), proof.getTarget().getBlockHash()) + || !Arrays.equals(marker.getResultHistoryDigest(), + proof.getTarget().getResultHistoryDigest()))) { + throw new IllegalArgumentException("State Archive durability proof final tail mismatch"); + } + } + + private void validateNext(BlockSnapshotMeta meta, byte[] firstFrame) { + byte[] previousDigest = previousHistoryDigest(firstFrame); + if (appendHead == null) { + if (!Arrays.equals(previousDigest, baselineHistoryDigest)) { + throw new IllegalArgumentException("State Archive first bundle history mismatch"); + } + return; + } + if (meta.getBlockNumber() != appendHead.getBlockNumber() + 1 + || meta.getEpoch() != appendHead.getEpoch() + 1 + || !Arrays.equals(meta.getParentHash(), appendHead.getBlockHash()) + || !Arrays.equals(previousDigest, resultHistoryDigest)) { + throw new IllegalArgumentException("State Archive bundle is not expected-next"); + } + } + + private LaneState openNewSegment(int laneId, long firstBlock, + byte[] previousHistory) throws IOException { + LaneState prior = lanes.get(laneId); + long sequence = prior == null ? nextSequence(laneId) : prior.segmentSeq + 1; + byte[] previousSegment = sequence == 0 + ? StateArchiveSegmentFormatV3.laneBaselineDigest(laneId) + : previousChainDigest(laneId, sequence - 1); + SegmentHeader header = new SegmentHeader(laneId, sequence, firstBlock, + previousSegment, previousHistory, compressionId); + byte[] headerBytes = StateArchiveSegmentFormatV3.encodeHeader(header); + SegmentHeader decodedHeader = StateArchiveSegmentFormatV3.decodeHeader(headerBytes); + Path dataPath = dataPath(laneId, sequence); + Path indexPath = indexPath(laneId, sequence); + Files.createDirectories(dataPath.getParent()); + FileChannel data = FileChannel.open(dataPath, StandardOpenOption.CREATE_NEW, + StandardOpenOption.READ, StandardOpenOption.WRITE); + FileChannel index = null; + try { + writeFully(data, ByteBuffer.wrap(headerBytes)); + data.force(false); + BlockIndexHeader indexHeader = new BlockIndexHeader(laneId, sequence, + decodedHeader.getHeaderDigest()); + index = FileChannel.open(indexPath, StandardOpenOption.CREATE_NEW, + StandardOpenOption.READ, StandardOpenOption.WRITE); + writeFully(index, ByteBuffer.wrap( + StateArchiveSegmentFormatV3.encodeBlockIndexHeader(indexHeader))); + index.force(false); + syncDirectory(dataPath.getParent()); + LaneState state = new LaneState(laneId, sequence, firstBlock, + decodedHeader.getHeaderDigest(), previousHistory, data, index, + newContentDigest(headerBytes)); + lanes.put(laneId, state); + return state; + } catch (IOException | RuntimeException failure) { + data.close(); + if (index != null) { + index.close(); + } + throw failure; + } + } + + private void appendLaneFrame(LaneState state, BlockSnapshotMeta meta, EncodedLane lane) + throws IOException { + byte[] frame = lane.getFrame(); + if (frame.length > StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES) { + throw new IllegalArgumentException("State Archive block frame exceeds 64 MiB"); + } + long offset = state.dataEndOffset; + state.data.position(offset); + writeBuffered(state.data, frame, state.appendBuffer); + state.contentDigest.update(frame); + byte[] indexEntry = StateArchiveSegmentFormatV3.encodeBlockIndexEntry( + new BlockIndexEntry(meta.getBlockNumber(), offset, frame.length, + ByteBuffer.wrap(lane.getEncodedFrameDigest()).getLong())); + state.index.position(state.index.size()); + writeFully(state.index, ByteBuffer.wrap(indexEntry)); + state.record(meta, frame, lane.getEncodedFrameDigest()); + } + + private FileTailProof markRotation(LaneState state, long checkpointSequence, + byte[] commonTargetDigest, SyncFaultHook faultHook) throws IOException { + if (state.lastMeta == null) { + throw new IllegalStateException("State Archive rotation has no unmarked bundle range"); + } + if (state.markedBlockFrameCount == state.blockFrameCount) { + if (state.lastCheckpointSequence != checkpointSequence + || !Arrays.equals(state.lastCommonTargetDigest, commonTargetDigest)) { + throw new IllegalStateException("State Archive rotation marker identity differs"); + } + return new FileTailProof(state.laneId, state.segmentSeq, state.lastMarkerOffset, + StateArchiveFileFormatV3.MARKER_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH, + state.lastMarkerEndOffset, state.previousMarkerDigest); + } + long blockCount = state.blockFrameCount - state.markedBlockFrameCount; + long firstBlock = state.lastBlock - blockCount + 1; + long markerOffset = state.dataEndOffset; + long markerEnd = markerOffset + StateArchiveFileFormatV3.MARKER_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH; + DurableMarker marker = new DurableMarker(checkpointSequence, firstBlock, + state.lastBlock, firstBlock, state.lastBlock, state.lastMeta.getBlockHash(), + state.endHistoryDigest, state.lastMarkerEndOffset, markerEnd, blockCount, + state.logicalPayloadBytes - state.markedLogicalBytes, + state.encodedBlockFrameBytes - state.markedEncodedBytes, + state.previousMarkerDigest, commonTargetDigest, state.laneId, state.segmentSeq); + byte[] encoded = StateArchiveSegmentFormatV3.encodeDurableMarker(marker); + state.data.position(markerOffset); + writeFully(state.data, ByteBuffer.wrap(encoded)); + state.contentDigest.update(encoded); + state.dataEndOffset = markerEnd; + faultHook.after(SyncStage.MARKER_WRITTEN, state.laneId); + state.data.force(false); + faultHook.after(SyncStage.DATA_FORCED, state.laneId); + byte[] reread = readExact(state.data, markerOffset, encoded.length); + DurableMarker decoded = StateArchiveSegmentFormatV3.decodeDurableMarker(reread); + if (!Arrays.equals(reread, encoded) || state.data.size() != decoded.getMarkerEndOffset()) { + throw new IllegalArgumentException("State Archive rotation marker reread mismatch"); + } + faultHook.after(SyncStage.MARKER_VERIFIED, state.laneId); + state.markedBlockFrameCount = state.blockFrameCount; + state.markedLogicalBytes = state.logicalPayloadBytes; + state.markedEncodedBytes = state.encodedBlockFrameBytes; + state.lastMarkerEndOffset = decoded.getMarkerEndOffset(); + state.lastMarkerOffset = markerOffset; + state.previousMarkerDigest = decoded.getEncodedFrameDigest(); + state.lastCheckpointSequence = checkpointSequence; + state.lastCommonTargetDigest = commonTargetDigest; + return new FileTailProof(state.laneId, state.segmentSeq, markerOffset, + reread.length, decoded.getMarkerEndOffset(), decoded.getEncodedFrameDigest()); + } + + private void addPendingTail(FileTailProof tail) { + boolean exists = pendingRotationTails.stream().anyMatch(candidate -> + candidate.getLaneId() == tail.getLaneId() + && candidate.getSegmentSeq() == tail.getSegmentSeq()); + if (!exists) { + pendingRotationTails.add(tail); + } + } + + private void seal(LaneState state) throws IOException { + if (state.markedBlockFrameCount != state.blockFrameCount) { + rotationWithUnsyncedData = true; + } + byte[] contentDigest = state.contentDigest.digest(); + int sealLength = StateArchiveFileFormatV3.SEAL_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH; + SegmentSeal seal = new SegmentSeal(state.laneId, state.segmentSeq, + state.firstBlock, state.lastBlock, state.blockFrameCount, state.entryCount, + state.logicalPayloadBytes, state.encodedBlockFrameBytes, state.dataEndOffset, + state.dataEndOffset + sealLength, state.firstFrameDigest, + state.lastFrameDigest, state.startHistoryDigest, state.endHistoryDigest, + contentDigest); + byte[] encodedSeal = StateArchiveSegmentFormatV3.encodeSeal(seal); + SegmentSeal decodedSeal = StateArchiveSegmentFormatV3.decodeSeal(encodedSeal); + state.data.position(state.dataEndOffset); + writeFully(state.data, ByteBuffer.wrap(encodedSeal)); + state.data.force(false); + state.index.force(false); + if (state.index.size() != StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + + state.blockFrameCount * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH) { + throw new IllegalStateException("State Archive sealed block index length mismatch"); + } + sealedSegments.add(new SealedSegment(state.laneId, state.segmentSeq, + state.firstBlock, state.lastBlock, state.blockFrameCount, + state.dataEndOffset + sealLength, state.index.size(), state.headerDigest, + contentDigest, decodedSeal.getEncodedFrameDigest(), new byte[32])); + state.close(); + lanes.remove(state.laneId); + } + + private void reopen(Long recoveryBoundary) throws IOException { + List dataFiles = listDataFiles(); + if (dataFiles.isEmpty()) { + resultHistoryDigest = Arrays.copyOf(baselineHistoryDigest, + baselineHistoryDigest.length); + if (activeRecoveryIntent != null) { + if (activeRecoveryIntent.getTarget() != null) { + throw new IllegalArgumentException("State Archive recovery target is unavailable"); + } + try { + verifyRecoveredIntent(activeRecoveryIntent); + recoveryFaultHook.after(RecoveryStage.TARGET_VERIFIED, -1); + clearIntent(); + activeRecoveryIntent = null; + } catch (IOException | RuntimeException failure) { + closeAfterFailure(failure); + throw failure; + } + } else if (activeRecoveryRequest != null && !activeRecoveryRequest.prototype + && activeRecoveryRequest.commonCommitted != null) { + throw new IllegalArgumentException("State Archive cannot verify Common committed"); + } + return; + } + Map> bundles = new java.util.TreeMap<>(); + Map expectedSequence = new HashMap<>(); + Map expectedPreviousChain = new HashMap<>(); + List scannedSegments = new ArrayList<>(); + boolean recovering = recoveryBoundary != null; + for (Path path : dataFiles) { + ParsedName name = parseName(path); + long expected = expectedSequence.getOrDefault(name.laneId, 0L); + if (name.segmentSeq != expected) { + throw new IllegalArgumentException("Non-contiguous State Archive segment sequence"); + } + expectedSequence.put(name.laneId, expected + 1); + ScannedSegment scanned = scanSegment(path, name, bundles, recovering); + scannedSegments.add(scanned); + byte[] expectedPrevious = name.segmentSeq == 0 + ? StateArchiveSegmentFormatV3.laneBaselineDigest(name.laneId) + : expectedPreviousChain.get(name.laneId); + if (!Arrays.equals(scanned.header.getPreviousSegmentDigest(), expectedPrevious)) { + throw new IllegalArgumentException("State Archive previous segment chain mismatch"); + } + if (!recovering && scanned.seal == null) { + if (lanes.containsKey(name.laneId)) { + throw new IllegalArgumentException("Multiple open State Archive lane segments"); + } + lanes.put(name.laneId, scanned.openState()); + } else if (scanned.seal != null) { + expectedPreviousChain.put(name.laneId, scanned.chainDigest()); + sealedSegments.add(scanned.sealedMap()); + } + } + long commonHead = rebuildBundleHead(bundles, recoveryBoundary); + if (appendHead != null) { + for (LaneState state : lanes.values()) { + if (state.lastBlock == appendHead.getBlockNumber()) { + state.lastMeta = appendHead; + } + } + } + if (!recovering) { + rebuildLastDurabilityProof(scannedSegments, bundles); + } + if (recovering) { + Intent intent = activeRecoveryIntent; + if (intent == null && activeRecoveryRequest != null + && !activeRecoveryRequest.prototype) { + RecoveryPoint targetPoint = appendHead == null ? null + : recoveryPoint(appendHead, resultHistoryDigest); + verifyRecoveryPoint(activeRecoveryRequest.authorizedCeiling, bundles, + "authorized ceiling", false); + if (activeRecoveryRequest.commonCommitted != null) { + verifyRecoveryPoint(activeRecoveryRequest.commonCommitted, bundles, + "Common committed", true); + } + List laneTargets = buildLaneTargets(scannedSegments, commonHead); + if (laneTargets.stream().noneMatch(StateArchiveFiveLaneSegmentWriterV3::mutates)) { + intent = null; + } else { + intent = new Intent(baselineHistoryDigest, + activeRecoveryRequest.authorizedCeiling, + activeRecoveryRequest.commonCommitted, targetPoint, + laneTargets); + try { + persistIntent(intent); + } catch (IOException | RuntimeException failure) { + closeScannedAfterFailure(scannedSegments, failure); + throw failure; + } + intent = loadIntent(); + activeRecoveryIntent = intent; + } + } + if (intent != null) { + long intendedHead = intent.getTarget() == null ? -1 + : intent.getTarget().getBlockNumber(); + if (commonHead != intendedHead) { + closeScanned(scannedSegments); + throw new IllegalArgumentException("State Archive recovery target is unavailable"); + } + verifyIntentSources(intent, scannedSegments); + } + for (ScannedSegment scanned : scannedSegments) { + if (scanned.seal != null && scanned.lastBlock > commonHead) { + closeScanned(scannedSegments); + throw new IllegalArgumentException( + "Authorized State Archive recovery boundary crosses a sealed segment"); + } + } + try { + for (ScannedSegment scanned : scannedSegments) { + if (scanned.seal == null) { + scanned.repairTo(commonHead, recoveryFaultHook); + } + } + } catch (IOException | RuntimeException failure) { + closeScannedAfterFailure(scannedSegments, failure); + throw failure; + } + closeScanned(scannedSegments); + lanes.clear(); + sealedSegments.clear(); + appendHead = null; + resultHistoryDigest = Arrays.copyOf(baselineHistoryDigest, + baselineHistoryDigest.length); + reopen(null); + if (intent != null) { + try { + verifyRecoveredIntent(intent); + recoveryFaultHook.after(RecoveryStage.TARGET_VERIFIED, -1); + clearIntent(); + activeRecoveryIntent = null; + } catch (IOException | RuntimeException failure) { + closeAfterFailure(failure); + throw failure; + } + } + } + } + + private void rebuildLastDurabilityProof(List scannedSegments, + Map> bundles) { + if (appendHead == null) { + return; + } + long latestSequence = scannedSegments.stream() + .filter(scanned -> scanned.lastMarker != null) + .mapToLong(scanned -> scanned.lastMarker.marker.getCheckpointSequence()) + .max().orElse(-1); + if (latestSequence < 0) { + return; + } + List latest = scannedSegments.stream() + .filter(scanned -> scanned.lastMarker != null + && scanned.lastMarker.marker.getCheckpointSequence() == latestSequence) + .map(scanned -> scanned.lastMarker) + .sorted(Comparator.comparingInt((ScannedMarker marker) -> marker.marker.getLaneId()) + .thenComparingLong(marker -> marker.marker.getSegmentSeq())) + .collect(Collectors.toList()); + DurableMarker identity = latest.get(0).marker; + List tails = new ArrayList<>(); + for (ScannedMarker candidate : latest) { + DurableMarker marker = candidate.marker; + if (marker.getCheckpointSequence() != identity.getCheckpointSequence() + || !Arrays.equals(marker.getCommonTargetDigest(), + identity.getCommonTargetDigest())) { + return; + } + tails.add(new FileTailProof(marker.getLaneId(), marker.getSegmentSeq(), + candidate.offset, StateArchiveFileFormatV3.MARKER_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH, + marker.getMarkerEndOffset(), marker.getEncodedFrameDigest())); + } + ArchiveDurabilityProof rebuilt; + try { + DurableMarker finalIdentity = null; + int laneCount = 0; + for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { + DurableMarker laneFinal = null; + for (ScannedMarker candidate : latest) { + if (candidate.marker.getLaneId() == laneId) { + laneFinal = candidate.marker; + } + } + if (laneFinal == null) { + throw new IllegalArgumentException("Incomplete State Archive marker group"); + } + laneCount++; + if (finalIdentity == null) { + finalIdentity = laneFinal; + } else if (laneFinal.getLastBlock() != finalIdentity.getLastBlock() + || !Arrays.equals(laneFinal.getLastBlockHash(), finalIdentity.getLastBlockHash()) + || !Arrays.equals(laneFinal.getResultHistoryDigest(), + finalIdentity.getResultHistoryDigest())) { + throw new IllegalArgumentException("Incomplete State Archive marker identity"); + } + } + if (laneCount != StateArchiveFileFormatV3.fiveLaneIds().length) { + throw new IllegalArgumentException("Incomplete State Archive marker lanes"); + } + RecoveryPoint target = recoveryPointAt(bundles, finalIdentity.getLastBlock()); + if (target == null + || !Arrays.equals(finalIdentity.getLastBlockHash(), target.getBlockHash()) + || !Arrays.equals(finalIdentity.getResultHistoryDigest(), + target.getResultHistoryDigest())) { + throw new IllegalArgumentException("Incomplete State Archive marker target"); + } + rebuilt = new ArchiveDurabilityProof(identity.getCheckpointSequence(), + target, identity.getCommonTargetDigest(), tails); + } catch (IllegalArgumentException incomplete) { + pendingRotationTails.addAll(tails); + activeCheckpointSequence = identity.getCheckpointSequence(); + activeCommonTargetDigest = identity.getCommonTargetDigest(); + return; + } + lastDurabilityProof = rebuilt; + lastDurabilityProofRecovered = true; + } + + private void reforceRecoveredProof(ArchiveDurabilityProof proof, + SyncFaultHook faultHook) throws IOException { + int previousLane = -1; + long previousSegment = -1; + for (FileTailProof tail : proof.getFileTails()) { + if (tail.getLaneId() == previousLane && tail.getSegmentSeq() == previousSegment) { + continue; + } + LaneState current = lanes.get(tail.getLaneId()); + if (current != null && current.segmentSeq == tail.getSegmentSeq()) { + current.data.force(false); + } else { + try (FileChannel channel = FileChannel.open( + dataPath(tail.getLaneId(), tail.getSegmentSeq()), StandardOpenOption.WRITE)) { + channel.force(false); + } + } + faultHook.after(SyncStage.DATA_FORCED, tail.getLaneId()); + previousLane = tail.getLaneId(); + previousSegment = tail.getSegmentSeq(); + } + verifyDurabilityProof(proof); + } + + private RecoveryPoint recoveryPointAt(Map> bundles, + long blockNumber) { + Map laneFrames = bundles.get(blockNumber); + if (laneFrames == null || laneFrames.size() != StateArchiveFileFormatV3.fiveLaneIds().length) { + return null; + } + List frames = new ArrayList<>(); + for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { + frames.add(laneFrames.get(laneId)); + } + DecodedBundle decoded = codec.decode(frames); + return recoveryPoint(decoded.getDiff().getMeta(), decoded.getResultHistoryDigest()); + } + + private List buildLaneTargets(List scannedSegments, + long commonHead) throws IOException { + List targets = new ArrayList<>(); + for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { + ScannedSegment open = null; + for (ScannedSegment scanned : scannedSegments) { + if (scanned.header.getLaneId() == laneId && scanned.seal == null) { + open = scanned; + } + } + targets.add(open == null ? missingLaneTarget(laneId) : open.laneTarget(commonHead)); + } + return targets; + } + + private static LaneTarget missingLaneTarget(int laneId) { + byte[] zero = new byte[StateArchiveFileFormatV3.HASH_LENGTH]; + return new LaneTarget(laneId, StateArchiveFiveLaneRecoveryIntentV3.SOURCE_PAIR_MISSING, + StateArchiveFiveLaneRecoveryIntentV3.NO_TARGET_SEGMENT, 0, 0, + StateArchiveFiveLaneRecoveryIntentV3.NO_TARGET_SEGMENT, 0, 0, + zero, zero, zero); + } + + private static boolean mutates(LaneTarget target) { + return (target.getActionFlags() & (StateArchiveFiveLaneRecoveryIntentV3.DATA_TRUNCATE + | StateArchiveFiveLaneRecoveryIntentV3.DELETE_PAIR + | StateArchiveFiveLaneRecoveryIntentV3.INDEX_REPLACE)) != 0; + } + + private void verifyIntentSources(Intent intent, List scannedSegments) + throws IOException { + for (LaneTarget target : intent.getLanes()) { + ScannedSegment source = null; + for (ScannedSegment scanned : scannedSegments) { + if (scanned.seal == null && scanned.header.getLaneId() == target.getLaneId() + && scanned.header.getSegmentSeq() == target.getSourceSegmentSeq()) { + source = scanned; + } + } + if ((target.getActionFlags() + & StateArchiveFiveLaneRecoveryIntentV3.SOURCE_PAIR_MISSING) != 0) { + if (source != null || hasLaneIndexFile(target.getLaneId())) { + throw new IllegalArgumentException("State Archive missing-source lane appeared"); + } + continue; + } + if (source == null) { + if ((target.getActionFlags() & StateArchiveFiveLaneRecoveryIntentV3.DELETE_PAIR) != 0 + && !Files.exists(dataPath(target.getLaneId(), target.getSourceSegmentSeq())) + && !Files.exists(indexPath(target.getLaneId(), target.getSourceSegmentSeq()))) { + continue; + } + throw new IllegalArgumentException("State Archive recovery source is missing"); + } + long dataSize = source.data.size(); + long indexSize = source.index == null ? 0 : source.index.size(); + boolean deleting = (target.getActionFlags() + & StateArchiveFiveLaneRecoveryIntentV3.DELETE_PAIR) != 0; + boolean indexSizeAllowed = indexSize == target.getOriginalIndexEnd() + || (target.getActionFlags() & StateArchiveFiveLaneRecoveryIntentV3.INDEX_REPLACE) != 0 + && indexSize == target.getTargetIndexEnd() + || deleting && indexSize == 0; + boolean targetDataMatches = deleting + || Arrays.equals(digestFilePrefix(StateArchiveFileFormatV3.RECOVERY_DATA_PREFIX_DOMAIN, + source.data, target.getTargetDataEnd()), target.getTargetDataPrefixDigest()); + boolean targetIndexMatches = deleting || indexSize != target.getTargetIndexEnd() + || source.index != null && Arrays.equals(digestFilePrefix( + StateArchiveFileFormatV3.RECOVERY_INDEX_FILE_DOMAIN, source.index, + target.getTargetIndexEnd()), target.getTargetIndexFileDigest()); + if (!Arrays.equals(source.header.getHeaderDigest(), + target.getSourceSegmentHeaderDigest()) + || dataSize < target.getTargetDataEnd() + || dataSize > target.getOriginalDataEnd() + || !indexSizeAllowed || !targetDataMatches || !targetIndexMatches) { + throw new IllegalArgumentException("State Archive recovery source drifted"); + } + } + } + + private void verifyRecoveryPoint(RecoveryPoint expected, + Map> bundles, String name, boolean required) { + Map laneFrames = bundles.get(expected.getBlockNumber()); + if (laneFrames == null || laneFrames.size() != StateArchiveFileFormatV3.fiveLaneIds().length) { + if (required) { + throw new IllegalArgumentException("State Archive cannot verify " + name); + } + return; + } + List frames = new ArrayList<>(); + for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { + frames.add(laneFrames.get(laneId)); + } + DecodedBundle decoded = codec.decode(frames); + RecoveryPoint actual = recoveryPoint(decoded.getDiff().getMeta(), + decoded.getResultHistoryDigest()); + requireSamePoint(actual, expected, name); + } + + private void verifyRecoveredIntent(Intent intent) { + RecoveryPoint target = intent.getTarget(); + if (target == null) { + if (appendHead != null) { + throw new IllegalArgumentException("State Archive baseline recovery retained blocks"); + } + } else { + if (appendHead == null) { + throw new IllegalArgumentException("State Archive recovery target is missing"); + } + requireSamePoint(recoveryPoint(appendHead, resultHistoryDigest), target, + "recovered target"); + } + RecoveryPoint common = intent.getCommonCommitted(); + if (common != null && (appendHead == null + || appendHead.getBlockNumber() < common.getBlockNumber())) { + throw new IllegalArgumentException("State Archive recovery fell below Common"); + } + for (LaneTarget lane : intent.getLanes()) { + verifyLaneTarget(lane); + } + } + + private void verifyLaneTarget(LaneTarget target) { + if ((target.getActionFlags() & StateArchiveFiveLaneRecoveryIntentV3.SOURCE_PAIR_MISSING) + != 0) { + try { + if (hasLaneIndexFile(target.getLaneId())) { + throw new IllegalArgumentException( + "State Archive missing-source recovery index appeared"); + } + } catch (IOException failure) { + throw new IllegalArgumentException( + "Cannot verify State Archive missing-source recovery lane", failure); + } + return; + } + Path data = dataPath(target.getLaneId(), target.getSourceSegmentSeq()); + Path index = indexPath(target.getLaneId(), target.getSourceSegmentSeq()); + if ((target.getActionFlags() & StateArchiveFiveLaneRecoveryIntentV3.DELETE_PAIR) != 0) { + if (Files.exists(data) || Files.exists(index)) { + throw new IllegalArgumentException("State Archive deleted recovery pair remains"); + } + return; + } + try { + try (FileChannel dataChannel = FileChannel.open(data, StandardOpenOption.READ); + FileChannel indexChannel = FileChannel.open(index, StandardOpenOption.READ)) { + if (dataChannel.size() != target.getTargetDataEnd() + || indexChannel.size() != target.getTargetIndexEnd() + || !Arrays.equals(digestFilePrefix( + StateArchiveFileFormatV3.RECOVERY_DATA_PREFIX_DOMAIN, dataChannel, + target.getTargetDataEnd()), target.getTargetDataPrefixDigest()) + || !Arrays.equals(digestFilePrefix( + StateArchiveFileFormatV3.RECOVERY_INDEX_FILE_DOMAIN, indexChannel, + target.getTargetIndexEnd()), target.getTargetIndexFileDigest())) { + throw new IllegalArgumentException("State Archive recovered lane target mismatch"); + } + } + } catch (IOException failure) { + throw new IllegalArgumentException("Cannot verify State Archive recovered lane", failure); + } + } + + private static RecoveryPoint recoveryPoint(BlockSnapshotMeta meta, byte[] historyDigest) { + return new RecoveryPoint(meta.getEpoch(), meta.getBlockNumber(), meta.getTimestamp(), + meta.getBlockHash(), meta.getParentHash(), historyDigest); + } + + private static void requireSamePoint(RecoveryPoint actual, RecoveryPoint expected, + String name) { + if (actual.getEpoch() != expected.getEpoch() + || actual.getBlockNumber() != expected.getBlockNumber() + || actual.getTimestamp() != expected.getTimestamp() + || !Arrays.equals(actual.getBlockHash(), expected.getBlockHash()) + || !Arrays.equals(actual.getParentHash(), expected.getParentHash()) + || !Arrays.equals(actual.getResultHistoryDigest(), expected.getResultHistoryDigest())) { + throw new IllegalArgumentException("State Archive " + name + " mismatch"); + } + } + + private ScannedSegment scanSegment(Path path, ParsedName name, + Map> bundles, boolean recovering) throws IOException { + FileChannel data = FileChannel.open(path, StandardOpenOption.READ, + StandardOpenOption.WRITE); + Path indexPath = indexPath(name.laneId, name.segmentSeq); + FileChannel index = Files.exists(indexPath) ? FileChannel.open(indexPath, + StandardOpenOption.READ, StandardOpenOption.WRITE) : null; + try { + byte[] headerBytes = readExact(data, 0, StateArchiveFileFormatV3.PART_HEADER_LENGTH); + SegmentHeader header = StateArchiveSegmentFormatV3.decodeHeader(headerBytes); + if (header.getLaneId() != name.laneId || header.getSegmentSeq() != name.segmentSeq + || header.getCompressionId() != compressionId) { + throw new IllegalArgumentException("State Archive segment filename/header mismatch"); + } + if (index != null && index.size() >= StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH) { + try { + BlockIndexHeader indexHeader = StateArchiveSegmentFormatV3.decodeBlockIndexHeader( + readExact(index, 0, StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH)); + if (indexHeader.getLaneId() != name.laneId + || indexHeader.getSegmentSeq() != name.segmentSeq + || !Arrays.equals(indexHeader.getDataSegmentHeaderDigest(), + header.getHeaderDigest())) { + throw new IllegalArgumentException("State Archive block index/header mismatch"); + } + } catch (IllegalArgumentException invalidIndexHeader) { + if (!recovering) { + throw invalidIndexHeader; + } + } + } else if (!recovering) { + throw new IllegalArgumentException("Truncated State Archive block index header"); + } + ScannedSegment scanned = new ScannedSegment(path, data, index, indexPath, header, + headerBytes); + long offset = StateArchiveFileFormatV3.PART_HEADER_LENGTH; + while (offset < data.size()) { + if (data.size() - offset < StateArchiveFileFormatV3.FRAME_ENVELOPE_LENGTH) { + if (recovering) { + scanned.tailDamaged = true; + break; + } + throw new IllegalArgumentException("Truncated State Archive frame envelope"); + } + byte[] envelope = readExact(data, offset, + StateArchiveFileFormatV3.FRAME_ENVELOPE_LENGTH); + ByteBuffer fields = ByteBuffer.wrap(envelope); + if (fields.getInt(0) != StateArchiveFileFormatV3.FRAME_MAGIC) { + if (recovering) { + scanned.tailDamaged = true; + break; + } + throw new IllegalArgumentException("State Archive frame magic mismatch"); + } + short frameType = fields.getShort(8); + long totalLength = fields.getLong(16); + if (totalLength <= 0 || totalLength > Integer.MAX_VALUE + || totalLength > data.size() - offset) { + if (recovering) { + scanned.tailDamaged = true; + break; + } + throw new IllegalArgumentException("Truncated State Archive frame"); + } + byte[] frame = readExact(data, offset, (int) totalLength); + if (frameType == StateArchiveFileFormatV3.BLOCK_FRAME_TYPE) { + scanned.addBlock(offset, frame); + byte[] duplicate = bundles.computeIfAbsent(blockNumber(frame), + ignored -> new HashMap<>()).put(name.laneId, frame); + if (duplicate != null) { + throw new IllegalArgumentException("Duplicate State Archive lane block frame"); + } + } else if (frameType == StateArchiveFileFormatV3.DURABLE_MARKER_FRAME_TYPE) { + try { + scanned.addMarker(offset, + StateArchiveSegmentFormatV3.decodeDurableMarker(frame), frame); + } catch (IllegalArgumentException invalidMarker) { + if (!recovering) { + throw invalidMarker; + } + scanned.tailDamaged = true; + break; + } + } else if (frameType == StateArchiveFileFormatV3.PART_SEAL_FRAME_TYPE) { + if (offset + totalLength != data.size()) { + throw new IllegalArgumentException("State Archive seal has trailing bytes"); + } + try { + scanned.setSeal(StateArchiveSegmentFormatV3.decodeSeal(frame)); + } catch (IllegalArgumentException invalidSeal) { + if (!recovering) { + throw invalidSeal; + } + scanned.tailDamaged = true; + break; + } + } else { + if (recovering) { + scanned.tailDamaged = true; + break; + } + throw new IllegalArgumentException("Unknown State Archive frame type"); + } + offset += totalLength; + } + scanned.finish(recovering); + return scanned; + } catch (IOException | RuntimeException failure) { + data.close(); + if (index != null) { + index.close(); + } + throw failure; + } + } + + private long rebuildBundleHead(Map> bundles, + Long recoveryBoundary) { + long commonHead = -1; + for (Map.Entry> entry : bundles.entrySet()) { + if (recoveryBoundary != null && entry.getKey() > recoveryBoundary) { + break; + } + List frames = new ArrayList<>(); + for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { + byte[] frame = entry.getValue().get(laneId); + if (frame == null) { + if (recoveryBoundary != null) { + return commonHead; + } + throw new IllegalArgumentException("Incomplete State Archive five-lane bundle"); + } + frames.add(frame); + } + DecodedBundle decoded; + try { + decoded = codec.decode(frames); + validateNext(decoded.getDiff().getMeta(), frames.get(0)); + } catch (IllegalArgumentException invalidBundle) { + if (recoveryBoundary != null) { + return commonHead; + } + throw invalidBundle; + } + appendHead = decoded.getDiff().getMeta(); + resultHistoryDigest = decoded.getResultHistoryDigest(); + commonHead = appendHead.getBlockNumber(); + } + return commonHead; + } + + private static void closeScanned(List scannedSegments) + throws IOException { + IOException failure = null; + for (ScannedSegment scanned : scannedSegments) { + try { + scanned.close(); + } catch (IOException closeFailure) { + if (failure == null) { + failure = closeFailure; + } else { + failure.addSuppressed(closeFailure); + } + } + } + if (failure != null) { + throw failure; + } + } + + private static void closeScannedAfterFailure(List scannedSegments, + Throwable failure) { + try { + closeScanned(scannedSegments); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + + private void closeAfterFailure(Throwable failure) { + try { + close(); + } catch (IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } + + private long nextSequence(int laneId) { + long next = 0; + for (SealedSegment segment : sealedSegments) { + if (segment.getLaneId() == laneId) { + next = Math.max(next, segment.getSegmentSeq() + 1); + } + } + return next; + } + + private byte[] previousChainDigest(int laneId, long sequence) { + for (SealedSegment segment : sealedSegments) { + if (segment.getLaneId() == laneId && segment.getSegmentSeq() == sequence) { + return StateArchiveSegmentFormatV3.segmentChainDigest(laneId, sequence, + segment.getSegmentHeaderDigest(), segment.getSegmentContentDigest(), + segment.getSealFrameDigest()); + } + } + throw new IllegalStateException("Missing previous State Archive sealed segment"); + } + + private List listDataFiles() throws IOException { + try (Stream paths = Files.walk(segmentRoot)) { + return paths.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith(".dat")) + .sorted(Comparator.comparing((Path path) -> parseName(path).laneId) + .thenComparingLong(path -> parseName(path).segmentSeq)) + .collect(Collectors.toList()); + } + } + + private boolean hasLaneIndexFile(int laneId) throws IOException { + String prefix = String.format("lane-%04d-seg-", laneId); + try (Stream paths = Files.walk(segmentRoot)) { + return paths.filter(Files::isRegularFile) + .map(path -> path.getFileName().toString()) + .anyMatch(name -> name.startsWith(prefix) && name.endsWith(".bidx")); + } + } + + private Path dataPath(int laneId, long sequence) { + return segmentRoot.resolve(String.format("shard-%06d", + sequence / StateArchiveFileFormatV3.SHARD_MAX_SEGMENTS)) + .resolve(String.format("lane-%04d-seg-%020d.dat", laneId, sequence)); + } + + private Path indexPath(int laneId, long sequence) { + String name = dataPath(laneId, sequence).getFileName().toString(); + return dataPath(laneId, sequence).resolveSibling( + name.substring(0, name.length() - 4) + ".bidx"); + } + + private static ParsedName parseName(Path path) { + String name = path.getFileName().toString(); + if (!name.matches("lane-[0-9]{4}-seg-[0-9]{20}\\.dat")) { + throw new IllegalArgumentException("Invalid State Archive segment filename: " + name); + } + try { + return new ParsedName(Integer.parseInt(name.substring(5, 9)), + Long.parseLong(name.substring(14, 34))); + } catch (NumberFormatException failure) { + throw new IllegalArgumentException("Invalid State Archive segment filename: " + name, + failure); + } + } + + private static byte[] previousHistoryDigest(byte[] frame) { + return Arrays.copyOfRange(frame, PREVIOUS_HISTORY_DIGEST_OFFSET, + PREVIOUS_HISTORY_DIGEST_OFFSET + StateArchiveFileFormatV3.HASH_LENGTH); + } + + private static long blockNumber(byte[] frame) { + return ByteBuffer.wrap(frame).getLong(BLOCK_NUMBER_OFFSET); + } + + private static byte[] encodedFrameDigest(byte[] frame) { + return Arrays.copyOfRange(frame, frame.length - ENCODED_DIGEST_FROM_END, + frame.length - ENCODED_DIGEST_FROM_END + StateArchiveFileFormatV3.HASH_LENGTH); + } + + private static MessageDigest newContentDigest(byte[] header) { + MessageDigest digest = sha256(); + digest.update(StateArchiveFileFormatV3.SEGMENT_CONTENT_DOMAIN); + digest.update(header); + return digest; + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException impossible) { + throw new IllegalStateException("SHA-256 is unavailable", impossible); + } + } + + private static byte[] readExact(FileChannel channel, long offset, int length) + throws IOException { + ByteBuffer bytes = ByteBuffer.allocate(length); + channel.position(offset); + while (bytes.hasRemaining()) { + if (channel.read(bytes) < 0) { + throw new IOException("Unexpected end of State Archive file"); + } + } + return bytes.array(); + } + + private static byte[] digestFilePrefix(byte[] domain, FileChannel channel, long length) + throws IOException { + if (length < 0 || length > channel.size()) { + throw new IllegalArgumentException("Invalid State Archive digest prefix length"); + } + MessageDigest digest = sha256(); + digest.update(domain); + ByteBuffer bytes = ByteBuffer.allocateDirect(APPEND_BUFFER_BYTES); + channel.position(0); + long remaining = length; + while (remaining > 0) { + bytes.clear(); + bytes.limit((int) Math.min(bytes.capacity(), remaining)); + int read = channel.read(bytes); + if (read < 0) { + throw new IOException("Unexpected end of State Archive digest prefix"); + } + bytes.flip(); + digest.update(bytes); + remaining -= read; + } + return digest.digest(); + } + + private static void writeFully(FileChannel channel, ByteBuffer bytes) throws IOException { + while (bytes.hasRemaining()) { + channel.write(bytes); + } + } + + private static void writeBuffered(FileChannel channel, byte[] bytes, ByteBuffer staging) + throws IOException { + int offset = 0; + while (offset < bytes.length) { + staging.clear(); + int length = Math.min(staging.remaining(), bytes.length - offset); + staging.put(bytes, offset, length); + staging.flip(); + writeFully(channel, staging); + offset += length; + } + } + + private static void syncDirectory(Path directory) throws IOException { + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + private static byte[] requireHash(byte[] value, String name) { + if (value == null || value.length != StateArchiveFileFormatV3.HASH_LENGTH) { + throw new IllegalArgumentException("Invalid State Archive " + name + " length"); + } + return Arrays.copyOf(value, value.length); + } + + private static void requireCompression(short value) { + if (value != StateArchiveFileFormatV3.COMPRESSION_NONE + && value != StateArchiveFileFormatV3.COMPRESSION_RAW_DEFLATE_LEVEL_1) { + throw new IllegalArgumentException("Unknown State Archive compression ID"); + } + } + + private void requireUsable() { + if (failed) { + throw new IllegalStateException("State Archive writer requires reopen after append failure"); + } + } + + @Override + public synchronized void close() throws IOException { + IOException failure = null; + for (LaneState state : lanes.values()) { + try { + state.close(); + } catch (IOException closeFailure) { + if (failure == null) { + failure = closeFailure; + } else { + failure.addSuppressed(closeFailure); + } + } + } + lanes.clear(); + if (failure != null) { + throw failure; + } + } + + private static final class ParsedName { + private final int laneId; + private final long segmentSeq; + + private ParsedName(int laneId, long segmentSeq) { + StateArchiveFileFormatV3.laneKind(laneId); + this.laneId = laneId; + this.segmentSeq = segmentSeq; + } + } + + private static final class RecoveryRequest { + private final RecoveryPoint authorizedCeiling; + private final RecoveryPoint commonCommitted; + private final long prototypeBoundary; + private final boolean prototype; + + private RecoveryRequest(RecoveryPoint authorizedCeiling, + RecoveryPoint commonCommitted) { + this.authorizedCeiling = Objects.requireNonNull(authorizedCeiling, + "authorizedCeiling"); + this.commonCommitted = commonCommitted; + this.prototypeBoundary = authorizedCeiling.getBlockNumber(); + this.prototype = false; + } + + private RecoveryRequest(long prototypeBoundary) { + this.authorizedCeiling = null; + this.commonCommitted = null; + this.prototypeBoundary = prototypeBoundary; + this.prototype = true; + } + + private static RecoveryRequest prototype(long boundary) { + return new RecoveryRequest(boundary); + } + + private long authorizedBlock() { + return prototypeBoundary; + } + } + + enum RecoveryStage { + TEMPORARY_FORCED, + INTENT_PUBLISHED, + LANE_DATA_APPLIED, + LANE_INDEX_APPLIED, + TARGET_VERIFIED, + INTENT_DELETED + } + + @FunctionalInterface + interface RecoveryFaultHook { + RecoveryFaultHook NONE = (stage, laneId) -> { }; + + void after(RecoveryStage stage, int laneId) throws IOException; + } + + enum SyncStage { + MARKER_WRITTEN, + DATA_FORCED, + MARKER_VERIFIED, + PROOF_READY + } + + @FunctionalInterface + interface SyncFaultHook { + SyncFaultHook NONE = (stage, laneId) -> { }; + + void after(SyncStage stage, int laneId) throws IOException; + } + + private static final class MarkerWrite { + private final LaneState state; + private final long offset; + private final byte[] encoded; + private final DurableMarker marker; + + private MarkerWrite(LaneState state, long offset, byte[] encoded, + DurableMarker marker) { + this.state = state; + this.offset = offset; + this.encoded = encoded; + this.marker = marker; + } + } + + private static final class ScannedMarker { + private final long offset; + private final DurableMarker marker; + + private ScannedMarker(long offset, DurableMarker marker) { + this.offset = offset; + this.marker = marker; + } + } + + public static final class FileTailProof { + private final int laneId; + private final long segmentSeq; + private final long markerOffset; + private final int markerLength; + private final long markerEndOffset; + private final byte[] markerDigest; + + FileTailProof(int laneId, long segmentSeq, long markerOffset, + int markerLength, long markerEndOffset, byte[] markerDigest) { + StateArchiveFileFormatV3.laneKind(laneId); + if (segmentSeq < 0 || markerOffset < StateArchiveFileFormatV3.PART_HEADER_LENGTH + || markerLength != StateArchiveFileFormatV3.MARKER_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH + || markerEndOffset != markerOffset + markerLength) { + throw new IllegalArgumentException("Invalid State Archive file tail proof"); + } + this.laneId = laneId; + this.segmentSeq = segmentSeq; + this.markerOffset = markerOffset; + this.markerLength = markerLength; + this.markerEndOffset = markerEndOffset; + this.markerDigest = requireHash(markerDigest, "marker digest"); + } + + public int getLaneId() { + return laneId; + } + + public long getSegmentSeq() { + return segmentSeq; + } + + public long getMarkerOffset() { + return markerOffset; + } + + public int getMarkerLength() { + return markerLength; + } + + public long getMarkerEndOffset() { + return markerEndOffset; + } + + public byte[] getMarkerDigest() { + return Arrays.copyOf(markerDigest, markerDigest.length); + } + } + + public static final class ArchiveDurabilityProof { + private final long checkpointSequence; + private final RecoveryPoint target; + private final byte[] commonTargetDigest; + private final List fileTails; + private final byte[] formatIdentity; + private final byte[] descriptorDigest; + + ArchiveDurabilityProof(long checkpointSequence, RecoveryPoint target, + byte[] commonTargetDigest, List fileTails) { + if (checkpointSequence < 0) { + throw new IllegalArgumentException("Invalid State Archive proof sequence"); + } + this.checkpointSequence = checkpointSequence; + this.target = Objects.requireNonNull(target, "target"); + this.commonTargetDigest = requireHash(commonTargetDigest, "proof Common target digest"); + this.fileTails = Collections.unmodifiableList(new ArrayList<>(fileTails)); + this.formatIdentity = StateArchiveFileFormatV3.compositeFormatDigest(); + this.descriptorDigest = StateArchiveFileFormatV3.fiveLaneDescriptorDigest(); + int[] laneIds = StateArchiveFileFormatV3.fiveLaneIds(); + if (this.fileTails.size() < laneIds.length) { + throw new IllegalArgumentException("Incomplete State Archive durability proof"); + } + int laneIndex = 0; + long previousSegment = -1; + for (FileTailProof tail : this.fileTails) { + if (tail.getLaneId() != laneIds[laneIndex]) { + if (laneIndex + 1 >= laneIds.length + || tail.getLaneId() != laneIds[++laneIndex]) { + throw new IllegalArgumentException("Non-canonical State Archive durability proof"); + } + previousSegment = -1; + } + if (tail.getSegmentSeq() <= previousSegment) { + throw new IllegalArgumentException("Non-canonical State Archive durability proof"); + } + previousSegment = tail.getSegmentSeq(); + } + if (laneIndex != laneIds.length - 1) { + throw new IllegalArgumentException("Incomplete State Archive durability proof"); + } + } + + public long getCheckpointSequence() { + return checkpointSequence; + } + + public RecoveryPoint getTarget() { + return target; + } + + public byte[] getCommonTargetDigest() { + return Arrays.copyOf(commonTargetDigest, commonTargetDigest.length); + } + + public List getFileTails() { + return fileTails; + } + + public byte[] getFormatIdentity() { + return Arrays.copyOf(formatIdentity, formatIdentity.length); + } + + public byte[] getDescriptorDigest() { + return Arrays.copyOf(descriptorDigest, descriptorDigest.length); + } + } + + private static final class LaneState { + private final int laneId; + private final long segmentSeq; + private final long firstBlock; + private final byte[] headerDigest; + private final byte[] startHistoryDigest; + private final FileChannel data; + private final FileChannel index; + private final MessageDigest contentDigest; + private final ByteBuffer appendBuffer = ByteBuffer.allocateDirect(APPEND_BUFFER_BYTES); + private long lastBlock; + private long blockFrameCount; + private long entryCount; + private long logicalPayloadBytes; + private long encodedBlockFrameBytes; + private long dataEndOffset = StateArchiveFileFormatV3.PART_HEADER_LENGTH; + private byte[] firstFrameDigest; + private byte[] lastFrameDigest; + private byte[] endHistoryDigest; + private BlockSnapshotMeta lastMeta; + private long markedBlockFrameCount; + private long markedLogicalBytes; + private long markedEncodedBytes; + private long lastMarkerEndOffset = StateArchiveFileFormatV3.PART_HEADER_LENGTH; + private long lastMarkerOffset = -1; + private byte[] previousMarkerDigest = new byte[StateArchiveFileFormatV3.HASH_LENGTH]; + private byte[] lastCommonTargetDigest; + private long lastCheckpointSequence = -1; + + private LaneState(int laneId, long segmentSeq, long firstBlock, byte[] headerDigest, + byte[] startHistoryDigest, FileChannel data, FileChannel index, + MessageDigest contentDigest) { + this.laneId = laneId; + this.segmentSeq = segmentSeq; + this.firstBlock = firstBlock; + this.lastBlock = firstBlock - 1; + this.headerDigest = headerDigest; + this.startHistoryDigest = startHistoryDigest; + this.data = data; + this.index = index; + this.contentDigest = contentDigest; + } + + private void record(BlockSnapshotMeta meta, byte[] frame, byte[] digest) { + long blockNumber = meta.getBlockNumber(); + if (blockFrameCount > 0 && blockNumber != lastBlock + 1) { + throw new IllegalArgumentException("Non-contiguous State Archive lane block"); + } + if (firstFrameDigest == null) { + firstFrameDigest = Arrays.copyOf(digest, digest.length); + } + lastFrameDigest = Arrays.copyOf(digest, digest.length); + lastBlock = blockNumber; + blockFrameCount++; + entryCount += ByteBuffer.wrap(frame).getLong(ENTRY_COUNT_OFFSET); + logicalPayloadBytes += ByteBuffer.wrap(frame).getLong(RAW_PAYLOAD_LENGTH_OFFSET); + encodedBlockFrameBytes += frame.length; + dataEndOffset += frame.length; + endHistoryDigest = Arrays.copyOfRange(frame, RESULT_HISTORY_DIGEST_OFFSET, + RESULT_HISTORY_DIGEST_OFFSET + StateArchiveFileFormatV3.HASH_LENGTH); + lastMeta = meta; + } + + private CurrentSegment currentMap() { + return new CurrentSegment(laneId, segmentSeq, firstBlock, lastBlock, + dataEndOffset, blockFrameCount, headerDigest); + } + + private void close() throws IOException { + data.close(); + index.close(); + } + } + + private static final class ScannedSegment { + private final Path dataPath; + private final FileChannel data; + private final FileChannel index; + private final Path indexPath; + private final SegmentHeader header; + private final MessageDigest contentDigest; + private final List expectedIndex = new ArrayList<>(); + private long firstBlock = -1; + private long lastBlock = -1; + private long count; + private long entryCount; + private long logicalBytes; + private long encodedBytes; + private long dataEnd = StateArchiveFileFormatV3.PART_HEADER_LENGTH; + private byte[] firstDigest; + private byte[] lastDigest; + private byte[] startHistory; + private byte[] endHistory; + private byte[] content; + private SegmentSeal seal; + private long markedCount; + private long markedLogicalBytes; + private long markedEncodedBytes; + private long lastMarkerEndOffset = StateArchiveFileFormatV3.PART_HEADER_LENGTH; + private long lastCheckpointSequence = -1; + private byte[] previousMarkerDigest = new byte[StateArchiveFileFormatV3.HASH_LENGTH]; + private ScannedMarker lastMarker; + private boolean tailDamaged; + + private ScannedSegment(Path dataPath, FileChannel data, FileChannel index, + Path indexPath, SegmentHeader header, byte[] headerBytes) { + this.dataPath = dataPath; + this.data = data; + this.index = index; + this.indexPath = indexPath; + this.header = header; + this.contentDigest = newContentDigest(headerBytes); + } + + private void addBlock(long offset, byte[] frame) { + if (ByteBuffer.wrap(frame).getShort(COMPRESSION_ID_OFFSET) + != header.getCompressionId()) { + throw new IllegalArgumentException("State Archive segment frame compression mismatch"); + } + long number = blockNumber(frame); + byte[] digest = encodedFrameDigest(frame); + if (count == 0) { + firstBlock = number; + startHistory = previousHistoryDigest(frame); + firstDigest = digest; + } else if (number != lastBlock + 1) { + throw new IllegalArgumentException("Non-contiguous State Archive segment block"); + } + lastBlock = number; + lastDigest = digest; + endHistory = Arrays.copyOfRange(frame, RESULT_HISTORY_DIGEST_OFFSET, + RESULT_HISTORY_DIGEST_OFFSET + StateArchiveFileFormatV3.HASH_LENGTH); + count++; + entryCount += ByteBuffer.wrap(frame).getLong(ENTRY_COUNT_OFFSET); + logicalBytes += ByteBuffer.wrap(frame).getLong(RAW_PAYLOAD_LENGTH_OFFSET); + encodedBytes += frame.length; + dataEnd = offset + frame.length; + contentDigest.update(frame); + expectedIndex.add(new BlockIndexEntry(number, offset, frame.length, + ByteBuffer.wrap(digest).getLong())); + } + + private void addMarker(long offset, DurableMarker marker, byte[] frame) { + long markerBlocks = count - markedCount; + long firstMarkedBlock = markerBlocks == 0 ? -1 : lastBlock - markerBlocks + 1; + if (marker.getLaneId() != header.getLaneId() + || marker.getSegmentSeq() != header.getSegmentSeq() + || marker.getCheckpointSequence() <= lastCheckpointSequence + || markerBlocks <= 0 || marker.getBlockCount() != markerBlocks + || marker.getFirstBlock() != firstMarkedBlock + || marker.getLastBlock() != lastBlock + || marker.getCoveredStartOffset() != lastMarkerEndOffset + || marker.getMarkerEndOffset() != offset + frame.length + || marker.getLogicalBytes() != logicalBytes - markedLogicalBytes + || marker.getEncodedBytes() != encodedBytes - markedEncodedBytes + || !Arrays.equals(marker.getResultHistoryDigest(), endHistory) + || !Arrays.equals(marker.getPreviousMarkerDigest(), previousMarkerDigest)) { + throw new IllegalArgumentException("State Archive durable marker range mismatch"); + } + contentDigest.update(frame); + dataEnd = offset + frame.length; + markedCount = count; + markedLogicalBytes = logicalBytes; + markedEncodedBytes = encodedBytes; + lastMarkerEndOffset = marker.getMarkerEndOffset(); + lastCheckpointSequence = marker.getCheckpointSequence(); + previousMarkerDigest = marker.getEncodedFrameDigest(); + lastMarker = new ScannedMarker(offset, marker); + } + + private void setSeal(SegmentSeal value) { + if (seal != null) { + throw new IllegalArgumentException("Duplicate State Archive segment seal"); + } + seal = value; + } + + private void finish(boolean recovering) throws IOException { + if (count == 0) { + if (recovering && seal == null) { + return; + } + throw new IllegalArgumentException("Empty State Archive segment"); + } + if (header.getActualFirstBlock() != firstBlock + || !Arrays.equals(header.getPreviousHistoryDigest(), startHistory)) { + throw new IllegalArgumentException("State Archive segment first identity mismatch"); + } + if (seal != null || !recovering) { + validateIndex(); + } + if (seal != null) { + content = contentDigest.digest(); + if (seal.getLaneId() != header.getLaneId() + || seal.getSegmentSeq() != header.getSegmentSeq() + || seal.getActualFirstBlock() != firstBlock + || seal.getActualLastBlock() != lastBlock + || seal.getBlockFrameCount() != count || seal.getEntryCount() != entryCount + || seal.getLogicalPayloadBytes() != logicalBytes + || seal.getEncodedBlockFrameBytes() != encodedBytes + || seal.getDataEndOffset() != dataEnd + || seal.getPhysicalFileBytes() != data.size() + || !Arrays.equals(seal.getFirstBlockFrameDigest(), firstDigest) + || !Arrays.equals(seal.getLastBlockFrameDigest(), lastDigest) + || !Arrays.equals(seal.getStartHistoryDigest(), startHistory) + || !Arrays.equals(seal.getEndHistoryDigest(), endHistory) + || !Arrays.equals(seal.getSegmentContentDigest(), content)) { + throw new IllegalArgumentException("State Archive segment seal mismatch"); + } + data.close(); + index.close(); + } + } + + private void validateIndex() throws IOException { + long expectedLength = StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + + count * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH; + if (index == null || index.size() != expectedLength) { + throw new IllegalArgumentException("State Archive block index length mismatch"); + } + for (int position = 0; position < expectedIndex.size(); position++) { + BlockIndexEntry actual = StateArchiveSegmentFormatV3.decodeBlockIndexEntry( + readExact(index, StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + + (long) position * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH, + StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH)); + BlockIndexEntry expected = expectedIndex.get(position); + if (actual.getBlockNumber() != expected.getBlockNumber() + || actual.getFrameOffset() != expected.getFrameOffset() + || actual.getFrameLength() != expected.getFrameLength() + || actual.getEncodedFrameDigestPrefix() + != expected.getEncodedFrameDigestPrefix()) { + throw new IllegalArgumentException("State Archive block index entry mismatch"); + } + } + } + + private LaneTarget laneTarget(long commonHead) throws IOException { + int keepCount = retainedCount(commonHead); + long originalDataEnd = data.size(); + long originalIndexEnd = index == null ? 0 : index.size(); + if (keepCount == 0) { + return new LaneTarget(header.getLaneId(), + StateArchiveFiveLaneRecoveryIntentV3.DELETE_PAIR + | (index == null + ? StateArchiveFiveLaneRecoveryIntentV3.ORIGINAL_INDEX_MISSING : 0), + header.getSegmentSeq(), originalDataEnd, originalIndexEnd, + StateArchiveFiveLaneRecoveryIntentV3.NO_TARGET_SEGMENT, 0, 0, + header.getHeaderDigest(), new byte[32], new byte[32]); + } + long targetDataEnd = expectedIndex.get(keepCount - 1).getFrameOffset() + + expectedIndex.get(keepCount - 1).getFrameLength(); + byte[] targetIndex = targetIndexBytes(keepCount); + int flags = targetDataEnd < originalDataEnd + ? StateArchiveFiveLaneRecoveryIntentV3.DATA_TRUNCATE : 0; + if (index == null) { + flags |= StateArchiveFiveLaneRecoveryIntentV3.INDEX_REPLACE + | StateArchiveFiveLaneRecoveryIntentV3.ORIGINAL_INDEX_MISSING; + } else if (index.size() != targetIndex.length + || !Arrays.equals(digestFilePrefix( + StateArchiveFileFormatV3.RECOVERY_INDEX_FILE_DOMAIN, index, index.size()), + StateArchiveFiveLaneRecoveryIntentV3.indexFileDigest(targetIndex))) { + flags |= StateArchiveFiveLaneRecoveryIntentV3.INDEX_REPLACE; + } + return new LaneTarget(header.getLaneId(), flags, header.getSegmentSeq(), + originalDataEnd, originalIndexEnd, header.getSegmentSeq(), targetDataEnd, + targetIndex.length, header.getHeaderDigest(), + digestFilePrefix(StateArchiveFileFormatV3.RECOVERY_DATA_PREFIX_DOMAIN, + data, targetDataEnd), + StateArchiveFiveLaneRecoveryIntentV3.indexFileDigest(targetIndex)); + } + + private int retainedCount(long commonHead) { + int keepCount = 0; + while (keepCount < expectedIndex.size() + && expectedIndex.get(keepCount).getBlockNumber() <= commonHead) { + keepCount++; + } + return keepCount; + } + + private byte[] targetIndexBytes(int keepCount) { + ByteBuffer bytes = ByteBuffer.allocate(StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + + keepCount * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH); + bytes.put(StateArchiveSegmentFormatV3.encodeBlockIndexHeader( + new BlockIndexHeader(header.getLaneId(), header.getSegmentSeq(), + header.getHeaderDigest()))); + for (int position = 0; position < keepCount; position++) { + bytes.put(StateArchiveSegmentFormatV3.encodeBlockIndexEntry( + expectedIndex.get(position))); + } + return bytes.array(); + } + + private void repairTo(long commonHead, RecoveryFaultHook faultHook) throws IOException { + int keepCount = retainedCount(commonHead); + if (keepCount == 0) { + close(); + Files.deleteIfExists(indexPath); + faultHook.after(RecoveryStage.LANE_INDEX_APPLIED, header.getLaneId()); + Files.deleteIfExists(dataPath); + syncDirectory(dataPath.getParent()); + faultHook.after(RecoveryStage.LANE_DATA_APPLIED, header.getLaneId()); + return; + } + long keepDataEnd = keepCount == 0 ? StateArchiveFileFormatV3.PART_HEADER_LENGTH + : expectedIndex.get(keepCount - 1).getFrameOffset() + + expectedIndex.get(keepCount - 1).getFrameLength(); + if (data.size() != keepDataEnd || tailDamaged) { + data.truncate(keepDataEnd); + data.force(false); + faultHook.after(RecoveryStage.LANE_DATA_APPLIED, header.getLaneId()); + } + boolean rewrite = index == null + || index.size() != StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + + (long) keepCount * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH; + if (!rewrite && index.size() >= StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH) { + try { + StateArchiveSegmentFormatV3.decodeBlockIndexHeader(readExact(index, 0, + StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH)); + for (int position = 0; position < keepCount; position++) { + BlockIndexEntry actual = StateArchiveSegmentFormatV3.decodeBlockIndexEntry( + readExact(index, StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + + (long) position * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH, + StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH)); + BlockIndexEntry expected = expectedIndex.get(position); + if (!sameEntry(actual, expected)) { + rewrite = true; + break; + } + } + } catch (IllegalArgumentException invalidIndex) { + rewrite = true; + } + } + if (rewrite) { + byte[] targetBytes = targetIndexBytes(keepCount); + Path temporary = indexPath.resolveSibling(indexPath.getFileName() + ".recovery.tmp"); + try (FileChannel temporaryIndex = FileChannel.open(temporary, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE)) { + writeFully(temporaryIndex, ByteBuffer.wrap(targetBytes)); + temporaryIndex.force(true); + } + if (index != null && index.isOpen()) { + index.close(); + } + try { + Files.move(temporary, indexPath, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("State Archive index repair requires atomic move", unsupported); + } + syncDirectory(indexPath.getParent()); + faultHook.after(RecoveryStage.LANE_INDEX_APPLIED, header.getLaneId()); + } + } + + private static boolean sameEntry(BlockIndexEntry left, BlockIndexEntry right) { + return left.getBlockNumber() == right.getBlockNumber() + && left.getFrameOffset() == right.getFrameOffset() + && left.getFrameLength() == right.getFrameLength() + && left.getEncodedFrameDigestPrefix() == right.getEncodedFrameDigestPrefix(); + } + + private void close() throws IOException { + if (data.isOpen()) { + data.close(); + } + if (index != null && index.isOpen()) { + index.close(); + } + } + + private LaneState openState() { + LaneState state = new LaneState(header.getLaneId(), header.getSegmentSeq(), firstBlock, + header.getHeaderDigest(), startHistory, data, index, contentDigest); + state.lastBlock = lastBlock; + state.blockFrameCount = count; + state.entryCount = entryCount; + state.logicalPayloadBytes = logicalBytes; + state.encodedBlockFrameBytes = encodedBytes; + state.dataEndOffset = dataEnd; + state.firstFrameDigest = firstDigest; + state.lastFrameDigest = lastDigest; + state.endHistoryDigest = endHistory; + state.markedBlockFrameCount = markedCount; + state.markedLogicalBytes = markedLogicalBytes; + state.markedEncodedBytes = markedEncodedBytes; + state.lastMarkerEndOffset = lastMarkerEndOffset; + state.lastMarkerOffset = lastMarker == null ? -1 : lastMarker.offset; + state.previousMarkerDigest = previousMarkerDigest; + state.lastCheckpointSequence = lastCheckpointSequence; + state.lastCommonTargetDigest = lastMarker == null ? null + : lastMarker.marker.getCommonTargetDigest(); + return state; + } + + private byte[] chainDigest() { + return StateArchiveSegmentFormatV3.segmentChainDigest(header.getLaneId(), + header.getSegmentSeq(), header.getHeaderDigest(), content, + seal.getEncodedFrameDigest()); + } + + private SealedSegment sealedMap() { + return new SealedSegment(header.getLaneId(), header.getSegmentSeq(), firstBlock, + lastBlock, count, seal.getPhysicalFileBytes(), + StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + + count * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH, + header.getHeaderDigest(), content, + seal.getEncodedFrameDigest(), new byte[32]); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializer.java index ae237b3bc24..da5d6010ee4 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotCheckpointMaterializer.java @@ -10,7 +10,7 @@ /** Default-off State Archive participant backed only by the independent Hot DB. */ public final class StateArchiveHotCheckpointMaterializer - implements CommonCheckpointMaterializer { + implements CommonCheckpointMaterializer, StateArchiveCheckpointPlanner { private final StateArchiveHotStore hotStore; @@ -19,12 +19,14 @@ public StateArchiveHotCheckpointMaterializer(StateArchiveHotStore hotStore) { } /** Computes the exact Hot batch identity without writing bodies or checkpoint metadata. */ + @Override public synchronized StateArchiveHotBatchDescriptor planCheckpoint( List diffs) throws IOException { return hotStore.planCheckpoint(Objects.requireNonNull(diffs, "diffs")); } /** Prepares one capture whose payload, descriptor and transient bodies share one identity. */ + @Override public synchronized CommonCheckpointTarget prepare(CommonCheckpointCapture capture) throws IOException { CommonCheckpointCapture admitted = Objects.requireNonNull(capture, "capture"); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java index de6f059c569..20105e54df6 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHotStore.java @@ -250,17 +250,35 @@ public synchronized void prepareCheckpoint(byte[] targetDigest, public synchronized StateArchiveHotBatchDescriptor planCheckpoint(List diffs) throws IOException { ensureOpen(); - List admitted = new ArrayList<>(Objects.requireNonNull(diffs, "diffs")); - if (admitted.isEmpty()) { - throw new IllegalArgumentException("Hot Archive checkpoint must contain blocks"); - } if (current.prepared != null || current.publishedBlock != current.endBlock) { throw new ArchivePersistenceException( "Hot Archive cannot plan across an unpublished checkpoint"); } - long previousBlock = current.publishedBlock; - byte[] previousHash = current.publishedHash; - byte[] resultContentDigest = current.publishedContentDigest; + return planCheckpointDescriptor(engine, current.publishedBlock, current.publishedHash, + current.publishedContentDigest, diffs, historyCodec); + } + + /** Pure v2 descriptor planner shared with append-file Common compatibility binding. */ + static StateArchiveHotBatchDescriptor planCheckpointDescriptor(Engine engine, + long baseBlock, byte[] baseHash, byte[] baseContentDigest, + List diffs) { + return planCheckpointDescriptor(engine, baseBlock, baseHash, baseContentDigest, diffs, + new BlockHistoryCodec()); + } + + private static StateArchiveHotBatchDescriptor planCheckpointDescriptor(Engine engine, + long baseBlock, byte[] baseHash, byte[] baseContentDigest, + List diffs, BlockHistoryCodec codec) { + List admitted = new ArrayList<>(Objects.requireNonNull(diffs, "diffs")); + if (admitted.isEmpty()) { + throw new IllegalArgumentException("Hot Archive checkpoint must contain blocks"); + } + long previousBlock = baseBlock; + byte[] previousHash = Arrays.copyOf(Objects.requireNonNull(baseHash, "baseHash"), + baseHash.length); + byte[] resultContentDigest = Arrays.copyOf( + Objects.requireNonNull(baseContentDigest, "baseContentDigest"), + baseContentDigest.length); long encodedBytes = 0; Hasher orderedRecords = Hashing.sha256().newHasher(); List blocks = new ArrayList<>(); @@ -273,7 +291,7 @@ public synchronized StateArchiveHotBatchDescriptor planCheckpoint(List 0 && dataEndOffset >= segmentTargetBytes; + } + + public static byte[] encodeHeader(SegmentHeader header) { + Objects.requireNonNull(header, "header"); + header.validate(); + ByteBuffer bytes = ByteBuffer.allocate(StateArchiveFileFormatV3.PART_HEADER_LENGTH); + bytes.putInt(StateArchiveFileFormatV3.SEGMENT_MAGIC); + bytes.putShort(StateArchiveFileFormatV3.MAJOR_VERSION); + bytes.putShort(StateArchiveFileFormatV3.MINOR_VERSION); + bytes.putInt(StateArchiveFileFormatV3.PART_HEADER_LENGTH); + bytes.putShort(StateArchiveFileFormatV3.laneKind(header.laneId)); + bytes.putShort((short) header.laneId); + bytes.putShort(StateArchiveFileFormatV3.SEGMENT_LAYOUT_ID); + bytes.putShort((short) 0); + bytes.putLong(header.segmentSeq); + bytes.putLong(header.actualFirstBlock); + bytes.putLong(header.actualFirstBlock); + bytes.putLong(StateArchiveFileFormatV3.SEGMENT_TARGET_BYTES); + bytes.putLong(StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES); + bytes.put(StateArchiveFileFormatV3.fiveLaneDescriptorDigest()); + bytes.put(StateArchiveFileFormatV3.compositeFormatDigest()); + bytes.put(header.previousSegmentDigest); + bytes.put(header.previousHistoryDigest); + bytes.putShort(StateArchiveFileFormatV3.laneBodyCodec(header.laneId)); + bytes.putShort(header.compressionId); + bytes.putShort(StateArchiveFileFormatV3.KEY_ORDER_ID); + bytes.putShort(StateArchiveFileFormatV3.DIGEST_ID); + bytes.putShort(StateArchiveFileFormatV3.CHECKSUM_ID); + bytes.putShort(StateArchiveFileFormatV3.IDENTITY_KIND); + bytes.put(new byte[276]); + if (bytes.position() != HEADER_DIGEST_OFFSET) { + throw new IllegalStateException("Invalid State Archive segment header layout"); + } + bytes.put(StateArchiveFileFormatV3.sha256(StateArchiveFileFormatV3.SEGMENT_HEADER_DOMAIN, + Arrays.copyOf(bytes.array(), HEADER_DIGEST_OFFSET))); + bytes.putInt(crc32c(bytes.array(), 0, HEADER_CRC_OFFSET)); + if (bytes.hasRemaining()) { + throw new IllegalStateException("Invalid State Archive segment header length"); + } + return bytes.array(); + } + + public static SegmentHeader decodeHeader(byte[] encoded) { + requireLength(encoded, StateArchiveFileFormatV3.PART_HEADER_LENGTH, "segment header"); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + requireInt(bytes, StateArchiveFileFormatV3.SEGMENT_MAGIC, "segment magic"); + requireShort(bytes, StateArchiveFileFormatV3.MAJOR_VERSION, "major version"); + requireShort(bytes, StateArchiveFileFormatV3.MINOR_VERSION, "minor version"); + requireInt(bytes, StateArchiveFileFormatV3.PART_HEADER_LENGTH, "header length"); + short laneKind = bytes.getShort(); + int laneId = Short.toUnsignedInt(bytes.getShort()); + if (laneKind != StateArchiveFileFormatV3.laneKind(laneId)) { + throw new IllegalArgumentException("State Archive segment lane kind mismatch"); + } + requireShort(bytes, StateArchiveFileFormatV3.SEGMENT_LAYOUT_ID, "segment layout"); + requireShort(bytes, (short) 0, "segment flags"); + long segmentSeq = requireNonNegative(bytes.getLong(), "segment sequence"); + long firstBlock = requireNonNegative(bytes.getLong(), "first block"); + if (bytes.getLong() != firstBlock) { + throw new IllegalArgumentException("State Archive segment first epoch mismatch"); + } + if (bytes.getLong() != StateArchiveFileFormatV3.SEGMENT_TARGET_BYTES + || bytes.getLong() != StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES) { + throw new IllegalArgumentException("State Archive segment size policy mismatch"); + } + requireArray(getBytes(bytes, 32), StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + "placement descriptor"); + requireArray(getBytes(bytes, 32), StateArchiveFileFormatV3.compositeFormatDigest(), + "composite format"); + byte[] previousSegmentDigest = getBytes(bytes, 32); + byte[] previousHistoryDigest = getBytes(bytes, 32); + requireShort(bytes, StateArchiveFileFormatV3.laneBodyCodec(laneId), "body codec"); + short compressionId = bytes.getShort(); + requireCompression(compressionId); + requireShort(bytes, StateArchiveFileFormatV3.KEY_ORDER_ID, "key order"); + requireShort(bytes, StateArchiveFileFormatV3.DIGEST_ID, "digest algorithm"); + requireShort(bytes, StateArchiveFileFormatV3.CHECKSUM_ID, "checksum algorithm"); + requireShort(bytes, StateArchiveFileFormatV3.IDENTITY_KIND, "identity kind"); + requireZero(bytes, 276, "segment reserved bytes"); + byte[] headerDigest = getBytes(bytes, 32); + requireArray(headerDigest, StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.SEGMENT_HEADER_DOMAIN, + Arrays.copyOf(encoded, HEADER_DIGEST_OFFSET)), "segment header digest"); + int expectedCrc = bytes.getInt(); + if (expectedCrc != crc32c(encoded, 0, HEADER_CRC_OFFSET)) { + throw new IllegalArgumentException("State Archive segment header checksum mismatch"); + } + return new SegmentHeader(laneId, segmentSeq, firstBlock, previousSegmentDigest, + previousHistoryDigest, compressionId, headerDigest); + } + + /** Returns the deterministic predecessor anchor for segment sequence zero of one lane. */ + public static byte[] laneBaselineDigest(int laneId) { + StateArchiveFileFormatV3.laneKind(laneId); + return StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.LANE_SEGMENT_BASELINE_DOMAIN, + StateArchiveFileFormatV3.compositeFormatDigest(), + StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + ByteBuffer.allocate(Short.BYTES).putShort((short) laneId).array()); + } + + /** Computes the link stored as previousSegmentDigest in the next segment header. */ + public static byte[] segmentChainDigest(int laneId, long segmentSeq, + byte[] segmentHeaderDigest, byte[] segmentContentDigest, byte[] sealFrameDigest) { + StateArchiveFileFormatV3.laneKind(laneId); + requireNonNegative(segmentSeq, "segment sequence"); + byte[] identity = ByteBuffer.allocate(Short.BYTES + Long.BYTES) + .putShort((short) laneId).putLong(segmentSeq).array(); + return StateArchiveFileFormatV3.sha256(StateArchiveFileFormatV3.SEGMENT_CHAIN_DOMAIN, + identity, + requireHash(segmentHeaderDigest, "segment header digest"), + requireHash(segmentContentDigest, "segment content digest"), + requireHash(sealFrameDigest, "seal frame digest")); + } + + public static byte[] encodeSeal(SegmentSeal seal) { + Objects.requireNonNull(seal, "seal"); + seal.validate(); + int totalLength = StateArchiveFileFormatV3.SEAL_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH; + ByteBuffer bytes = ByteBuffer.allocate(totalLength); + bytes.putInt(StateArchiveFileFormatV3.FRAME_MAGIC); + bytes.putShort(StateArchiveFileFormatV3.MAJOR_VERSION); + bytes.putShort(StateArchiveFileFormatV3.MINOR_VERSION); + bytes.putShort(StateArchiveFileFormatV3.PART_SEAL_FRAME_TYPE); + bytes.putShort((short) 0); + bytes.putInt(StateArchiveFileFormatV3.SEAL_HEADER_LENGTH); + bytes.putLong(totalLength); + bytes.putLong(0); + bytes.putShort((short) seal.laneId); + bytes.putShort((short) 0); + bytes.putLong(seal.segmentSeq); + bytes.putLong(seal.actualFirstBlock); + bytes.putLong(seal.actualLastBlock); + bytes.putLong(seal.blockFrameCount); + bytes.putLong(seal.entryCount); + bytes.putLong(seal.logicalPayloadBytes); + bytes.putLong(seal.encodedBlockFrameBytes); + bytes.putLong(seal.dataEndOffset); + bytes.putLong(seal.physicalFileBytes); + bytes.put(StateArchiveFileFormatV3.fiveLaneDescriptorDigest()); + bytes.put(seal.firstBlockFrameDigest); + bytes.put(seal.lastBlockFrameDigest); + bytes.put(seal.startHistoryDigest); + bytes.put(seal.endHistoryDigest); + bytes.put(seal.segmentContentDigest); + bytes.put(new byte[20]); + if (bytes.position() != SEAL_DIGEST_OFFSET) { + throw new IllegalStateException("Invalid State Archive seal header layout"); + } + byte[] encodedFrameDigest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ENCODED_SEGMENT_SEAL_DOMAIN, + Arrays.copyOf(bytes.array(), SEAL_DIGEST_OFFSET)); + bytes.put(encodedFrameDigest); + bytes.putLong(totalLength); + bytes.putInt(crc32c(bytes.array(), 0, SEAL_CRC_OFFSET)); + bytes.putInt(StateArchiveFileFormatV3.FRAME_TRAILER_MAGIC); + if (bytes.hasRemaining()) { + throw new IllegalStateException("Invalid State Archive seal frame length"); + } + return bytes.array(); + } + + public static SegmentSeal decodeSeal(byte[] encoded) { + int totalLength = StateArchiveFileFormatV3.SEAL_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH; + requireLength(encoded, totalLength, "seal frame"); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + requireInt(bytes, StateArchiveFileFormatV3.FRAME_MAGIC, "seal magic"); + requireShort(bytes, StateArchiveFileFormatV3.MAJOR_VERSION, "seal major version"); + requireShort(bytes, StateArchiveFileFormatV3.MINOR_VERSION, "seal minor version"); + requireShort(bytes, StateArchiveFileFormatV3.PART_SEAL_FRAME_TYPE, "seal frame type"); + requireShort(bytes, (short) 0, "seal flags"); + requireInt(bytes, StateArchiveFileFormatV3.SEAL_HEADER_LENGTH, "seal header length"); + requireLong(bytes, totalLength, "seal total length"); + requireLong(bytes, 0, "seal payload length"); + int laneId = Short.toUnsignedInt(bytes.getShort()); + StateArchiveFileFormatV3.laneKind(laneId); + requireShort(bytes, (short) 0, "seal reserved field"); + long segmentSeq = requireNonNegative(bytes.getLong(), "segment sequence"); + long firstBlock = requireNonNegative(bytes.getLong(), "first block"); + long lastBlock = requireNonNegative(bytes.getLong(), "last block"); + long blockFrameCount = requireNonNegative(bytes.getLong(), "block frame count"); + long entryCount = requireNonNegative(bytes.getLong(), "entry count"); + long logicalPayloadBytes = requireNonNegative(bytes.getLong(), + "logical payload bytes"); + long encodedBlockFrameBytes = requireNonNegative(bytes.getLong(), + "encoded block frame bytes"); + long dataEndOffset = requireNonNegative(bytes.getLong(), "data end offset"); + long physicalFileBytes = requireNonNegative(bytes.getLong(), "physical file bytes"); + requireArray(getBytes(bytes, 32), StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + "seal placement descriptor"); + SegmentSeal seal = new SegmentSeal(laneId, + segmentSeq, firstBlock, lastBlock, blockFrameCount, entryCount, + logicalPayloadBytes, encodedBlockFrameBytes, dataEndOffset, physicalFileBytes, + getBytes(bytes, 32), getBytes(bytes, 32), getBytes(bytes, 32), + getBytes(bytes, 32), getBytes(bytes, 32), null); + requireZero(bytes, 20, "seal reserved bytes"); + byte[] encodedFrameDigest = getBytes(bytes, 32); + requireArray(encodedFrameDigest, StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ENCODED_SEGMENT_SEAL_DOMAIN, + Arrays.copyOf(encoded, SEAL_DIGEST_OFFSET)), "seal encoded frame digest"); + requireLong(bytes, totalLength, "repeated seal total length"); + if (bytes.getInt() != crc32c(encoded, 0, SEAL_CRC_OFFSET)) { + throw new IllegalArgumentException("State Archive seal checksum mismatch"); + } + requireInt(bytes, StateArchiveFileFormatV3.FRAME_TRAILER_MAGIC, + "seal trailer magic"); + return seal.withEncodedFrameDigest(encodedFrameDigest); + } + + public static byte[] encodeDurableMarker(DurableMarker marker) { + Objects.requireNonNull(marker, "marker"); + marker.validate(); + int totalLength = StateArchiveFileFormatV3.MARKER_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH; + ByteBuffer bytes = ByteBuffer.allocate(totalLength); + bytes.putInt(StateArchiveFileFormatV3.FRAME_MAGIC); + bytes.putShort(StateArchiveFileFormatV3.MAJOR_VERSION); + bytes.putShort(StateArchiveFileFormatV3.MINOR_VERSION); + bytes.putShort(StateArchiveFileFormatV3.DURABLE_MARKER_FRAME_TYPE); + bytes.putShort((short) 0); + bytes.putInt(StateArchiveFileFormatV3.MARKER_HEADER_LENGTH); + bytes.putLong(totalLength); + bytes.putLong(0); + bytes.putLong(marker.checkpointSequence); + bytes.putLong(marker.firstEpoch); + bytes.putLong(marker.lastEpoch); + bytes.putLong(marker.firstBlock); + bytes.putLong(marker.lastBlock); + bytes.put(marker.lastBlockHash); + bytes.put(StateArchiveFileFormatV3.fiveLaneDescriptorDigest()); + bytes.put(marker.resultHistoryDigest); + bytes.putLong(marker.coveredStartOffset); + bytes.putLong(marker.markerEndOffset); + bytes.putLong(marker.blockCount); + bytes.putLong(marker.logicalBytes); + bytes.putLong(marker.encodedBytes); + bytes.put(marker.previousMarkerDigest); + bytes.put(marker.commonTargetDigest); + bytes.putShort((short) marker.laneId); + bytes.putShort((short) 0); + bytes.putLong(marker.segmentSeq); + bytes.putInt(0); + if (bytes.position() != MARKER_DIGEST_OFFSET) { + throw new IllegalStateException("Invalid State Archive durable marker layout"); + } + byte[] digest = StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ENCODED_MARKER_DOMAIN, + Arrays.copyOf(bytes.array(), MARKER_DIGEST_OFFSET)); + bytes.put(digest); + bytes.putLong(totalLength); + bytes.putInt(crc32c(bytes.array(), 0, MARKER_CRC_OFFSET)); + bytes.putInt(StateArchiveFileFormatV3.FRAME_TRAILER_MAGIC); + if (bytes.hasRemaining()) { + throw new IllegalStateException("Invalid State Archive durable marker length"); + } + return bytes.array(); + } + + public static DurableMarker decodeDurableMarker(byte[] encoded) { + int totalLength = StateArchiveFileFormatV3.MARKER_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH; + requireLength(encoded, totalLength, "durable marker"); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + requireInt(bytes, StateArchiveFileFormatV3.FRAME_MAGIC, "marker magic"); + requireShort(bytes, StateArchiveFileFormatV3.MAJOR_VERSION, "marker major version"); + requireShort(bytes, StateArchiveFileFormatV3.MINOR_VERSION, "marker minor version"); + requireShort(bytes, StateArchiveFileFormatV3.DURABLE_MARKER_FRAME_TYPE, + "marker frame type"); + requireShort(bytes, (short) 0, "marker flags"); + requireInt(bytes, StateArchiveFileFormatV3.MARKER_HEADER_LENGTH, + "marker header length"); + requireLong(bytes, totalLength, "marker total length"); + requireLong(bytes, 0, "marker payload length"); + long checkpointSequence = requireNonNegative(bytes.getLong(), "checkpoint sequence"); + long firstEpoch = requireNonNegative(bytes.getLong(), "first epoch"); + long lastEpoch = requireNonNegative(bytes.getLong(), "last epoch"); + long firstBlock = requireNonNegative(bytes.getLong(), "first block"); + long lastBlock = requireNonNegative(bytes.getLong(), "last block"); + byte[] lastBlockHash = getBytes(bytes, 32); + requireArray(getBytes(bytes, 32), StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + "marker placement descriptor"); + byte[] resultHistoryDigest = getBytes(bytes, 32); + long coveredStartOffset = requireNonNegative(bytes.getLong(), "covered start offset"); + long markerEndOffset = requireNonNegative(bytes.getLong(), "marker end offset"); + long blockCount = requireNonNegative(bytes.getLong(), "marker block count"); + long logicalBytes = requireNonNegative(bytes.getLong(), "marker logical bytes"); + long encodedBytes = requireNonNegative(bytes.getLong(), "marker encoded bytes"); + byte[] previousMarkerDigest = getBytes(bytes, 32); + byte[] commonTargetDigest = getBytes(bytes, 32); + int laneId = Short.toUnsignedInt(bytes.getShort()); + requireShort(bytes, (short) 0, "marker lane reserved field"); + long segmentSeq = requireNonNegative(bytes.getLong(), "marker segment sequence"); + requireInt(bytes, 0, "marker reserved tail"); + DurableMarker marker = new DurableMarker(checkpointSequence, firstEpoch, lastEpoch, + firstBlock, lastBlock, lastBlockHash, resultHistoryDigest, coveredStartOffset, + markerEndOffset, blockCount, logicalBytes, encodedBytes, previousMarkerDigest, + commonTargetDigest, laneId, segmentSeq); + byte[] digest = getBytes(bytes, 32); + requireArray(digest, StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.ENCODED_MARKER_DOMAIN, + Arrays.copyOf(encoded, MARKER_DIGEST_OFFSET)), "marker encoded frame digest"); + requireLong(bytes, totalLength, "repeated marker total length"); + if (bytes.getInt() != crc32c(encoded, 0, MARKER_CRC_OFFSET)) { + throw new IllegalArgumentException("State Archive durable marker checksum mismatch"); + } + requireInt(bytes, StateArchiveFileFormatV3.FRAME_TRAILER_MAGIC, + "marker trailer magic"); + return marker.withEncodedFrameDigest(digest); + } + + public static byte[] encodeBlockIndexHeader(BlockIndexHeader header) { + Objects.requireNonNull(header, "header"); + header.validate(); + ByteBuffer bytes = ByteBuffer.allocate(StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH); + bytes.putInt(StateArchiveFileFormatV3.BLOCK_INDEX_MAGIC); + bytes.putShort(StateArchiveFileFormatV3.MAJOR_VERSION); + bytes.putShort(StateArchiveFileFormatV3.MINOR_VERSION); + bytes.putShort((short) StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH); + bytes.putShort((short) StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH); + bytes.putShort((short) header.laneId); + bytes.putShort(StateArchiveFileFormatV3.SEGMENT_LAYOUT_ID); + bytes.putLong(header.segmentSeq); + bytes.put(header.dataSegmentHeaderDigest); + bytes.put(StateArchiveFileFormatV3.fiveLaneDescriptorDigest()); + bytes.putInt(0); + if (bytes.position() != BLOCK_INDEX_HEADER_DIGEST_OFFSET) { + throw new IllegalStateException("Invalid State Archive block index header layout"); + } + bytes.put(StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_DOMAIN, + Arrays.copyOf(bytes.array(), BLOCK_INDEX_HEADER_DIGEST_OFFSET))); + bytes.putInt(crc32c(bytes.array(), 0, BLOCK_INDEX_HEADER_CRC_OFFSET)); + if (bytes.hasRemaining()) { + throw new IllegalStateException("Invalid State Archive block index header length"); + } + return bytes.array(); + } + + public static BlockIndexHeader decodeBlockIndexHeader(byte[] encoded) { + requireLength(encoded, StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH, + "block index header"); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + requireInt(bytes, StateArchiveFileFormatV3.BLOCK_INDEX_MAGIC, "block index magic"); + requireShort(bytes, StateArchiveFileFormatV3.MAJOR_VERSION, "index major version"); + requireShort(bytes, StateArchiveFileFormatV3.MINOR_VERSION, "index minor version"); + requireShort(bytes, (short) StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH, + "index header length"); + requireShort(bytes, (short) StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH, + "index entry length"); + int laneId = Short.toUnsignedInt(bytes.getShort()); + StateArchiveFileFormatV3.laneKind(laneId); + requireShort(bytes, StateArchiveFileFormatV3.SEGMENT_LAYOUT_ID, + "index segment layout"); + long segmentSeq = requireNonNegative(bytes.getLong(), "index segment sequence"); + byte[] dataHeaderDigest = getBytes(bytes, 32); + requireArray(getBytes(bytes, 32), StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + "index placement descriptor"); + requireInt(bytes, 0, "index reserved field"); + byte[] headerDigest = getBytes(bytes, 32); + requireArray(headerDigest, StateArchiveFileFormatV3.sha256( + StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_DOMAIN, + Arrays.copyOf(encoded, BLOCK_INDEX_HEADER_DIGEST_OFFSET)), + "block index header digest"); + if (bytes.getInt() != crc32c(encoded, 0, BLOCK_INDEX_HEADER_CRC_OFFSET)) { + throw new IllegalArgumentException("State Archive block index checksum mismatch"); + } + return new BlockIndexHeader(laneId, segmentSeq, dataHeaderDigest, headerDigest); + } + + public static byte[] encodeBlockIndexEntry(BlockIndexEntry entry) { + Objects.requireNonNull(entry, "entry"); + entry.validate(); + return ByteBuffer.allocate(StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH) + .putLong(entry.blockNumber) + .putLong(entry.frameOffset) + .putInt(entry.frameLength) + .putInt(0) + .putLong(entry.encodedFrameDigestPrefix) + .array(); + } + + public static BlockIndexEntry decodeBlockIndexEntry(byte[] encoded) { + requireLength(encoded, StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH, + "block index entry"); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + BlockIndexEntry entry = new BlockIndexEntry( + requireNonNegative(bytes.getLong(), "index block number"), + requireNonNegative(bytes.getLong(), "index frame offset"), + bytes.getInt(), bytes.getInt(), bytes.getLong()); + entry.validate(); + return entry; + } + + public static byte[] encodeSealedMapRecord(SealedSegment segment) { + Objects.requireNonNull(segment, "segment"); + segment.validate(); + ByteBuffer bytes = ByteBuffer.allocate(StateArchiveFileFormatV3.SEGMENT_MAP_ENTRY_LENGTH); + bytes.putShort((short) segment.laneId); + bytes.putShort(StateArchiveFileFormatV3.laneKind(segment.laneId)); + bytes.putInt(0); + bytes.putLong(segment.segmentSeq); + bytes.putLong(segment.firstBlock); + bytes.putLong(segment.lastBlock); + bytes.putLong(segment.blockFrameCount); + bytes.putLong(segment.dataFileBytes); + bytes.putLong(segment.blockIndexBytes); + bytes.put(segment.segmentHeaderDigest); + bytes.put(segment.segmentContentDigest); + bytes.put(segment.sealFrameDigest); + bytes.put(segment.manifestDigest); + bytes.putLong(0); + if (bytes.hasRemaining()) { + throw new IllegalStateException("Invalid State Archive SegmentMap record length"); + } + return bytes.array(); + } + + public static SealedSegment decodeSealedMapRecord(byte[] encoded) { + requireLength(encoded, StateArchiveFileFormatV3.SEGMENT_MAP_ENTRY_LENGTH, + "SegmentMap record"); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + int laneId = Short.toUnsignedInt(bytes.getShort()); + if (bytes.getShort() != StateArchiveFileFormatV3.laneKind(laneId)) { + throw new IllegalArgumentException("State Archive SegmentMap lane kind mismatch"); + } + requireInt(bytes, 0, "SegmentMap flags"); + SealedSegment result = new SealedSegment(laneId, + requireNonNegative(bytes.getLong(), "segment sequence"), + requireNonNegative(bytes.getLong(), "first block"), + requireNonNegative(bytes.getLong(), "last block"), + requireNonNegative(bytes.getLong(), "block count"), + requireNonNegative(bytes.getLong(), "data file bytes"), + requireNonNegative(bytes.getLong(), "block index bytes"), + getBytes(bytes, 32), getBytes(bytes, 32), getBytes(bytes, 32), getBytes(bytes, 32)); + requireLong(bytes, 0, "SegmentMap reserved field"); + result.validate(); + return result; + } + + private static void requireCompression(short compressionId) { + if (compressionId != StateArchiveFileFormatV3.COMPRESSION_NONE + && compressionId != StateArchiveFileFormatV3.COMPRESSION_RAW_DEFLATE_LEVEL_1) { + throw new IllegalArgumentException("Unknown State Archive compression ID"); + } + } + + private static byte[] requireHash(byte[] value, String name) { + requireLength(value, StateArchiveFileFormatV3.HASH_LENGTH, name); + return Arrays.copyOf(value, value.length); + } + + private static void requireLength(byte[] value, int length, String name) { + if (value == null || value.length != length) { + throw new IllegalArgumentException("Invalid State Archive " + name + " length"); + } + } + + private static byte[] getBytes(ByteBuffer bytes, int length) { + byte[] result = new byte[length]; + bytes.get(result); + return result; + } + + private static void requireZero(ByteBuffer bytes, int length, String name) { + for (int index = 0; index < length; index++) { + if (bytes.get() != 0) { + throw new IllegalArgumentException("Non-zero State Archive " + name); + } + } + } + + private static long requireNonNegative(long value, String name) { + if (value < 0) { + throw new IllegalArgumentException("Negative State Archive " + name); + } + return value; + } + + private static void requireInt(ByteBuffer bytes, int expected, String name) { + if (bytes.getInt() != expected) { + throw new IllegalArgumentException("State Archive " + name + " mismatch"); + } + } + + private static void requireShort(ByteBuffer bytes, short expected, String name) { + if (bytes.getShort() != expected) { + throw new IllegalArgumentException("State Archive " + name + " mismatch"); + } + } + + private static void requireLong(ByteBuffer bytes, long expected, String name) { + if (bytes.getLong() != expected) { + throw new IllegalArgumentException("State Archive " + name + " mismatch"); + } + } + + private static void requireArray(byte[] actual, byte[] expected, String name) { + if (!Arrays.equals(actual, expected)) { + throw new IllegalArgumentException("State Archive " + name + " mismatch"); + } + } + + private static int crc32c(byte[] bytes, int offset, int length) { + return Hashing.crc32c().hashBytes(bytes, offset, length).asInt(); + } + + public static final class DurableMarker { + private final long checkpointSequence; + private final long firstEpoch; + private final long lastEpoch; + private final long firstBlock; + private final long lastBlock; + private final byte[] lastBlockHash; + private final byte[] resultHistoryDigest; + private final long coveredStartOffset; + private final long markerEndOffset; + private final long blockCount; + private final long logicalBytes; + private final long encodedBytes; + private final byte[] previousMarkerDigest; + private final byte[] commonTargetDigest; + private final int laneId; + private final long segmentSeq; + private final byte[] encodedFrameDigest; + + public DurableMarker(long checkpointSequence, long firstEpoch, long lastEpoch, + long firstBlock, long lastBlock, byte[] lastBlockHash, + byte[] resultHistoryDigest, long coveredStartOffset, long markerEndOffset, + long blockCount, long logicalBytes, long encodedBytes, + byte[] previousMarkerDigest, byte[] commonTargetDigest, int laneId, + long segmentSeq) { + this(checkpointSequence, firstEpoch, lastEpoch, firstBlock, lastBlock, + lastBlockHash, resultHistoryDigest, coveredStartOffset, markerEndOffset, + blockCount, logicalBytes, encodedBytes, previousMarkerDigest, + commonTargetDigest, laneId, segmentSeq, null); + } + + private DurableMarker(long checkpointSequence, long firstEpoch, long lastEpoch, + long firstBlock, long lastBlock, byte[] lastBlockHash, + byte[] resultHistoryDigest, long coveredStartOffset, long markerEndOffset, + long blockCount, long logicalBytes, long encodedBytes, + byte[] previousMarkerDigest, byte[] commonTargetDigest, int laneId, + long segmentSeq, byte[] encodedFrameDigest) { + this.checkpointSequence = checkpointSequence; + this.firstEpoch = firstEpoch; + this.lastEpoch = lastEpoch; + this.firstBlock = firstBlock; + this.lastBlock = lastBlock; + this.lastBlockHash = requireHash(lastBlockHash, "marker last block hash"); + this.resultHistoryDigest = requireHash(resultHistoryDigest, + "marker result history digest"); + this.coveredStartOffset = coveredStartOffset; + this.markerEndOffset = markerEndOffset; + this.blockCount = blockCount; + this.logicalBytes = logicalBytes; + this.encodedBytes = encodedBytes; + this.previousMarkerDigest = requireHash(previousMarkerDigest, + "previous marker digest"); + this.commonTargetDigest = requireHash(commonTargetDigest, + "marker Common target digest"); + this.laneId = laneId; + this.segmentSeq = segmentSeq; + this.encodedFrameDigest = encodedFrameDigest == null ? null + : requireHash(encodedFrameDigest, "marker encoded frame digest"); + validate(); + } + + private DurableMarker withEncodedFrameDigest(byte[] digest) { + return new DurableMarker(checkpointSequence, firstEpoch, lastEpoch, firstBlock, + lastBlock, lastBlockHash, resultHistoryDigest, coveredStartOffset, + markerEndOffset, blockCount, logicalBytes, encodedBytes, + previousMarkerDigest, commonTargetDigest, laneId, segmentSeq, digest); + } + + private void validate() { + StateArchiveFileFormatV3.laneKind(laneId); + int totalLength = StateArchiveFileFormatV3.MARKER_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH; + if (checkpointSequence < 0 || firstEpoch < 0 || lastEpoch < firstEpoch + || firstBlock < 0 || lastBlock < firstBlock + || firstEpoch != firstBlock || lastEpoch != lastBlock + || blockCount <= 0 || lastBlock - firstBlock != blockCount - 1 + || logicalBytes < 0 || encodedBytes <= 0 + || coveredStartOffset < StateArchiveFileFormatV3.PART_HEADER_LENGTH + || coveredStartOffset > Long.MAX_VALUE - encodedBytes - totalLength + || markerEndOffset != coveredStartOffset + encodedBytes + totalLength + || segmentSeq < 0) { + throw new IllegalArgumentException("Invalid State Archive durable marker state"); + } + } + + public long getCheckpointSequence() { + return checkpointSequence; + } + + public long getFirstBlock() { + return firstBlock; + } + + public long getLastBlock() { + return lastBlock; + } + + public byte[] getLastBlockHash() { + return Arrays.copyOf(lastBlockHash, lastBlockHash.length); + } + + public byte[] getResultHistoryDigest() { + return Arrays.copyOf(resultHistoryDigest, resultHistoryDigest.length); + } + + public long getCoveredStartOffset() { + return coveredStartOffset; + } + + public long getMarkerEndOffset() { + return markerEndOffset; + } + + public long getBlockCount() { + return blockCount; + } + + public long getLogicalBytes() { + return logicalBytes; + } + + public long getEncodedBytes() { + return encodedBytes; + } + + public byte[] getPreviousMarkerDigest() { + return Arrays.copyOf(previousMarkerDigest, previousMarkerDigest.length); + } + + public byte[] getCommonTargetDigest() { + return Arrays.copyOf(commonTargetDigest, commonTargetDigest.length); + } + + public int getLaneId() { + return laneId; + } + + public long getSegmentSeq() { + return segmentSeq; + } + + public byte[] getEncodedFrameDigest() { + return encodedFrameDigest == null ? null + : Arrays.copyOf(encodedFrameDigest, encodedFrameDigest.length); + } + } + + public static final class SegmentSeal { + private final int laneId; + private final long segmentSeq; + private final long actualFirstBlock; + private final long actualLastBlock; + private final long blockFrameCount; + private final long entryCount; + private final long logicalPayloadBytes; + private final long encodedBlockFrameBytes; + private final long dataEndOffset; + private final long physicalFileBytes; + private final byte[] firstBlockFrameDigest; + private final byte[] lastBlockFrameDigest; + private final byte[] startHistoryDigest; + private final byte[] endHistoryDigest; + private final byte[] segmentContentDigest; + private final byte[] encodedFrameDigest; + + public SegmentSeal(int laneId, long segmentSeq, long actualFirstBlock, + long actualLastBlock, long blockFrameCount, long entryCount, + long logicalPayloadBytes, long encodedBlockFrameBytes, long dataEndOffset, + long physicalFileBytes, byte[] firstBlockFrameDigest, + byte[] lastBlockFrameDigest, byte[] startHistoryDigest, + byte[] endHistoryDigest, byte[] segmentContentDigest) { + this(laneId, segmentSeq, actualFirstBlock, actualLastBlock, blockFrameCount, + entryCount, logicalPayloadBytes, encodedBlockFrameBytes, dataEndOffset, + physicalFileBytes, firstBlockFrameDigest, lastBlockFrameDigest, + startHistoryDigest, endHistoryDigest, segmentContentDigest, null); + } + + private SegmentSeal(int laneId, long segmentSeq, long actualFirstBlock, + long actualLastBlock, long blockFrameCount, long entryCount, + long logicalPayloadBytes, long encodedBlockFrameBytes, long dataEndOffset, + long physicalFileBytes, byte[] firstBlockFrameDigest, + byte[] lastBlockFrameDigest, byte[] startHistoryDigest, + byte[] endHistoryDigest, byte[] segmentContentDigest, + byte[] encodedFrameDigest) { + this.laneId = laneId; + this.segmentSeq = segmentSeq; + this.actualFirstBlock = actualFirstBlock; + this.actualLastBlock = actualLastBlock; + this.blockFrameCount = blockFrameCount; + this.entryCount = entryCount; + this.logicalPayloadBytes = logicalPayloadBytes; + this.encodedBlockFrameBytes = encodedBlockFrameBytes; + this.dataEndOffset = dataEndOffset; + this.physicalFileBytes = physicalFileBytes; + this.firstBlockFrameDigest = requireHash(firstBlockFrameDigest, + "first block frame digest"); + this.lastBlockFrameDigest = requireHash(lastBlockFrameDigest, + "last block frame digest"); + this.startHistoryDigest = requireHash(startHistoryDigest, + "start history digest"); + this.endHistoryDigest = requireHash(endHistoryDigest, "end history digest"); + this.segmentContentDigest = requireHash(segmentContentDigest, + "segment content digest"); + this.encodedFrameDigest = encodedFrameDigest == null ? null + : requireHash(encodedFrameDigest, "seal encoded frame digest"); + validate(); + } + + private SegmentSeal withEncodedFrameDigest(byte[] digest) { + return new SegmentSeal(laneId, segmentSeq, actualFirstBlock, actualLastBlock, + blockFrameCount, entryCount, logicalPayloadBytes, encodedBlockFrameBytes, + dataEndOffset, physicalFileBytes, firstBlockFrameDigest, + lastBlockFrameDigest, startHistoryDigest, endHistoryDigest, + segmentContentDigest, digest); + } + + private void validate() { + StateArchiveFileFormatV3.laneKind(laneId); + int sealLength = StateArchiveFileFormatV3.SEAL_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH; + if (segmentSeq < 0 || actualFirstBlock < 0 || actualLastBlock < actualFirstBlock + || blockFrameCount <= 0 + || actualLastBlock - actualFirstBlock != blockFrameCount - 1 + || entryCount < 0 || logicalPayloadBytes < 0 || encodedBlockFrameBytes < 0 + || dataEndOffset < StateArchiveFileFormatV3.PART_HEADER_LENGTH + || dataEndOffset > Long.MAX_VALUE - sealLength + || physicalFileBytes != dataEndOffset + sealLength) { + throw new IllegalArgumentException("Invalid State Archive segment seal state"); + } + } + + public int getLaneId() { + return laneId; + } + + public long getSegmentSeq() { + return segmentSeq; + } + + public long getActualFirstBlock() { + return actualFirstBlock; + } + + public long getActualLastBlock() { + return actualLastBlock; + } + + public long getBlockFrameCount() { + return blockFrameCount; + } + + public long getEntryCount() { + return entryCount; + } + + public long getLogicalPayloadBytes() { + return logicalPayloadBytes; + } + + public long getEncodedBlockFrameBytes() { + return encodedBlockFrameBytes; + } + + public long getDataEndOffset() { + return dataEndOffset; + } + + public long getPhysicalFileBytes() { + return physicalFileBytes; + } + + public byte[] getFirstBlockFrameDigest() { + return Arrays.copyOf(firstBlockFrameDigest, firstBlockFrameDigest.length); + } + + public byte[] getLastBlockFrameDigest() { + return Arrays.copyOf(lastBlockFrameDigest, lastBlockFrameDigest.length); + } + + public byte[] getStartHistoryDigest() { + return Arrays.copyOf(startHistoryDigest, startHistoryDigest.length); + } + + public byte[] getEndHistoryDigest() { + return Arrays.copyOf(endHistoryDigest, endHistoryDigest.length); + } + + public byte[] getSegmentContentDigest() { + return Arrays.copyOf(segmentContentDigest, segmentContentDigest.length); + } + + public byte[] getEncodedFrameDigest() { + return encodedFrameDigest == null ? null + : Arrays.copyOf(encodedFrameDigest, encodedFrameDigest.length); + } + } + + public static final class BlockIndexHeader { + private final int laneId; + private final long segmentSeq; + private final byte[] dataSegmentHeaderDigest; + private final byte[] headerDigest; + + public BlockIndexHeader(int laneId, long segmentSeq, + byte[] dataSegmentHeaderDigest) { + this(laneId, segmentSeq, dataSegmentHeaderDigest, null); + } + + private BlockIndexHeader(int laneId, long segmentSeq, + byte[] dataSegmentHeaderDigest, byte[] headerDigest) { + this.laneId = laneId; + this.segmentSeq = segmentSeq; + this.dataSegmentHeaderDigest = requireHash(dataSegmentHeaderDigest, + "data segment header digest"); + this.headerDigest = headerDigest == null ? null + : requireHash(headerDigest, "block index header digest"); + validate(); + } + + private void validate() { + StateArchiveFileFormatV3.laneKind(laneId); + requireNonNegative(segmentSeq, "index segment sequence"); + } + + public int getLaneId() { + return laneId; + } + + public long getSegmentSeq() { + return segmentSeq; + } + + public byte[] getDataSegmentHeaderDigest() { + return Arrays.copyOf(dataSegmentHeaderDigest, dataSegmentHeaderDigest.length); + } + + public byte[] getHeaderDigest() { + return headerDigest == null ? null : Arrays.copyOf(headerDigest, headerDigest.length); + } + } + + public static final class BlockIndexEntry { + private final long blockNumber; + private final long frameOffset; + private final int frameLength; + private final int flags; + private final long encodedFrameDigestPrefix; + + public BlockIndexEntry(long blockNumber, long frameOffset, int frameLength, + long encodedFrameDigestPrefix) { + this(blockNumber, frameOffset, frameLength, 0, encodedFrameDigestPrefix); + } + + private BlockIndexEntry(long blockNumber, long frameOffset, int frameLength, + int flags, long encodedFrameDigestPrefix) { + this.blockNumber = blockNumber; + this.frameOffset = frameOffset; + this.frameLength = frameLength; + this.flags = flags; + this.encodedFrameDigestPrefix = encodedFrameDigestPrefix; + validate(); + } + + private void validate() { + if (blockNumber < 0 || frameOffset < StateArchiveFileFormatV3.PART_HEADER_LENGTH + || frameLength < StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH + || frameLength > StateArchiveFileFormatV3.MAX_BLOCK_FRAME_BYTES || flags != 0) { + throw new IllegalArgumentException("Invalid State Archive block index entry"); + } + } + + public long getBlockNumber() { + return blockNumber; + } + + public long getFrameOffset() { + return frameOffset; + } + + public int getFrameLength() { + return frameLength; + } + + public long getEncodedFrameDigestPrefix() { + return encodedFrameDigestPrefix; + } + } + + public static final class SegmentHeader { + private final int laneId; + private final long segmentSeq; + private final long actualFirstBlock; + private final byte[] previousSegmentDigest; + private final byte[] previousHistoryDigest; + private final short compressionId; + private final byte[] headerDigest; + + public SegmentHeader(int laneId, long segmentSeq, long actualFirstBlock, + byte[] previousSegmentDigest, byte[] previousHistoryDigest, short compressionId) { + this(laneId, segmentSeq, actualFirstBlock, previousSegmentDigest, + previousHistoryDigest, compressionId, null); + } + + private SegmentHeader(int laneId, long segmentSeq, long actualFirstBlock, + byte[] previousSegmentDigest, byte[] previousHistoryDigest, short compressionId, + byte[] headerDigest) { + this.laneId = laneId; + this.segmentSeq = segmentSeq; + this.actualFirstBlock = actualFirstBlock; + this.previousSegmentDigest = requireHash(previousSegmentDigest, + "previous segment digest"); + this.previousHistoryDigest = requireHash(previousHistoryDigest, + "previous history digest"); + this.compressionId = compressionId; + this.headerDigest = headerDigest == null ? null : requireHash(headerDigest, + "segment header digest"); + } + + private void validate() { + StateArchiveFileFormatV3.laneKind(laneId); + requireNonNegative(segmentSeq, "segment sequence"); + requireNonNegative(actualFirstBlock, "first block"); + requireCompression(compressionId); + } + + public int getLaneId() { + return laneId; + } + + public long getSegmentSeq() { + return segmentSeq; + } + + public long getActualFirstBlock() { + return actualFirstBlock; + } + + public byte[] getPreviousSegmentDigest() { + return Arrays.copyOf(previousSegmentDigest, previousSegmentDigest.length); + } + + public byte[] getPreviousHistoryDigest() { + return Arrays.copyOf(previousHistoryDigest, previousHistoryDigest.length); + } + + public short getCompressionId() { + return compressionId; + } + + public byte[] getHeaderDigest() { + return headerDigest == null ? null : Arrays.copyOf(headerDigest, headerDigest.length); + } + } + + public static final class CurrentSegment { + private final int laneId; + private final long segmentSeq; + private final long firstBlock; + private final long currentLastBlock; + private final long dataEndOffset; + private final long blockFrameCount; + private final byte[] headerDigest; + + public CurrentSegment(int laneId, long segmentSeq, long firstBlock, long currentLastBlock, + long dataEndOffset, long blockFrameCount, byte[] headerDigest) { + StateArchiveFileFormatV3.laneKind(laneId); + if (segmentSeq < 0 || firstBlock < 0 || currentLastBlock < firstBlock + || blockFrameCount <= 0 || currentLastBlock - firstBlock + 1 != blockFrameCount + || dataEndOffset < StateArchiveFileFormatV3.PART_HEADER_LENGTH) { + throw new IllegalArgumentException("Invalid State Archive current SegmentMap state"); + } + this.laneId = laneId; + this.segmentSeq = segmentSeq; + this.firstBlock = firstBlock; + this.currentLastBlock = currentLastBlock; + this.dataEndOffset = dataEndOffset; + this.blockFrameCount = blockFrameCount; + this.headerDigest = requireHash(headerDigest, "current segment header digest"); + } + + public int getLaneId() { + return laneId; + } + + public long getSegmentSeq() { + return segmentSeq; + } + + public long getFirstBlock() { + return firstBlock; + } + + public long getCurrentLastBlock() { + return currentLastBlock; + } + + public long getDataEndOffset() { + return dataEndOffset; + } + + public long getBlockFrameCount() { + return blockFrameCount; + } + + public byte[] getHeaderDigest() { + return Arrays.copyOf(headerDigest, headerDigest.length); + } + } + + public static final class SealedSegment { + private final int laneId; + private final long segmentSeq; + private final long firstBlock; + private final long lastBlock; + private final long blockFrameCount; + private final long dataFileBytes; + private final long blockIndexBytes; + private final byte[] segmentHeaderDigest; + private final byte[] segmentContentDigest; + private final byte[] sealFrameDigest; + private final byte[] manifestDigest; + + public SealedSegment(int laneId, long segmentSeq, long firstBlock, long lastBlock, + long blockFrameCount, long dataFileBytes, long blockIndexBytes, + byte[] segmentHeaderDigest, byte[] segmentContentDigest, byte[] sealFrameDigest, + byte[] manifestDigest) { + this.laneId = laneId; + this.segmentSeq = segmentSeq; + this.firstBlock = firstBlock; + this.lastBlock = lastBlock; + this.blockFrameCount = blockFrameCount; + this.dataFileBytes = dataFileBytes; + this.blockIndexBytes = blockIndexBytes; + this.segmentHeaderDigest = requireHash(segmentHeaderDigest, "segment header digest"); + this.segmentContentDigest = requireHash(segmentContentDigest, "segment content digest"); + this.sealFrameDigest = requireHash(sealFrameDigest, "seal frame digest"); + this.manifestDigest = requireHash(manifestDigest, "manifest digest"); + validate(); + } + + private void validate() { + StateArchiveFileFormatV3.laneKind(laneId); + if (segmentSeq < 0 || firstBlock < 0 || lastBlock < firstBlock + || blockFrameCount <= 0 || lastBlock - firstBlock + 1 != blockFrameCount + || dataFileBytes < StateArchiveFileFormatV3.PART_HEADER_LENGTH + + StateArchiveFileFormatV3.SEAL_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH + || blockIndexBytes < StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH) { + throw new IllegalArgumentException("Invalid State Archive sealed SegmentMap state"); + } + } + + public int getLaneId() { + return laneId; + } + + public long getSegmentSeq() { + return segmentSeq; + } + + public long getFirstBlock() { + return firstBlock; + } + + public long getLastBlock() { + return lastBlock; + } + + public long getBlockFrameCount() { + return blockFrameCount; + } + + public long getDataFileBytes() { + return dataFileBytes; + } + + public long getBlockIndexBytes() { + return blockIndexBytes; + } + + public byte[] getSegmentHeaderDigest() { + return Arrays.copyOf(segmentHeaderDigest, segmentHeaderDigest.length); + } + + public byte[] getSegmentContentDigest() { + return Arrays.copyOf(segmentContentDigest, segmentContentDigest.length); + } + + public byte[] getSealFrameDigest() { + return Arrays.copyOf(sealFrameDigest, sealFrameDigest.length); + } + + public byte[] getManifestDigest() { + return Arrays.copyOf(manifestDigest, manifestDigest.length); + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java index a61e01426dc..3d0b4c7b56e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointPayloadFactory.java @@ -10,7 +10,7 @@ import org.tron.core.db2.archive.BlockReverseDiff; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.archive.StateArchiveHotBatchDescriptor; -import org.tron.core.db2.archive.StateArchiveHotCheckpointMaterializer; +import org.tron.core.db2.archive.StateArchiveCheckpointPlanner; import org.tron.core.db2.common.Key; import org.tron.core.db2.common.Value; import org.tron.core.db2.common.WrappedByteArray; @@ -22,14 +22,14 @@ public final class CommonCheckpointPayloadFactory { /** Captures v2 coordination data while retaining Archive bodies only in transient memory. */ public CommonCheckpointCapture captureV2(byte[] formatIdentity, List databases, - int flushCount, StateArchiveHotCheckpointMaterializer hotMaterializer) throws IOException { + int flushCount, StateArchiveCheckpointPlanner archivePlanner) throws IOException { CommonCheckpointPayload captured = capture(formatIdentity, databases, flushCount); List archiveDiffs = new ArrayList<>(); for (CommonCheckpointPayload.BlockPayload block : captured.getBlocks()) { archiveDiffs.add(block.getArchiveDiff()); } - StateArchiveHotBatchDescriptor binding = Objects.requireNonNull(hotMaterializer, - "hotMaterializer") + StateArchiveHotBatchDescriptor binding = Objects.requireNonNull(archivePlanner, + "archivePlanner") .planCheckpoint(archiveDiffs); CommonCheckpointPayload coordination = CommonCheckpointPayload.coordinateV2(captured, binding); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java index 9569f7c86d7..f4a0781e402 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java @@ -8,6 +8,7 @@ import java.util.Objects; import java.util.function.LongSupplier; import org.tron.core.db2.archive.StateArchiveCheckpointMaterializer; +import org.tron.core.db2.archive.StateArchiveCheckpointPlanner; import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; import org.tron.core.db2.archive.StateArchiveHotCheckpointMaterializer; import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; @@ -27,7 +28,7 @@ public final class CommonCheckpointRuntime implements AutoCloseable { private final StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory; private final CommonCheckpointMemoryRebaser memoryRebaser; private final CommonCheckpointHotRecovery hotRecovery; - private final StateArchiveHotCheckpointMaterializer hotMaterializer; + private final StateArchiveCheckpointPlanner archivePlanner; private final CommonCheckpointMaterializedStore materializedStore; private final LongSupplier nanoTime; private final TimingSink timingSink; @@ -57,6 +58,17 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, + Path archiveDirectory, byte[] formatIdentity, Engine engine, + StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory, + CommonCheckpointMemoryRebaser memoryRebaser, + StateArchiveCheckpointPlanner archivePlanner) { + this(owner, databases, archiveDirectory, formatIdentity, engine, latestFactory, + memoryRebaser, null, Objects.requireNonNull(archivePlanner, "archivePlanner"), + null, System::nanoTime, CommonCheckpointRuntime::logTiming); + } + public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List databases, Path archiveDirectory, byte[] formatIdentity, Engine engine, StateArchiveCheckpointReadSnapshot.PinnedLatestStateFactory latestFactory, @@ -91,7 +103,7 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List(Objects.requireNonNull(databases, "databases")); @@ -105,13 +117,14 @@ public CommonCheckpointRuntime(CommonCheckpointRuntimeOwner owner, List { CommonCheckpointRuntimeOwner owner = new CommonCheckpointRuntimeOwner(coordinator); + if (admittedAppendMaterializer != null) { + return new CommonCheckpointRuntime(owner, snapshots.getDbs(), appendDirectory, + formatIdentity, archiveRuntimeEngine, latest::pin, + admittedOwner::prepareCommonCheckpointRebase, admittedAppendMaterializer); + } if (admittedHotMaterializer == null) { return new CommonCheckpointRuntime(owner, snapshots.getDbs(), archiveDirectory, formatIdentity, archiveRuntimeEngine, latest::pin, @@ -942,7 +960,10 @@ private void initCommonCheckpoint() { } if (Files.isRegularFile(pathDirectory.resolve(PathStateCheckpointMaterializer.CURRENT_FILE), LinkOption.NOFOLLOW_LINKS)) { - if (admittedHotStore == null) { + if (admittedAppendMaterializer != null) { + requireAppendCommonPublishedAuthorities(checkpointDirectory, pathDirectory, + formatIdentity, admittedAppendMaterializer); + } else if (admittedHotStore == null) { requireCommonPublishedAuthorities(checkpointDirectory, archiveDirectory, pathDirectory, formatIdentity, servingIndexEngine, materializedStore); } else { @@ -965,6 +986,7 @@ private void initCommonCheckpoint() { pathOwner = null; attachment = null; hotStore = null; + appendMaterializer = null; logger.info("Common checkpoint runtime attached: checkpoint={}, archive={}, path={}, " + "head={}, format={}, pathEngine={}, archiveEngine={}", checkpointDirectory, archiveDirectory, pathDirectory, canonical.getBlockNumber(), CommonCheckpointFormat.ID, @@ -990,6 +1012,7 @@ private void initCommonCheckpoint() { if (attachment != null) { attachment.close(); hotStore = null; + appendMaterializer = null; } if (hotStore != null) { try { @@ -998,6 +1021,13 @@ private void initCommonCheckpoint() { failure.addSuppressed(closeFailure); } } + if (appendMaterializer != null) { + try { + appendMaterializer.close(); + } catch (java.io.IOException closeFailure) { + failure.addSuppressed(closeFailure); + } + } if (pathOwner != null) { try { pathOwner.close(); @@ -1014,7 +1044,9 @@ private void recoverPendingCommonCheckpoint(SnapshotManager snapshots, PathStateStoreManifest.Engine pathEngine, PathStateStoreManifest.Engine archiveEngine, long residentNodeCacheBytes, byte[] formatIdentity, CommonCheckpointBaselineFile baselineFile, boolean baselineExists, - boolean modeAdmitted) throws java.io.IOException { + boolean modeAdmitted, boolean appendEnabled, Path appendDirectory, + org.tron.core.config.args.StorageConfig.StateArchiveAppendFileConfig appendConfig) + throws java.io.IOException { CommonCheckpointFile checkpointFile = new CommonCheckpointFile(checkpointDirectory); if (!checkpointFile.isPresent()) { return; @@ -1026,16 +1058,22 @@ private void recoverPendingCommonCheckpoint(SnapshotManager snapshots, CommonCheckpointBaseline baseline = baselineFile.load(); CommonCheckpointMaterializedStore materializedStore = new CommonCheckpointMaterializedStore(checkpointDirectory); + org.tron.core.db2.core.CommonCheckpointMaterializer archiveRecovery = appendEnabled + ? new StateArchiveAppendCheckpointMaterializerV3(appendDirectory, formatIdentity, + archiveEngine, baseline.getStateRoot(), StateArchiveFileFormatV3.COMPRESSION_NONE, + appendConfig.getSegmentTargetBytes()) + : new StateArchiveCheckpointMaterializer(archiveDirectory, formatIdentity, baseline, + archiveEngine, materializedStore); try (PathStateCheckpointMaterializer.RecoverySession pathRecovery = PathStateCheckpointMaterializer.openRecovery(pathDirectory, pathEngine, residentNodeCacheBytes, formatIdentity, baseline, materializedStore); + org.tron.core.db2.core.CommonCheckpointMaterializer admittedArchive = archiveRecovery; CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( checkpointFile, new ChainbaseCheckpointMaterializer(checkpointDirectory, formatIdentity, snapshots.getDbs(), baseline, materializedStore), pathRecovery.getMaterializer(), - new StateArchiveCheckpointMaterializer(archiveDirectory, formatIdentity, baseline, - archiveEngine, materializedStore))) { + admittedArchive)) { CommonCheckpointRedoCoordinator.RecoveryAction action = coordinator.recover(); logger.info("Common checkpoint startup redo completed before PathState open: action={}", action); @@ -1105,6 +1143,30 @@ private static void requireHotCommonPublishedAuthorities(Path checkpointDirector } } + private static void requireAppendCommonPublishedAuthorities(Path checkpointDirectory, + Path pathDirectory, byte[] formatIdentity, + StateArchiveAppendCheckpointMaterializerV3 materializer) throws java.io.IOException { + ChainbaseCheckpointMaterializer.PublishedHead chain = + ChainbaseCheckpointMaterializer.loadPublishedHead(checkpointDirectory, formatIdentity); + PathStateCheckpointMaterializer.PublishedHead path = + PathStateCheckpointMaterializer.loadPublishedHead(pathDirectory, formatIdentity); + org.tron.core.db2.core.CommonCheckpointTarget archive = materializer + .loadPublishedTargetIfPresent().orElseThrow(() -> + new java.io.IOException("Append-file Archive readable target is missing")); + BlockSnapshotMeta last = archive.getLastBlock(); + if (chain.getEpoch() != last.getEpoch() || path.getEpoch() != last.getEpoch() + || chain.getBlockNumber() != last.getBlockNumber() + || path.getBlockNumber() != last.getBlockNumber() + || !Arrays.equals(chain.getBlockHash(), last.getBlockHash()) + || !Arrays.equals(path.getBlockHash(), last.getBlockHash()) + || !Arrays.equals(chain.getPayloadDigest(), archive.getPayloadDigest()) + || !Arrays.equals(path.getPayloadDigest(), archive.getPayloadDigest()) + || !Arrays.equals(chain.getStateRoot(), archive.getStateRoot()) + || !Arrays.equals(path.getStateRoot(), archive.getStateRoot())) { + throw new java.io.IOException("Append-file common checkpoint authorities differ"); + } + } + private static void requireEmptyOrMissing(Path directory, String label) throws java.io.IOException { if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index a3c1fa528a7..750d28b01c6 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -45,6 +45,13 @@ storage { # Independent auxiliary DB engines. Changing one requires rebuilding/resyncing that DB. stateArchive.servingIndexEngine = "ROCKSDB" stateArchive.hotStore.engine = "ROCKSDB" + # Default-off five-lane append-file v3 path; requires commonCheckpoint.enabled. + stateArchive.appendFile.enabled = false + stateArchive.appendFile.formatVersion = 3 + stateArchive.appendFile.appendBufferBytes = 2097152 + stateArchive.appendFile.maxBlockFrameBytes = 67108864 + stateArchive.appendFile.segmentTargetBytes = 2000000000 + stateArchive.appendFile.shardMaxSegments = 1024 commonCheckpoint.enabled = false commonCheckpoint.directory = "common-checkpoint" diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 95227a53b5e..b5a57d33883 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -355,6 +355,7 @@ public void testAuxiliaryDatabaseEnginesMapIndependentlyFromChainbase() { override.put("storage.db.engine", "LEVELDB"); override.put("storage.stateArchive.servingIndexEngine", "ROCKSDB"); override.put("storage.stateArchive.hotStore.engine", "LEVELDB"); + override.put("storage.stateArchive.appendFile.segmentTargetBytes", "123456789"); override.put("storage.pathStateRoot.engine", "ROCKSDB"); Config config = ConfigFactory.parseMap(override) .withFallback(ConfigFactory.defaultReference()); @@ -365,6 +366,8 @@ public void testAuxiliaryDatabaseEnginesMapIndependentlyFromChainbase() { Assert.assertEquals("LEVELDB", storage.getDbEngine()); Assert.assertEquals("ROCKSDB", storage.getStateArchiveServingIndexEngine()); Assert.assertEquals("LEVELDB", storage.getStateArchiveHotStoreSettings().getEngine()); + Assert.assertEquals(123456789L, + storage.getStateArchiveAppendFileSettings().getSegmentTargetBytes()); Assert.assertEquals("ROCKSDB", storage.getPathStateRootEngine()); Args.clearParam(); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java new file mode 100644 index 00000000000..5f78fe1d42f --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java @@ -0,0 +1,319 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.StateArchiveFiveLaneSegmentWriterV3.SyncStage; +import org.tron.core.db2.core.CommonCheckpointCapture; +import org.tron.core.db2.core.CommonCheckpointFile; +import org.tron.core.db2.core.CommonCheckpointMaterializer; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Authority; +import org.tron.core.db2.core.CommonCheckpointMaterializer.Status; +import org.tron.core.db2.core.CommonCheckpointPayload; +import org.tron.core.db2.core.CommonCheckpointRedoCoordinator; +import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateFlushTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class StateArchiveAppendCheckpointMaterializerV3Test { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void preparesBeforeWalPublishesAndReopensExactTarget() throws Exception { + Path root = temporaryFolder.newFolder("append-materializer").toPath(); + byte[] format = hash(70); + byte[] baseline = hash(80); + BlockReverseDiff diff = diff(1, 64); + CommonCheckpointPayload payload; + CommonCheckpointTarget target; + try (StateArchiveAppendCheckpointMaterializerV3 archive = materializer( + root, format, baseline, 10_000)) { + StateArchiveHotBatchDescriptor descriptor = archive.planCheckpoint( + Collections.singletonList(diff)); + payload = payload(format, Collections.singletonList(diff), descriptor); + target = CommonCheckpointTarget.from(payload); + CommonCheckpointCapture capture = CommonCheckpointCapture.create(payload, + Collections.singletonList(diff), descriptor); + + assertEquals(Status.NEEDS_MATERIALIZATION, archive.inspect(target)); + assertThrows(java.io.IOException.class, () -> archive.materialize(payload, target)); + assertEquals(target, archive.prepare(capture)); + assertEquals(target, archive.prepare(capture)); + assertEquals(Status.MATERIALIZED, archive.inspect(target)); + assertFalse(Files.exists(root.resolve(StateArchiveCheckpointMaterializer.READABLE_FILE))); + archive.materialize(payload, target); + archive.publish(target); + assertEquals(Status.PUBLISHED, archive.inspect(target)); + } + + try (StateArchiveAppendCheckpointMaterializerV3 reopened = materializer( + root, format, baseline, 10_000)) { + assertEquals(Status.PUBLISHED, reopened.inspect(target)); + reopened.materialize(payload, target); + reopened.publish(target); + } + } + + @Test + public void crossesExistingCoordinatorWithoutArchiveBodiesInWal() throws Exception { + Path root = temporaryFolder.newFolder("append-coordinator").toPath(); + byte[] format = hash(71); + BlockReverseDiff diff = diff(1, 32); + StateArchiveAppendCheckpointMaterializerV3 archive = materializer( + root.resolve("history"), format, hash(81), 10_000); + StateArchiveHotBatchDescriptor descriptor = archive.planCheckpoint( + Collections.singletonList(diff)); + CommonCheckpointPayload payload = payload(format, Collections.singletonList(diff), + descriptor); + CommonCheckpointTarget target = archive.prepare(CommonCheckpointCapture.create(payload, + Collections.singletonList(diff), descriptor)); + + try (CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( + new CommonCheckpointFile(root.resolve("wal")), new FakeMaterializer(Authority.CHAINBASE), + new FakeMaterializer(Authority.PATH_STATE), archive)) { + assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.COMPLETED_REDO, + coordinator.apply(payload)); + assertEquals(Status.PUBLISHED, archive.inspect(target)); + assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.NO_CHECKPOINT, + coordinator.recover()); + } + } + + @Test + public void persistsRotationSpanningSap3DuringPrepare() throws Exception { + Path root = temporaryFolder.newFolder("append-materializer-rotation").toPath(); + byte[] format = hash(72); + byte[] baseline = hash(82); + List diffs = Arrays.asList(diff(1, 1_400), diff(2, 0)); + try (StateArchiveAppendCheckpointMaterializerV3 archive = materializer( + root, format, baseline, 1_500)) { + StateArchiveHotBatchDescriptor descriptor = archive.planCheckpoint(diffs); + CommonCheckpointPayload payload = payload(format, diffs, descriptor); + CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); + archive.prepare(CommonCheckpointCapture.create(payload, diffs, descriptor)); + StateArchiveFiveLaneSegmentWriterV3.ArchiveDurabilityProof proof = + StateArchiveFiveLaneDurabilityProofV3.decode(Files.readAllBytes( + root.resolve(StateArchiveFiveLaneDurabilityProofV3.FILE_NAME))); + assertEquals(6, proof.getFileTails().size()); + assertEquals(Status.MATERIALIZED, archive.inspect(target)); + } + } + + @Test + public void advancesTwoPublishedTargetsAndPreservesFreshHotBindingBytes() throws Exception { + Path root = temporaryFolder.newFolder("append-materializer-sequential").toPath(); + byte[] format = hash(73); + byte[] baseline = hash(83); + BlockReverseDiff first = diff(1, 48); + try (StateArchiveAppendCheckpointMaterializerV3 archive = materializer( + root.resolve("history"), format, baseline, 10_000)) { + StateArchiveHotBatchDescriptor firstDescriptor = archive.planCheckpoint( + Collections.singletonList(first)); + assertEquals(StateArchiveHotStore.planCheckpointDescriptor(Engine.LEVELDB, + 0, hash(0), new byte[StateArchiveFileFormatV3.HASH_LENGTH], + Collections.singletonList(first)), firstDescriptor); + CommonCheckpointPayload firstPayload = payload(format, + Collections.singletonList(first), firstDescriptor); + CommonCheckpointTarget firstTarget = archive.prepare(CommonCheckpointCapture.create( + firstPayload, Collections.singletonList(first), firstDescriptor)); + archive.publish(firstTarget); + assertEquals(Status.PUBLISHED, archive.inspect(firstTarget)); + + BlockReverseDiff second = diff(2, 16); + StateArchiveHotBatchDescriptor secondDescriptor = archive.planCheckpoint( + Collections.singletonList(second)); + CommonCheckpointPayload secondPayload = payload(format, + Collections.singletonList(second), secondDescriptor); + CommonCheckpointTarget secondTarget = archive.prepare(CommonCheckpointCapture.create( + secondPayload, Collections.singletonList(second), secondDescriptor)); + assertEquals(Status.MATERIALIZED, archive.inspect(secondTarget)); + archive.publish(secondTarget); + assertEquals(Status.PUBLISHED, archive.inspect(secondTarget)); + assertThrows(java.io.IOException.class, () -> archive.inspect(firstTarget)); + } + } + + @Test + public void resumesEveryRotationMarkerPhaseBeforeSap3Publication() throws Exception { + for (SyncStage stage : new SyncStage[]{SyncStage.MARKER_WRITTEN, + SyncStage.DATA_FORCED, SyncStage.MARKER_VERIFIED}) { + Path root = temporaryFolder.newFolder("append-rotation-resume-" + stage).toPath(); + byte[] format = hash(74); + byte[] baseline = hash(84); + List diffs = Arrays.asList(diff(1, 1_400), diff(2, 0)); + CommonCheckpointCapture capture = capture(root, format, baseline, diffs, 1_500); + CommonCheckpointTarget target = CommonCheckpointTarget.from(capture.getPayload()); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + StateArchiveFiveLaneBlockCodecV3.EncodedBundle first = codec.encode( + diffs.get(0), baseline, StateArchiveFileFormatV3.COMPRESSION_NONE); + StateArchiveFiveLaneBlockCodecV3.EncodedBundle second = codec.encode( + diffs.get(1), first.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)) { + writer.appendForCheckpoint(first, 2, target.getPayloadDigest()); + assertThrows(java.io.IOException.class, + () -> writer.appendForCheckpoint(second, 2, target.getPayloadDigest(), + (actual, laneId) -> { + if (actual == stage && laneId == 0) { + throw new java.io.IOException("rotation crash at " + stage); + } + })); + } + try (StateArchiveAppendCheckpointMaterializerV3 recovered = materializer( + root, format, baseline, 1_500)) { + recovered.prepare(capture); + assertEquals(Status.MATERIALIZED, recovered.inspect(target)); + assertEquals(6, StateArchiveFiveLaneDurabilityProofV3.decode(Files.readAllBytes( + root.resolve(StateArchiveFiveLaneDurabilityProofV3.FILE_NAME))) + .getFileTails().size()); + } + } + } + + @Test + public void resumesEveryFinalBarrierPhaseBeforeSap3Publication() throws Exception { + for (SyncStage stage : SyncStage.values()) { + Path root = temporaryFolder.newFolder("append-sync-resume-" + stage).toPath(); + byte[] format = hash(75); + byte[] baseline = hash(85); + List diffs = Collections.singletonList(diff(1, 24)); + CommonCheckpointCapture capture = capture(root, format, baseline, diffs, 10_000); + CommonCheckpointTarget target = CommonCheckpointTarget.from(capture.getPayload()); + StateArchiveFiveLaneBlockCodecV3.EncodedBundle bundle = + new StateArchiveFiveLaneBlockCodecV3().encode(diffs.get(0), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + writer.appendForCheckpoint(bundle, 1, target.getPayloadDigest()); + assertThrows(java.io.IOException.class, + () -> writer.sync(1, point(bundle), target.getPayloadDigest(), + (actual, laneId) -> { + if (actual == stage) { + throw new java.io.IOException("sync crash at " + stage); + } + })); + } + try (StateArchiveAppendCheckpointMaterializerV3 recovered = materializer( + root, format, baseline, 10_000)) { + recovered.prepare(capture); + assertEquals(Status.MATERIALIZED, recovered.inspect(target)); + } + } + } + + private static StateArchiveAppendCheckpointMaterializerV3 materializer(Path root, + byte[] format, byte[] baseline, long rotationTarget) throws Exception { + return new StateArchiveAppendCheckpointMaterializerV3(root, format, Engine.LEVELDB, + baseline, StateArchiveFileFormatV3.COMPRESSION_NONE, rotationTarget); + } + + private static CommonCheckpointCapture capture(Path root, byte[] format, byte[] baseline, + List diffs, long rotationTarget) throws Exception { + StateArchiveHotBatchDescriptor descriptor; + try (StateArchiveAppendCheckpointMaterializerV3 planner = materializer( + root, format, baseline, rotationTarget)) { + descriptor = planner.planCheckpoint(diffs); + } + CommonCheckpointPayload payload = payload(format, diffs, descriptor); + return CommonCheckpointCapture.create(payload, diffs, descriptor); + } + + private static StateArchiveFiveLaneRecoveryIntentV3.RecoveryPoint point( + StateArchiveFiveLaneBlockCodecV3.EncodedBundle bundle) { + BlockSnapshotMeta meta = bundle.getDiff().getMeta(); + return new StateArchiveFiveLaneRecoveryIntentV3.RecoveryPoint(meta.getEpoch(), + meta.getBlockNumber(), meta.getTimestamp(), meta.getBlockHash(), meta.getParentHash(), + bundle.getResultHistoryDigest()); + } + + private static CommonCheckpointPayload payload(byte[] format, List diffs, + StateArchiveHotBatchDescriptor descriptor) { + List bindings = new ArrayList<>(); + for (BlockReverseDiff diff : diffs) { + BlockSnapshotMeta meta = diff.getMeta(); + PathStateFlushTarget.BlockBinding binding = mock(PathStateFlushTarget.BlockBinding.class); + when(binding.getMeta()).thenReturn(meta); + when(binding.getParentStateRoot()).thenReturn(hash(30 + (int) meta.getBlockNumber())); + when(binding.getStateRoot()).thenReturn(hash(31 + (int) meta.getBlockNumber())); + when(binding.getTransitionPayloadDigest()).thenReturn(hash(90)); + bindings.add(binding); + } + PathStateFlushTarget path = mock(PathStateFlushTarget.class); + byte[] parentStateRoot = bindings.get(0).getParentStateRoot(); + byte[] stateRoot = bindings.get(bindings.size() - 1).getStateRoot(); + when(path.getBlocks()).thenReturn(bindings); + when(path.getParentStateRoot()).thenReturn(parentStateRoot); + when(path.getStateRoot()).thenReturn(stateRoot); + when(path.getStores()).thenReturn(Collections.emptyList()); + when(path.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + return CommonCheckpointPayload.createV2(format, path, descriptor, Collections.emptyList()); + } + + private static BlockReverseDiff diff(int blockNumber, int valueLength) { + List groups; + if (valueLength == 0) { + groups = Collections.emptyList(); + } else { + byte[] value = new byte[valueLength]; + Arrays.fill(value, (byte) blockNumber); + groups = Collections.singletonList(new DbGroup("code", Collections.singletonList( + new Entry(new byte[]{1}, OldValue.present(value))))); + } + return new BlockReverseDiff(BlockSnapshotMeta.forBlock(blockNumber, hash(blockNumber), + hash(blockNumber - 1), blockNumber * 3_000L), groups); + } + + private static byte[] hash(int marker) { + byte[] hash = new byte[32]; + hash[31] = (byte) marker; + return hash; + } + + private static final class FakeMaterializer implements CommonCheckpointMaterializer { + private final Authority authority; + private Status status = Status.NEEDS_MATERIALIZATION; + + private FakeMaterializer(Authority authority) { + this.authority = authority; + } + + @Override + public Authority authority() { + return authority; + } + + @Override + public Status inspect(CommonCheckpointTarget target) { + return status; + } + + @Override + public void materialize(CommonCheckpointPayload payload, CommonCheckpointTarget target) { + status = Status.MATERIALIZED; + } + + @Override + public void publish(CommonCheckpointTarget target) { + status = Status.PUBLISHED; + } + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveBlockFrameCodecV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveBlockFrameCodecV3Test.java new file mode 100644 index 00000000000..fb15ed1405d --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveBlockFrameCodecV3Test.java @@ -0,0 +1,216 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; + +public class StateArchiveBlockFrameCodecV3Test { + + private static final String FORMAT_DESCRIPTOR_HEX = + "464d543300030000000000600150012001400030020000800020010000a000300020" + + "0050001000010000000100030001000100010001000040000000000077359400" + + "00000000040000000000000007ffffff0000000000000000000000000000"; + + private final StateArchiveBlockFrameCodecV3 codec = + new StateArchiveBlockFrameCodecV3(); + + @Test + public void freezesFormatDescriptorAndEmptyBlockGoldenFrame() { + assertEquals(96, StateArchiveFileFormatV3.formatDescriptor().length); + assertEquals(FORMAT_DESCRIPTOR_HEX, + hex(StateArchiveFileFormatV3.formatDescriptor())); + assertEquals("380d370ba9adf724ae313c05ede5625749c82ab1240ef5f8b510e3aa5c4f649a", + hex(StateArchiveFileFormatV3.formatDigest())); + assertEquals("19c7827c66ad7811d60a964d3a24b85a0b2ed41119bc5eedf444835f738159c8", + hex(StateArchiveFileFormatV3.storeDescriptorDigest())); + + StateArchiveBlockFrameCodecV3.EncodedBlock encoded = codec.encode( + diff(12, Collections.emptyList()), hash(10), + StateArchiveFileFormatV3.COMPRESSION_NONE); + assertEquals(416, encoded.getFrame().length); + assertEquals(32, encoded.getCanonicalPayload().length); + assertEquals("3123bbd47cd083892c1c5cd9e38eb016081d47dc238ddc5757ccb7e4112f63fb", + hex(StateArchiveFileFormatV3.sha256(encoded.getFrame()))); + + StateArchiveBlockFrameCodecV3.DecodedBlock decoded = codec.decode(encoded.getFrame()); + assertEquals(diff(12, Collections.emptyList()).getMeta(), decoded.getDiff().getMeta()); + assertTrue(decoded.getDiff().getGroups().isEmpty()); + } + + @Test + public void canonicalizesStoreIdsAndUnsignedKeysAndPreservesValueStates() { + List storeOneEntries = Arrays.asList( + new Entry(bytes(0xff), OldValue.present(bytes(4, 5))), + new Entry(bytes(0x00, 0x01), OldValue.absent()), + new Entry(bytes(0x80), OldValue.present(new byte[0])), + new Entry(bytes(0x00), OldValue.present(bytes(7)))); + BlockReverseDiff first = diff(12, Arrays.asList( + new DbGroup("IncrementalMerkleTree", Collections.singletonList( + new Entry(bytes(0x7f), OldValue.absent()))), + new DbGroup("abi", storeOneEntries))); + List reversedEntries = new ArrayList<>(storeOneEntries); + Collections.reverse(reversedEntries); + BlockReverseDiff reordered = diff(12, Arrays.asList( + new DbGroup("abi", reversedEntries), + new DbGroup("IncrementalMerkleTree", Collections.singletonList( + new Entry(bytes(0x7f), OldValue.absent()))))); + + StateArchiveBlockFrameCodecV3.EncodedBlock firstFrame = codec.encode(first, hash(10), + StateArchiveFileFormatV3.COMPRESSION_NONE); + StateArchiveBlockFrameCodecV3.EncodedBlock reorderedFrame = codec.encode(reordered, + hash(10), StateArchiveFileFormatV3.COMPRESSION_NONE); + assertArrayEquals(firstFrame.getFrame(), reorderedFrame.getFrame()); + + ByteBuffer payload = ByteBuffer.wrap(firstFrame.getCanonicalPayload()); + payload.position(StateArchiveFileFormatV3.PAYLOAD_HEADER_LENGTH); + assertEquals(1, Short.toUnsignedInt(payload.getShort())); + StateArchiveBlockFrameCodecV3.DecodedBlock decoded = codec.decode(firstFrame.getFrame()); + assertEquivalent(first, decoded.getDiff()); + List decodedStoreOne = group(decoded.getDiff(), "abi").getEntries(); + assertArrayEquals(bytes(0x00), decodedStoreOne.get(0).getKey()); + assertArrayEquals(bytes(0x00, 0x01), decodedStoreOne.get(1).getKey()); + assertArrayEquals(bytes(0x80), decodedStoreOne.get(2).getKey()); + assertArrayEquals(bytes(0xff), decodedStoreOne.get(3).getKey()); + assertTrue(decodedStoreOne.get(0).getOldValue().isPresent()); + assertFalse(decodedStoreOne.get(1).getOldValue().isPresent()); + assertEquals(0, decodedStoreOne.get(2).getOldValue().getValue().length); + } + + @Test + public void compressionChangesPhysicalFrameButNotLogicalIdentity() { + BlockReverseDiff diff = diff(42, Collections.singletonList( + new DbGroup("account", Collections.singletonList( + new Entry(bytes(1, 2, 3), OldValue.present(new byte[4096])))))); + StateArchiveBlockFrameCodecV3.EncodedBlock none = codec.encode(diff, hash(41), + StateArchiveFileFormatV3.COMPRESSION_NONE); + StateArchiveBlockFrameCodecV3.EncodedBlock compressed = codec.encode(diff, hash(41), + StateArchiveFileFormatV3.COMPRESSION_RAW_DEFLATE_LEVEL_1); + + assertFalse(Arrays.equals(none.getFrame(), compressed.getFrame())); + assertTrue(compressed.getFrame().length < none.getFrame().length); + assertArrayEquals(none.getCanonicalPayload(), compressed.getCanonicalPayload()); + assertArrayEquals(none.getPayloadDigest(), compressed.getPayloadDigest()); + assertArrayEquals(none.getBlockHistoryDigest(), compressed.getBlockHistoryDigest()); + assertArrayEquals(none.getResultHistoryDigest(), compressed.getResultHistoryDigest()); + assertFalse(Arrays.equals(none.getEncodedFrameDigest(), + compressed.getEncodedFrameDigest())); + assertEquivalent(diff, codec.decode(compressed.getFrame()).getDiff()); + } + + @Test + public void matchesLegacyHotBodyAtStoreKeyAndOldValueBoundary() { + BlockReverseDiff expected = diff(77, Arrays.asList( + new DbGroup("votes", Arrays.asList( + new Entry(bytes(0x80), OldValue.present(bytes(9))), + new Entry(bytes(0xff), OldValue.absent()))), + new DbGroup("account", Arrays.asList( + new Entry(bytes(0), OldValue.present(new byte[0])), + new Entry(bytes(0, 1), OldValue.present(bytes(3, 4))))))); + BlockReverseDiff legacy = new BlockHistoryCodec().decode( + new BlockHistoryCodec().encode(expected)); + BlockReverseDiff v3 = codec.decode(codec.encode(expected, hash(76), + StateArchiveFileFormatV3.COMPRESSION_NONE).getFrame()).getDiff(); + + assertEquivalent(legacy, v3); + } + + @Test + public void rejectsDuplicateStoreKeyCorruptionAndIdentityDrift() { + assertThrows(ArchivePersistenceException.class, + () -> StateArchiveFileFormatV3.requireExactCapture( + Collections.singletonList("account"))); + + BlockReverseDiff duplicateStore = diff(12, Arrays.asList( + new DbGroup("account", Collections.singletonList( + new Entry(bytes(1), OldValue.absent()))), + new DbGroup("account", Collections.singletonList( + new Entry(bytes(2), OldValue.absent()))))); + assertThrows(IllegalArgumentException.class, () -> codec.encode(duplicateStore, hash(11), + StateArchiveFileFormatV3.COMPRESSION_NONE)); + + BlockReverseDiff duplicateKey = diff(12, Collections.singletonList( + new DbGroup("account", Arrays.asList( + new Entry(bytes(1), OldValue.absent()), + new Entry(bytes(1), OldValue.present(bytes(2))))))); + assertThrows(IllegalArgumentException.class, () -> codec.encode(duplicateKey, hash(11), + StateArchiveFileFormatV3.COMPRESSION_NONE)); + + BlockReverseDiff epochDrift = new BlockReverseDiff(new BlockSnapshotMeta( + 11, 12, hash(12), hash(11), 36_000L), Collections.emptyList()); + assertThrows(IllegalArgumentException.class, () -> codec.encode(epochDrift, hash(10), + StateArchiveFileFormatV3.COMPRESSION_NONE)); + + byte[] frame = codec.encode(diff(12, Collections.emptyList()), hash(10), + StateArchiveFileFormatV3.COMPRESSION_NONE).getFrame(); + byte[] corrupted = Arrays.copyOf(frame, frame.length); + corrupted[StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH] ^= 1; + assertThrows(IllegalArgumentException.class, () -> codec.decode(corrupted)); + assertThrows(IllegalArgumentException.class, + () -> codec.decode(Arrays.copyOf(frame, frame.length - 1))); + } + + private static BlockReverseDiff diff(long blockNumber, List groups) { + return new BlockReverseDiff(BlockSnapshotMeta.forBlock( + blockNumber, hash((int) blockNumber), hash((int) blockNumber - 1), + blockNumber * 3_000L), groups); + } + + private static DbGroup group(BlockReverseDiff diff, String dbName) { + for (DbGroup group : diff.getGroups()) { + if (group.getDbName().equals(dbName)) { + return group; + } + } + throw new AssertionError("Missing group " + dbName); + } + + private static void assertEquivalent(BlockReverseDiff expected, BlockReverseDiff actual) { + assertEquals(expected.getMeta(), actual.getMeta()); + assertEquals(expected.getGroups().size(), actual.getGroups().size()); + for (int groupIndex = 0; groupIndex < expected.getGroups().size(); groupIndex++) { + DbGroup expectedGroup = expected.getGroups().get(groupIndex); + DbGroup actualGroup = actual.getGroups().get(groupIndex); + assertEquals(expectedGroup.getDbName(), actualGroup.getDbName()); + assertEquals(expectedGroup.getEntries().size(), actualGroup.getEntries().size()); + for (int entryIndex = 0; entryIndex < expectedGroup.getEntries().size(); entryIndex++) { + Entry expectedEntry = expectedGroup.getEntries().get(entryIndex); + Entry actualEntry = actualGroup.getEntries().get(entryIndex); + assertArrayEquals(expectedEntry.getKey(), actualEntry.getKey()); + assertEquals(expectedEntry.getOldValue(), actualEntry.getOldValue()); + } + } + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] bytes(int... values) { + byte[] bytes = new byte[values.length]; + for (int index = 0; index < values.length; index++) { + bytes[index] = (byte) values[index]; + } + return bytes; + } + + private static String hex(byte[] bytes) { + StringBuilder result = new StringBuilder(bytes.length * 2); + for (byte value : bytes) { + result.append(String.format("%02x", value & 0xff)); + } + return result.toString(); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java index b95cb31bd74..5a7bed45882 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCheckpointMaterializerTest.java @@ -42,7 +42,7 @@ public void preservesEveryBlockBoundaryBeforePublishingReadableAcrossReopen() CommonCheckpointPayload payload = payload(format, 1, 3, hash(0), hash(10), hash(13)); CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); StateArchiveCheckpointMaterializer materializer = - new StateArchiveCheckpointMaterializer(root, format); + new StateArchiveCheckpointMaterializer(root, format, null, Engine.LEVELDB); assertEquals(Status.NEEDS_MATERIALIZATION, materializer.inspect(target)); materializer.materialize(payload, target); @@ -50,7 +50,7 @@ public void preservesEveryBlockBoundaryBeforePublishingReadableAcrossReopen() assertFalse(Files.exists(root.resolve(StateArchiveCheckpointMaterializer.READABLE_FILE))); assertEquals(3, blockFileCount(root)); assertThrows(IOException.class, - () -> StateArchiveCheckpointReadAdapter.open(root, target)); + () -> StateArchiveCheckpointReadAdapter.open(root, target, Engine.LEVELDB)); for (int index = 0; index < payload.getBlocks().size(); index++) { BlockReverseDiff actual = materializer.loadBlock(target, index); assertEquals(payload.getBlocks().get(index).getMeta(), actual.getMeta()); @@ -62,9 +62,9 @@ public void preservesEveryBlockBoundaryBeforePublishingReadableAcrossReopen() materializer.publish(target); assertEquals(Status.PUBLISHED, materializer.inspect(target)); try (StateArchiveCheckpointReadAdapter reader = - StateArchiveCheckpointReadAdapter.open(root, target); + StateArchiveCheckpointReadAdapter.open(root, target, Engine.LEVELDB); StateArchiveCheckpointReadAdapter concurrent = - StateArchiveCheckpointReadAdapter.open(root, target)) { + StateArchiveCheckpointReadAdapter.open(root, target, Engine.LEVELDB)) { assertEquals(0, reader.getIndexedFrom()); assertEquals(3, reader.getIndexedThrough()); assertArrayEquals(new byte[]{0}, reader.findOldValueAfter("code", new byte[]{1}, 0) @@ -80,7 +80,7 @@ public void preservesEveryBlockBoundaryBeforePublishingReadableAcrossReopen() assertThrows(IOException.class, () -> wrongEngine.inspect(target)); StateArchiveCheckpointMaterializer reopened = - new StateArchiveCheckpointMaterializer(root, format); + new StateArchiveCheckpointMaterializer(root, format, null, Engine.LEVELDB); assertEquals(Status.PUBLISHED, reopened.inspect(target)); reopened.publish(target); @@ -90,7 +90,7 @@ public void preservesEveryBlockBoundaryBeforePublishingReadableAcrossReopen() reopened.publish(childTarget); assertEquals(Status.PUBLISHED, reopened.inspect(childTarget)); try (StateArchiveCheckpointReadAdapter reader = - StateArchiveCheckpointReadAdapter.open(root, childTarget)) { + StateArchiveCheckpointReadAdapter.open(root, childTarget, Engine.LEVELDB)) { assertEquals(0, reader.getIndexedFrom()); assertEquals(5, reader.getIndexedThrough()); assertArrayEquals(hash(5), reader.getHeadHash()); @@ -100,10 +100,10 @@ public void preservesEveryBlockBoundaryBeforePublishingReadableAcrossReopen() .get().getValue()); } CommonCheckpointTarget restored = - StateArchiveCheckpointMaterializer.loadPublishedTarget(root, format); + StateArchiveCheckpointMaterializer.loadPublishedTarget(root, format, Engine.LEVELDB); assertEquals(childTarget, restored); try (StateArchiveCheckpointReadAdapter reader = - StateArchiveCheckpointReadAdapter.open(root, format)) { + StateArchiveCheckpointReadAdapter.open(root, format, Engine.LEVELDB)) { assertEquals(5, reader.getIndexedThrough()); assertArrayEquals(new byte[]{1}, reader.findOldValueAfter("code", new byte[]{2}, 0) .get().getValue()); @@ -197,14 +197,15 @@ public void rejectsForeignFormatCorruptImmutableBlockAndNonParentReadable() CommonCheckpointPayload payload = payload(format, 1, 2, hash(0), hash(30), hash(32)); CommonCheckpointTarget target = CommonCheckpointTarget.from(payload); StateArchiveCheckpointMaterializer materializer = - new StateArchiveCheckpointMaterializer(root, format); + new StateArchiveCheckpointMaterializer(root, format, null, Engine.LEVELDB); CommonCheckpointPayload foreign = payload(hash(99), 1, 1, hash(0), hash(30), hash(31)); assertThrows(IOException.class, () -> materializer.materialize(foreign, CommonCheckpointTarget.from(foreign))); StateArchiveCheckpointMaterializer interrupted = new StateArchiveCheckpointMaterializer(root, - format, failAt(StateArchiveCheckpointMaterializer.Stage.AFTER_BLOCK_FILE)); + format, Engine.LEVELDB, + failAt(StateArchiveCheckpointMaterializer.Stage.AFTER_BLOCK_FILE)); assertThrows(IOException.class, () -> interrupted.materialize(payload, target)); Path block = firstBlockFile(root); byte[] corrupt = Files.readAllBytes(block); @@ -214,7 +215,7 @@ public void rejectsForeignFormatCorruptImmutableBlockAndNonParentReadable() Path cleanRoot = temporaryFolder.newFolder("non-parent").toPath(); StateArchiveCheckpointMaterializer clean = - new StateArchiveCheckpointMaterializer(cleanRoot, format); + new StateArchiveCheckpointMaterializer(cleanRoot, format, null, Engine.LEVELDB); clean.materialize(payload, target); clean.publish(target); CommonCheckpointPayload nonChild = payload(format, 5, 1, hash(9), hash(40), hash(41)); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneBlockCodecV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneBlockCodecV3Test.java new file mode 100644 index 00000000000..f6893dc2e95 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneBlockCodecV3Test.java @@ -0,0 +1,332 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.StateArchiveFiveLaneBlockCodecV3.DecodedBundle; +import org.tron.core.db2.archive.StateArchiveFiveLaneBlockCodecV3.EncodedBundle; +import org.tron.core.db2.archive.StateArchiveFiveLaneBlockCodecV3.EncodedLane; + +public class StateArchiveFiveLaneBlockCodecV3Test { + + private static final String EMPTY_BUNDLE_GOLDEN = + "9a9e46c5b7fea88a262bc7e4e52582b0c0d42bd6e5e454001c1d571b77076c4a" + + ":2c6ef2d937c331ce2d8dbdf9f0cfbb5656addc43bb1720259b3cac873d6166f5" + + ":da6987a93e4fdcd3550e17d5abe60152bce13ca674d18e92e108b83f55550485" + + ":ef3b67f170ae5154c8fbe8b7021695a46492146686e3557bedf0c1763b5fd2b1" + + ":2f50e5993055074c70ee5d32b5f7208782ef9ee2fa5cf1f94b4319ae0fa401dd" + + ":937abd4c413f25d5e9ed4147339b0a9b9b17a22e86171df82d347bf861aa9a7b" + + ":d5c21252a7b74ee1ea037dcf891a81918673d14c953ada93a26a02b2f44bc9c5" + + ":72c3cadb4dad71dc9f230afd2bd306d442325a2705e325e95083fc28c9470dcc"; + + private final StateArchiveFiveLaneBlockCodecV3 codec = + new StateArchiveFiveLaneBlockCodecV3(); + + @Test + public void emitsFiveEmptyFramesWithExactDisjointCoverage() { + EncodedBundle bundle = codec.encode(diff(12, Collections.emptyList()), hash(11), + StateArchiveFileFormatV3.COMPRESSION_NONE); + + assertEquals(5, bundle.getLanes().size()); + long coverage = 0; + int[] laneIds = StateArchiveFileFormatV3.fiveLaneIds(); + for (int index = 0; index < laneIds.length; index++) { + EncodedLane lane = bundle.getLanes().get(index); + int laneId = laneIds[index]; + assertEquals(laneId, lane.getLaneId()); + assertEquals(StateArchiveFileFormatV3.laneBodyCodec(laneId), lane.getBodyCodec()); + assertEquals(32, lane.getCanonicalPayload().length); + assertEquals(0, coverage & lane.getCoverageBitmap()); + coverage |= lane.getCoverageBitmap(); + } + assertEquals(StateArchiveFileFormatV3.EXACT_COVERAGE_BITMAP, coverage); + assertEquals(StateArchiveFileFormatV3.MIXED_LANE_COVERAGE_BITMAP, + bundle.getLanes().get(0).getCoverageBitmap()); + assertFalse(Arrays.equals(StateArchiveFileFormatV3.storeDescriptorDigest(), + StateArchiveFileFormatV3.fiveLaneDescriptorDigest())); + StringBuilder golden = new StringBuilder() + .append(hex(StateArchiveFileFormatV3.fiveLaneDescriptorDigest())); + for (EncodedLane lane : bundle.getLanes()) { + golden.append(':').append(hex(StateArchiveFileFormatV3.sha256(lane.getFrame()))); + } + golden.append(':').append(hex(bundle.getBlockHistoryDigest())) + .append(':').append(hex(bundle.getResultHistoryDigest())); + assertEquals(EMPTY_BUNDLE_GOLDEN, golden.toString()); + + DecodedBundle decoded = codec.decode(frames(bundle)); + assertEquivalent(diff(12, Collections.emptyList()), decoded.getDiff()); + assertArrayEquals(bundle.getBlockHistoryDigest(), decoded.getBlockHistoryDigest()); + assertArrayEquals(bundle.getResultHistoryDigest(), decoded.getResultHistoryDigest()); + } + + @Test + public void roundTripsVariableAndFixedWidthLanes() { + BlockReverseDiff input = diff(42, Arrays.asList( + new DbGroup("abi", Arrays.asList( + new Entry(bytes(0), OldValue.absent()), + new Entry(bytes(0xff), OldValue.present(bytes(1))))), + new DbGroup("account", Arrays.asList( + new Entry(fixedKey(21, 1), OldValue.absent()), + new Entry(fixedKey(21, 2), OldValue.present(new byte[0])), + new Entry(fixedKey(21, 3), OldValue.present(bytes(7, 8))))), + new DbGroup("account-asset", Arrays.asList( + new Entry(fixedKey(22, 1), OldValue.present(bytes(9))), + new Entry(fixedKey(29, 2), OldValue.absent()))), + new DbGroup("delegation", Arrays.asList( + new Entry(bytes(0, 0x80), OldValue.present(bytes(3))), + new Entry(bytes(0xff), OldValue.absent()))), + new DbGroup("storage-row", Arrays.asList( + new Entry(fixedKey(32, 4), OldValue.present(bytes(5))), + new Entry(fixedKey(32, 5), OldValue.absent()))))); + + EncodedBundle bundle = codec.encode(input, hash(41), + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedLane account = lane(bundle, 4); + EncodedLane accountAsset = lane(bundle, 5); + EncodedLane storage = lane(bundle, 22); + assertEquals(StateArchiveFileFormatV3.DEDICATED_FIXED_WIDTH_BODY_CODEC_ID, + account.getBodyCodec()); + assertEquals(StateArchiveFileFormatV3.LANE_VARIABLE_BODY_CODEC_ID, + accountAsset.getBodyCodec()); + assertEquals(StateArchiveFileFormatV3.DEDICATED_FIXED_WIDTH_BODY_CODEC_ID, + storage.getBodyCodec()); + assertFixedSection(account.getCanonicalPayload(), 4, 21, 3); + assertVariableSection(accountAsset.getCanonicalPayload(), 5, 2); + assertFixedSection(storage.getCanonicalPayload(), 22, 32, 2); + + byte[] u0AccountPayload = new StateArchiveBlockFrameCodecV3().encode( + diff(42, Collections.singletonList(group(input, "account"))), hash(41), + StateArchiveFileFormatV3.COMPRESSION_NONE).getCanonicalPayload(); + assertTrue(account.getCanonicalPayload().length < u0AccountPayload.length); + assertEquivalent(input, codec.decode(frames(bundle)).getDiff()); + } + + @Test + public void roundTripsEveryExact27StoreThroughItsAssignedLane() { + ArchiveParticipantDescriptor descriptor = ArchiveParticipantDescriptor.current(); + List groups = new ArrayList<>(); + for (String dbName : descriptor.getActiveDatabases()) { + int storeId = descriptor.getStoreId(dbName); + int laneId = StateArchiveFileFormatV3.laneId(storeId); + int keyWidth = StateArchiveFileFormatV3.fixedKeyWidth(laneId); + byte[] key = keyWidth == 0 + ? bytes(storeId, 0xff - storeId) + : fixedKey(keyWidth, storeId); + OldValue oldValue = (storeId & 1) == 0 + ? OldValue.absent() + : OldValue.present(bytes(storeId)); + groups.add(new DbGroup(dbName, + Collections.singletonList(new Entry(key, oldValue)))); + } + BlockReverseDiff input = diff(64, groups); + + EncodedBundle bundle = codec.encode(input, hash(63), + StateArchiveFileFormatV3.COMPRESSION_RAW_DEFLATE_LEVEL_1); + + assertEquals(5, bundle.getLanes().size()); + assertEquivalent(input, codec.decode(frames(bundle)).getDiff()); + } + + @Test + public void compressionKeepsBundleIdentityAndCanonicalLanePayloads() { + BlockReverseDiff input = diff(77, Arrays.asList( + new DbGroup("account", Collections.singletonList( + new Entry(fixedKey(21, 1), OldValue.present(new byte[4096])))), + new DbGroup("votes", Collections.singletonList( + new Entry(bytes(0x80), OldValue.present(new byte[2048])))))); + EncodedBundle none = codec.encode(input, hash(76), + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle compressed = codec.encode(input, hash(76), + StateArchiveFileFormatV3.COMPRESSION_RAW_DEFLATE_LEVEL_1); + + assertArrayEquals(none.getBlockHistoryDigest(), compressed.getBlockHistoryDigest()); + assertArrayEquals(none.getResultHistoryDigest(), compressed.getResultHistoryDigest()); + boolean physicalDifference = false; + for (int index = 0; index < none.getLanes().size(); index++) { + assertArrayEquals(none.getLanes().get(index).getCanonicalPayload(), + compressed.getLanes().get(index).getCanonicalPayload()); + physicalDifference |= !Arrays.equals(none.getLanes().get(index).getFrame(), + compressed.getLanes().get(index).getFrame()); + } + assertTrue(physicalDifference); + assertEquivalent(input, codec.decode(frames(compressed)).getDiff()); + } + + @Test + public void rejectsInvalidFixedKeysIncompleteAndMixedBundles() { + BlockReverseDiff invalidAccount = diff(12, Collections.singletonList( + new DbGroup("account", Collections.singletonList( + new Entry(fixedKey(20, 1), OldValue.absent()))))); + assertThrows(IllegalArgumentException.class, () -> codec.encode( + invalidAccount, hash(11), StateArchiveFileFormatV3.COMPRESSION_NONE)); + BlockReverseDiff invalidStorage = diff(12, Collections.singletonList( + new DbGroup("storage-row", Collections.singletonList( + new Entry(fixedKey(33, 1), OldValue.absent()))))); + assertThrows(IllegalArgumentException.class, () -> codec.encode( + invalidStorage, hash(11), StateArchiveFileFormatV3.COMPRESSION_NONE)); + + EncodedBundle first = codec.encode(diff(12, Collections.emptyList()), hash(11), + StateArchiveFileFormatV3.COMPRESSION_NONE); + List missing = frames(first); + missing.remove(0); + assertThrows(IllegalArgumentException.class, () -> codec.decode(missing)); + + List duplicate = frames(first); + duplicate.set(4, duplicate.get(0)); + assertThrows(IllegalArgumentException.class, () -> codec.decode(duplicate)); + + EncodedBundle second = codec.encode(diff(13, Collections.emptyList()), hash(12), + StateArchiveFileFormatV3.COMPRESSION_NONE); + List mixed = frames(first); + mixed.set(2, second.getLanes().get(2).getFrame()); + assertThrows(IllegalArgumentException.class, () -> codec.decode(mixed)); + + List corrupted = frames(first); + byte[] bad = corrupted.get(1); + bad[StateArchiveFileFormatV3.BLOCK_HEADER_LENGTH] ^= 1; + assertThrows(IllegalArgumentException.class, () -> codec.decode(corrupted)); + } + + @Test + public void bundleEncodingIsDeterministicAndMatchesLegacySemanticOracle() { + List entries = Arrays.asList( + new Entry(bytes(0xff), OldValue.absent()), + new Entry(bytes(0), OldValue.present(new byte[0])), + new Entry(bytes(0x80), OldValue.present(bytes(3, 4)))); + BlockReverseDiff input = diff(88, Arrays.asList( + new DbGroup("votes", entries), + new DbGroup("abi", Collections.singletonList( + new Entry(bytes(7), OldValue.present(bytes(1))))))); + List reversedEntries = new ArrayList<>(entries); + Collections.reverse(reversedEntries); + BlockReverseDiff reordered = diff(88, Arrays.asList( + new DbGroup("abi", Collections.singletonList( + new Entry(bytes(7), OldValue.present(bytes(1))))), + new DbGroup("votes", reversedEntries))); + + EncodedBundle first = codec.encode(input, hash(87), + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle second = codec.encode(reordered, hash(87), + StateArchiveFileFormatV3.COMPRESSION_NONE); + for (int index = 0; index < first.getLanes().size(); index++) { + assertArrayEquals(first.getLanes().get(index).getFrame(), + second.getLanes().get(index).getFrame()); + } + BlockReverseDiff legacy = new BlockHistoryCodec().decode( + new BlockHistoryCodec().encode(input)); + assertEquivalent(legacy, codec.decode(frames(first)).getDiff()); + assertNotEquals(hex(StateArchiveFileFormatV3.formatDigest()), + hex(StateArchiveFileFormatV3.fiveLaneDescriptorDigest())); + } + + private static void assertFixedSection(byte[] payloadBytes, int storeId, + int keyWidth, int entryCount) { + ByteBuffer payload = ByteBuffer.wrap(payloadBytes); + payload.position(StateArchiveFileFormatV3.PAYLOAD_HEADER_LENGTH); + assertEquals(storeId, Short.toUnsignedInt(payload.getShort())); + assertEquals(2, Short.toUnsignedInt(payload.getShort())); + assertEquals(96, payload.getInt()); + assertEquals(entryCount, payload.getInt()); + assertEquals(0, payload.getInt()); + assertEquals(keyWidth, payload.getInt()); + } + + private static void assertVariableSection(byte[] payloadBytes, int storeId, + int entryCount) { + ByteBuffer payload = ByteBuffer.wrap(payloadBytes); + payload.position(StateArchiveFileFormatV3.PAYLOAD_HEADER_LENGTH); + assertEquals(storeId, Short.toUnsignedInt(payload.getShort())); + assertEquals(1, Short.toUnsignedInt(payload.getShort())); + assertEquals(StateArchiveFileFormatV3.SECTION_HEADER_LENGTH, payload.getInt()); + assertEquals(entryCount, payload.getInt()); + } + + private static EncodedLane lane(EncodedBundle bundle, int laneId) { + for (EncodedLane lane : bundle.getLanes()) { + if (lane.getLaneId() == laneId) { + return lane; + } + } + throw new AssertionError("Missing lane " + laneId); + } + + private static List frames(EncodedBundle bundle) { + List frames = new ArrayList<>(); + for (EncodedLane lane : bundle.getLanes()) { + frames.add(lane.getFrame()); + } + return frames; + } + + private static BlockReverseDiff diff(long blockNumber, List groups) { + return new BlockReverseDiff(BlockSnapshotMeta.forBlock( + blockNumber, hash((int) blockNumber), hash((int) blockNumber - 1), + blockNumber * 3_000L), groups); + } + + private static DbGroup group(BlockReverseDiff diff, String dbName) { + for (DbGroup group : diff.getGroups()) { + if (group.getDbName().equals(dbName)) { + return group; + } + } + throw new AssertionError("Missing group " + dbName); + } + + private static void assertEquivalent(BlockReverseDiff expected, BlockReverseDiff actual) { + assertEquals(expected.getMeta(), actual.getMeta()); + assertEquals(expected.getGroups().size(), actual.getGroups().size()); + for (int groupIndex = 0; groupIndex < expected.getGroups().size(); groupIndex++) { + DbGroup expectedGroup = expected.getGroups().get(groupIndex); + DbGroup actualGroup = actual.getGroups().get(groupIndex); + assertEquals(expectedGroup.getDbName(), actualGroup.getDbName()); + assertEquals(expectedGroup.getEntries().size(), actualGroup.getEntries().size()); + for (int entryIndex = 0; entryIndex < expectedGroup.getEntries().size(); entryIndex++) { + Entry expectedEntry = expectedGroup.getEntries().get(entryIndex); + Entry actualEntry = actualGroup.getEntries().get(entryIndex); + assertArrayEquals(expectedEntry.getKey(), actualEntry.getKey()); + assertEquals(expectedEntry.getOldValue(), actualEntry.getOldValue()); + } + } + } + + private static byte[] hash(int suffix) { + byte[] hash = new byte[32]; + hash[31] = (byte) suffix; + return hash; + } + + private static byte[] fixedKey(int length, int suffix) { + byte[] key = new byte[length]; + key[length - 1] = (byte) suffix; + return key; + } + + private static byte[] bytes(int... values) { + byte[] bytes = new byte[values.length]; + for (int index = 0; index < values.length; index++) { + bytes[index] = (byte) values[index]; + } + return bytes; + } + + private static String hex(byte[] bytes) { + StringBuilder result = new StringBuilder(bytes.length * 2); + for (byte value : bytes) { + result.append(String.format("%02x", value & 0xff)); + } + return result.toString(); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProcessTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProcessTest.java new file mode 100644 index 00000000000..ef031e217e0 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProcessTest.java @@ -0,0 +1,123 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.StateArchiveFiveLaneBlockCodecV3.EncodedBundle; +import org.tron.core.db2.archive.StateArchiveFiveLaneRecoveryIntentV3.RecoveryPoint; +import org.tron.core.db2.archive.StateArchiveFiveLaneSegmentWriterV3.ArchiveDurabilityProof; + +/** Process boundary for marker force: the child never closes the writer normally. */ +public class StateArchiveFiveLaneDurabilityProcessTest { + + private static final int HALT_CODE = 92; + private static final byte[] BASELINE = hash(20); + private static final byte[] COMMON_TARGET = hash(90); + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void jvmHaltAfterFiveLaneForceLeavesRestartVerifiableProof() throws Exception { + Path root = temporaryFolder.newFolder("five-lane-durability-process").toPath(); + Process child = new ProcessBuilder(javaExecutable(), "-cp", runtimeClasspath(), + StateArchiveFiveLaneDurabilityProcessTest.class.getName(), "halt-after-sync", + root.toString()).redirectErrorStream(true) + .redirectOutput(root.resolve("halt-after-sync.log").toFile()).start(); + assertTrue("child process timed out", child.waitFor(30, TimeUnit.SECONDS)); + assertEquals(HALT_CODE, child.exitValue()); + + try (StateArchiveFiveLaneSegmentWriterV3 reopened = + new StateArchiveFiveLaneSegmentWriterV3(root, BASELINE, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + ArchiveDurabilityProof proof = reopened.getLastDurabilityProof(); + assertNotNull(proof); + assertEquals(1, proof.getCheckpointSequence()); + assertEquals(2, proof.getTarget().getBlockNumber()); + assertEquals(5, proof.getFileTails().size()); + reopened.verifyDurabilityProof(proof); + } + } + + public static void main(String[] args) throws Exception { + if (args.length != 2 || !"halt-after-sync".equals(args[0])) { + throw new IllegalArgumentException("unknown five-lane durability process mode"); + } + Path root = Paths.get(args[1]); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + EncodedBundle first = codec.encode(diff(1), BASELINE, + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle second = codec.encode(diff(2), first.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, BASELINE, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000); + writer.append(first); + writer.append(second); + writer.sync(1, point(second), COMMON_TARGET); + Runtime.getRuntime().halt(HALT_CODE); + } + + private static BlockReverseDiff diff(int blockNumber) { + byte[] value = new byte[10]; + java.util.Arrays.fill(value, (byte) blockNumber); + DbGroup group = new DbGroup(StateArchiveFileFormatV3.dbName(1), + Collections.singletonList(new Entry(new byte[]{1}, OldValue.present(value)))); + return new BlockReverseDiff(new BlockSnapshotMeta(blockNumber, blockNumber, + hash(blockNumber), hash(blockNumber - 1), blockNumber * 3_000L), + Collections.singletonList(group)); + } + + private static RecoveryPoint point(EncodedBundle bundle) { + BlockSnapshotMeta meta = bundle.getDiff().getMeta(); + return new RecoveryPoint(meta.getEpoch(), meta.getBlockNumber(), meta.getTimestamp(), + meta.getBlockHash(), meta.getParentHash(), bundle.getResultHistoryDigest()); + } + + private static String javaExecutable() { + return Paths.get(System.getProperty("java.home"), "bin", "java").toString(); + } + + private static String runtimeClasspath() { + Set entries = new LinkedHashSet<>(); + String configured = System.getProperty("java.class.path", ""); + Collections.addAll(entries, configured.split(java.util.regex.Pattern.quote( + File.pathSeparator))); + for (ClassLoader loader = StateArchiveFiveLaneDurabilityProcessTest.class.getClassLoader(); + loader != null; loader = loader.getParent()) { + if (loader instanceof URLClassLoader) { + for (URL url : ((URLClassLoader) loader).getURLs()) { + if ("file".equals(url.getProtocol())) { + try { + entries.add(Paths.get(url.toURI()).toString()); + } catch (java.net.URISyntaxException invalid) { + throw new IllegalStateException("invalid test runtime classpath", invalid); + } + } + } + } + } + return String.join(File.pathSeparator, entries); + } + + private static byte[] hash(int suffix) { + byte[] result = new byte[32]; + result[31] = (byte) suffix; + return result; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProofV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProofV3Test.java new file mode 100644 index 00000000000..53faa67788c --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneDurabilityProofV3Test.java @@ -0,0 +1,173 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.StateArchiveFiveLaneBlockCodecV3.EncodedBundle; +import org.tron.core.db2.archive.StateArchiveFiveLaneRecoveryIntentV3.RecoveryPoint; +import org.tron.core.db2.archive.StateArchiveFiveLaneSegmentWriterV3.ArchiveDurabilityProof; +import org.tron.core.db2.archive.StateArchiveFiveLaneSegmentWriterV3.FileTailProof; + +public class StateArchiveFiveLaneDurabilityProofV3Test { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void freezesRoundTripAndReloadsExactMarkerProof() throws Exception { + Path root = temporaryFolder.newFolder("proof").toPath(); + byte[] baseline = hash(40); + byte[] commonDigest = hash(99); + ArchiveDurabilityProof proof; + try (StateArchiveFiveLaneSegmentWriterV3 writer = writer(root, baseline)) { + EncodedBundle bundle = bundle(baseline); + writer.append(bundle); + proof = writer.sync(7, point(bundle), commonDigest); + StateArchiveFiveLaneDurabilityProofV3.publish(root, proof); + } + + byte[] encoded = Files.readAllBytes( + root.resolve(StateArchiveFiveLaneDurabilityProofV3.FILE_NAME)); + assertEquals(760, encoded.length); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + assertEquals(StateArchiveFiveLaneDurabilityProofV3.MAGIC, bytes.getInt(0)); + assertEquals(352, bytes.getInt(8)); + assertEquals(72, Short.toUnsignedInt(bytes.getShort(12))); + assertEquals(5, Short.toUnsignedInt(bytes.getShort(14))); + assertEquals(760, bytes.getLong(16)); + assertArrayEquals(StateArchiveFileFormatV3.compositeFormatDigest(), + slice(encoded, 32, 32)); + assertArrayEquals(StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + slice(encoded, 64, 32)); + assertEquals(7, bytes.getLong(96)); + assertEquals(1, bytes.getLong(112)); + assertEquals(0, Short.toUnsignedInt(bytes.getShort(352))); + assertEquals(22, Short.toUnsignedInt(bytes.getShort(352 + 4 * 72))); + + ArchiveDurabilityProof decoded = StateArchiveFiveLaneDurabilityProofV3.decode(encoded); + assertEquals(7, decoded.getCheckpointSequence()); + assertEquals(1, decoded.getTarget().getBlockNumber()); + assertArrayEquals(commonDigest, decoded.getCommonTargetDigest()); + assertEquals(5, decoded.getFileTails().size()); + + try (StateArchiveFiveLaneSegmentWriterV3 reopened = writer(root, baseline)) { + ArchiveDurabilityProof verified = StateArchiveFiveLaneDurabilityProofV3.loadAndVerify( + root, reopened, proof.getTarget(), commonDigest); + assertEquals(1, verified.getTarget().getBlockNumber()); + assertArrayEquals(proof.getFileTails().get(4).getMarkerDigest(), + verified.getFileTails().get(4).getMarkerDigest()); + assertThrows(IllegalArgumentException.class, + () -> StateArchiveFiveLaneDurabilityProofV3.loadAndVerify(root, reopened, + proof.getTarget(), hash(98))); + } + } + + @Test + public void rejectsHeaderTailTrailerAndMarkerDrift() throws Exception { + Path root = temporaryFolder.newFolder("proof-fault").toPath(); + byte[] baseline = hash(50); + ArchiveDurabilityProof proof; + try (StateArchiveFiveLaneSegmentWriterV3 writer = writer(root, baseline)) { + EncodedBundle bundle = bundle(baseline); + writer.append(bundle); + proof = writer.sync(8, point(bundle), hash(100)); + StateArchiveFiveLaneDurabilityProofV3.publish(root, proof); + } + byte[] encoded = Files.readAllBytes( + root.resolve(StateArchiveFiveLaneDurabilityProofV3.FILE_NAME)); + for (int offset : new int[]{50, 400, 759}) { + byte[] corrupt = encoded.clone(); + corrupt[offset] ^= 1; + assertThrows(IllegalArgumentException.class, + () -> StateArchiveFiveLaneDurabilityProofV3.decode(corrupt)); + } + + FileTailProof last = proof.getFileTails().get(4); + try (StateArchiveFiveLaneSegmentWriterV3 reopened = writer(root, baseline)) { + Path marker = root.resolve("segments").resolve("shard-000000") + .resolve(String.format("lane-%04d-seg-%020d.dat", last.getLaneId(), + last.getSegmentSeq())); + try (FileChannel channel = FileChannel.open(marker, StandardOpenOption.READ, + StandardOpenOption.WRITE)) { + ByteBuffer one = ByteBuffer.allocate(1); + channel.position(last.getMarkerOffset() + 20); + channel.read(one); + one.flip(); + one.put(0, (byte) (one.get(0) ^ 1)); + channel.position(last.getMarkerOffset() + 20); + channel.write(one); + channel.force(false); + } + assertThrows(IllegalArgumentException.class, + () -> StateArchiveFiveLaneDurabilityProofV3.loadAndVerify(root, reopened)); + } + } + + @Test + public void rejectsMissingDuplicateAndOutOfOrderLaneTails() throws Exception { + Path root = temporaryFolder.newFolder("proof-order").toPath(); + byte[] baseline = hash(60); + ArchiveDurabilityProof proof; + try (StateArchiveFiveLaneSegmentWriterV3 writer = writer(root, baseline)) { + EncodedBundle bundle = bundle(baseline); + writer.append(bundle); + proof = writer.sync(9, point(bundle), hash(101)); + } + assertThrows(IllegalArgumentException.class, () -> new ArchiveDurabilityProof(9, + proof.getTarget(), proof.getCommonTargetDigest(), + proof.getFileTails().subList(0, 4))); + java.util.List duplicate = new java.util.ArrayList<>(proof.getFileTails()); + duplicate.add(1, duplicate.get(0)); + assertThrows(IllegalArgumentException.class, () -> new ArchiveDurabilityProof(9, + proof.getTarget(), proof.getCommonTargetDigest(), duplicate)); + java.util.List reversed = new java.util.ArrayList<>(proof.getFileTails()); + Collections.swap(reversed, 0, 1); + assertThrows(IllegalArgumentException.class, () -> new ArchiveDurabilityProof(9, + proof.getTarget(), proof.getCommonTargetDigest(), reversed)); + assertTrue(proof.getFileTails().stream().allMatch(tail -> tail.getMarkerLength() == 336)); + } + + private static StateArchiveFiveLaneSegmentWriterV3 writer(Path root, byte[] baseline) + throws Exception { + return new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000); + } + + private static EncodedBundle bundle(byte[] baseline) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(1, 1, hash(1), hash(0), 3_000); + DbGroup group = new DbGroup(StateArchiveFileFormatV3.dbName(1), + Collections.singletonList(new Entry(new byte[]{1}, OldValue.present(new byte[]{7})))); + return new StateArchiveFiveLaneBlockCodecV3().encode( + new BlockReverseDiff(meta, Collections.singletonList(group)), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + } + + private static RecoveryPoint point(EncodedBundle bundle) { + BlockSnapshotMeta meta = bundle.getDiff().getMeta(); + return new RecoveryPoint(meta.getEpoch(), meta.getBlockNumber(), meta.getTimestamp(), + meta.getBlockHash(), meta.getParentHash(), bundle.getResultHistoryDigest()); + } + + private static byte[] hash(int suffix) { + byte[] result = new byte[32]; + result[31] = (byte) suffix; + return result; + } + + private static byte[] slice(byte[] bytes, int offset, int length) { + return java.util.Arrays.copyOfRange(bytes, offset, offset + length); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneRecoveryIntentV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneRecoveryIntentV3Test.java new file mode 100644 index 00000000000..678f1fcca03 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneRecoveryIntentV3Test.java @@ -0,0 +1,151 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.tron.core.db2.archive.StateArchiveFiveLaneRecoveryIntentV3.Intent; +import org.tron.core.db2.archive.StateArchiveFiveLaneRecoveryIntentV3.LaneTarget; +import org.tron.core.db2.archive.StateArchiveFiveLaneRecoveryIntentV3.RecoveryPoint; + +public class StateArchiveFiveLaneRecoveryIntentV3Test { + + @Test + public void freezesLayoutAndRoundTripsCompleteFiveLanePlan() { + Intent intent = intent(); + byte[] encoded = StateArchiveFiveLaneRecoveryIntentV3.encode(intent); + assertEquals(1_616, encoded.length); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + assertEquals(StateArchiveFileFormatV3.RECOVERY_INTENT_MAGIC, bytes.getInt(0)); + assertEquals(768, bytes.getInt(8)); + assertEquals(160, Short.toUnsignedInt(bytes.getShort(12))); + assertEquals(5, Short.toUnsignedInt(bytes.getShort(14))); + assertEquals(1_616, bytes.getLong(16)); + assertEquals(3, Short.toUnsignedInt(bytes.getShort(26))); + assertArrayEquals(StateArchiveFileFormatV3.compositeFormatDigest(), + slice(encoded, 32, 32)); + assertArrayEquals(StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + slice(encoded, 64, 32)); + assertEquals(12, bytes.getLong(136)); + assertEquals(10, bytes.getLong(256)); + assertEquals(11, bytes.getLong(376)); + assertEquals(0, Short.toUnsignedInt(bytes.getShort(768))); + assertEquals(22, Short.toUnsignedInt(bytes.getShort(768 + 4 * 160))); + + Intent decoded = StateArchiveFiveLaneRecoveryIntentV3.decode(encoded); + assertEquals(12, decoded.getAuthorizedCeiling().getBlockNumber()); + assertEquals(10, decoded.getCommonCommitted().getBlockNumber()); + assertEquals(11, decoded.getTarget().getBlockNumber()); + assertEquals(5, decoded.getLanes().size()); + assertEquals(StateArchiveFiveLaneRecoveryIntentV3.NO_TARGET_SEGMENT, + decoded.getLanes().get(4).getTargetSegmentSeq()); + assertArrayEquals(slice(encoded, 732, 32), decoded.getHeaderDigest()); + assertArrayEquals(slice(encoded, 488, 32), decoded.getRecordsDigest()); + assertArrayEquals(slice(encoded, 1_568, 32), decoded.getIntentDigest()); + + assertEquals( + "524944330003000000000020030000a000050030000006500001000100010001" + + ":e19713bb16c631eafb76b4fdd82101440de0e842e6c70e369f289934402e364b" + + ":c5464d3c1c3b2ee2d5622dabd83169db2140ac05d7fe995866bd294524b8f6a4" + + ":6399bb2ce2bdf435c2fd5b080c2eaade83e4fdd1ba00970495da96b41e186463", + Hex.toHexString(StateArchiveFileFormatV3.recoveryIntentLayoutDescriptor()) + ":" + + Hex.toHexString(StateArchiveFileFormatV3.recoveryIntentLayoutDigest()) + ":" + + Hex.toHexString(StateArchiveFileFormatV3.compositeFormatDigest()) + ":" + + Hex.toHexString(StateArchiveFileFormatV3.sha256(encoded))); + } + + @Test + public void rejectsHeaderRecordTrailerAndPointDrift() { + byte[] encoded = StateArchiveFiveLaneRecoveryIntentV3.encode(intent()); + for (int offset : new int[]{600, 800, 1_615}) { + byte[] corrupt = encoded.clone(); + corrupt[offset] ^= 1; + assertThrows(IllegalArgumentException.class, + () -> StateArchiveFiveLaneRecoveryIntentV3.decode(corrupt)); + } + + List wrongOrder = lanes(); + java.util.Collections.swap(wrongOrder, 0, 1); + assertThrows(IllegalArgumentException.class, () -> new Intent(hash(90), point(12), + point(10), point(11), wrongOrder)); + assertThrows(IllegalArgumentException.class, () -> new Intent(hash(90), point(12), + point(11), point(10), lanes())); + assertThrows(IllegalArgumentException.class, () -> new Intent(hash(90), point(11), + point(11), new RecoveryPoint(11, 11, 33_000, hash(99), hash(10), hash(111)), + lanes())); + assertThrows(IllegalArgumentException.class, () -> new Intent(hash(90), point(12), + point(10), null, lanes())); + } + + @Test + public void representsBaselineTargetAndLanesNotYetCreated() { + byte[] zero = new byte[32]; + List baselineLanes = Arrays.asList( + new LaneTarget(0, StateArchiveFiveLaneRecoveryIntentV3.DELETE_PAIR, + 0, 700, 128, StateArchiveFiveLaneRecoveryIntentV3.NO_TARGET_SEGMENT, + 0, 0, hash(1), zero, zero), + missingLane(4), missingLane(5), missingLane(13), missingLane(22)); + byte[] encoded = StateArchiveFiveLaneRecoveryIntentV3.encode( + new Intent(hash(90), point(12), null, null, baselineLanes)); + + assertEquals(0, Short.toUnsignedInt(ByteBuffer.wrap(encoded).getShort(26))); + assertArrayEquals(new byte[240], slice(encoded, 248, 240)); + Intent decoded = StateArchiveFiveLaneRecoveryIntentV3.decode(encoded); + assertNull(decoded.getCommonCommitted()); + assertNull(decoded.getTarget()); + assertEquals(StateArchiveFiveLaneRecoveryIntentV3.SOURCE_PAIR_MISSING, + decoded.getLanes().get(1).getActionFlags()); + } + + private static Intent intent() { + return new Intent(hash(90), point(12), point(10), point(11), lanes()); + } + + private static RecoveryPoint point(int block) { + return new RecoveryPoint(block, block, block * 3_000L, + hash(block), hash(block - 1), hash(100 + block)); + } + + private static List lanes() { + byte[] zero = new byte[32]; + return new java.util.ArrayList<>(Arrays.asList( + new LaneTarget(0, + StateArchiveFiveLaneRecoveryIntentV3.DATA_TRUNCATE + | StateArchiveFiveLaneRecoveryIntentV3.INDEX_REPLACE, + 2, 1_000, 192, 2, 900, 160, hash(1), hash(2), hash(3)), + new LaneTarget(4, StateArchiveFiveLaneRecoveryIntentV3.INDEX_REPLACE, + 1, 800, 160, 1, 800, 160, hash(4), hash(5), hash(6)), + new LaneTarget(5, 0, + 1, 800, 160, 1, 800, 160, hash(7), hash(8), hash(9)), + new LaneTarget(13, + StateArchiveFiveLaneRecoveryIntentV3.INDEX_REPLACE + | StateArchiveFiveLaneRecoveryIntentV3.ORIGINAL_INDEX_MISSING, + 1, 800, 0, 1, 800, 160, hash(10), hash(11), hash(12)), + new LaneTarget(22, StateArchiveFiveLaneRecoveryIntentV3.DELETE_PAIR, + 3, 700, 128, StateArchiveFiveLaneRecoveryIntentV3.NO_TARGET_SEGMENT, + 0, 0, hash(13), zero, zero))); + } + + private static LaneTarget missingLane(int laneId) { + byte[] zero = new byte[32]; + return new LaneTarget(laneId, StateArchiveFiveLaneRecoveryIntentV3.SOURCE_PAIR_MISSING, + StateArchiveFiveLaneRecoveryIntentV3.NO_TARGET_SEGMENT, 0, 0, + StateArchiveFiveLaneRecoveryIntentV3.NO_TARGET_SEGMENT, 0, 0, zero, zero, zero); + } + + private static byte[] hash(int suffix) { + byte[] result = new byte[32]; + result[31] = (byte) suffix; + return result; + } + + private static byte[] slice(byte[] bytes, int offset, int length) { + return Arrays.copyOfRange(bytes, offset, offset + length); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java new file mode 100644 index 00000000000..37beb9d75bc --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java @@ -0,0 +1,509 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.StateArchiveFiveLaneBlockCodecV3.EncodedBundle; +import org.tron.core.db2.archive.StateArchiveFiveLaneRecoveryIntentV3.RecoveryPoint; +import org.tron.core.db2.archive.StateArchiveFiveLaneSegmentWriterV3.ArchiveDurabilityProof; +import org.tron.core.db2.archive.StateArchiveFiveLaneSegmentWriterV3.RecoveryStage; +import org.tron.core.db2.archive.StateArchiveFiveLaneSegmentWriterV3.SyncStage; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.CurrentSegment; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SealedSegment; + +public class StateArchiveFiveLaneSegmentWriterV3Test { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void independentlyRotatesOvershotLaneAndReopensCompleteBundle() throws Exception { + Path root = temporaryFolder.newFolder("five-lane-segments").toPath(); + byte[] baseline = hash(90); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + EncodedBundle first = codec.encode(diff(1, 1_400), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle second = codec.encode(diff(2, 0), first.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)) { + writer.append(first); + writer.append(second); + assertEquals(2, writer.getAppendHead().getBlockNumber()); + assertEquals(1, writer.getSealedSegments().size()); + SealedSegment sealed = writer.getSealedSegments().get(0); + assertEquals(0, sealed.getLaneId()); + assertEquals(0, sealed.getSegmentSeq()); + assertEquals(1, sealed.getFirstBlock()); + assertEquals(1, sealed.getLastBlock()); + + List current = writer.getCurrentSegments(); + assertEquals(5, current.size()); + assertEquals(1, current.stream().filter(segment -> segment.getSegmentSeq() == 1) + .count()); + CurrentSegment mixed = current.stream().filter(segment -> segment.getLaneId() == 0) + .findFirst().get(); + assertEquals(2, mixed.getFirstBlock()); + assertEquals(2, mixed.getCurrentLastBlock()); + assertTrue(current.stream().filter(segment -> segment.getLaneId() != 0) + .allMatch(segment -> segment.getFirstBlock() == 1 + && segment.getCurrentLastBlock() == 2 + && segment.getSegmentSeq() == 0)); + } + + assertEquals(6, filesWithSuffix(root, ".dat").size()); + assertEquals(6, filesWithSuffix(root, ".bidx").size()); + + try (StateArchiveFiveLaneSegmentWriterV3 reopened = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)) { + assertEquals(2, reopened.getAppendHead().getBlockNumber()); + assertEquals(1, reopened.getSealedSegments().size()); + EncodedBundle third = codec.encode(diff(3, 0), reopened.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + reopened.append(third); + assertEquals(3, reopened.getAppendHead().getBlockNumber()); + } + } + + @Test + public void provesSealedAndCurrentTailsAcrossIndependentRotation() throws Exception { + Path root = temporaryFolder.newFolder("five-lane-rotation-proof").toPath(); + byte[] baseline = hash(89); + byte[] commonTarget = hash(109); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + EncodedBundle first = codec.encode(diff(1, 1_400), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle second = codec.encode(diff(2, 0), first.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + ArchiveDurabilityProof proof; + + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)) { + writer.appendForCheckpoint(first, 11, commonTarget); + writer.appendForCheckpoint(second, 11, commonTarget); + proof = writer.sync(11, point(second), commonTarget); + assertEquals(6, proof.getFileTails().size()); + assertEquals(2, proof.getFileTails().stream() + .filter(tail -> tail.getLaneId() == 0).count()); + assertEquals(0, proof.getFileTails().get(0).getSegmentSeq()); + assertEquals(1, proof.getFileTails().get(1).getSegmentSeq()); + writer.verifyDurabilityProof(proof); + StateArchiveFiveLaneDurabilityProofV3.publish(root, proof); + } + + assertEquals(832, Files.size( + root.resolve(StateArchiveFiveLaneDurabilityProofV3.FILE_NAME))); + try (StateArchiveFiveLaneSegmentWriterV3 reopened = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)) { + ArchiveDurabilityProof verified = StateArchiveFiveLaneDurabilityProofV3.loadAndVerify( + root, reopened, point(second), commonTarget); + assertEquals(6, verified.getFileTails().size()); + } + } + + @Test + public void failsClosedAtEveryRotationMarkerPhase() throws Exception { + for (SyncStage stage : new SyncStage[]{SyncStage.MARKER_WRITTEN, + SyncStage.DATA_FORCED, SyncStage.MARKER_VERIFIED}) { + Path root = temporaryFolder.newFolder("five-lane-rotation-fault-" + stage).toPath(); + byte[] baseline = hash(88); + byte[] commonTarget = hash(108); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + EncodedBundle first = codec.encode(diff(1, 1_400), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle second = codec.encode(diff(2, 0), first.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)) { + writer.appendForCheckpoint(first, 12, commonTarget); + assertThrows(java.io.IOException.class, + () -> writer.appendForCheckpoint(second, 12, commonTarget, + (actual, laneId) -> { + if (actual == stage && laneId == 0) { + throw new java.io.IOException("injected rotation fault at " + stage); + } + })); + assertThrows(IllegalStateException.class, () -> writer.sync( + 12, point(first), commonTarget)); + } + } + } + + @Test + public void repairsPartialDataAndIndexAtFiveLaneCommonBoundary() throws Exception { + Path root = temporaryFolder.newFolder("five-lane-recovery").toPath(); + byte[] baseline = hash(80); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + EncodedBundle first = codec.encode(diff(1, 10), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle second = codec.encode(diff(2, 10), first.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle third = codec.encode(diff(3, 10), second.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + writer.append(first); + writer.append(second); + } + + Path lane22Data = segment(root, 22, ".dat"); + try (FileChannel channel = FileChannel.open(lane22Data, StandardOpenOption.WRITE)) { + channel.truncate(channel.size() - 10); + channel.force(false); + } + Path lane5Index = segment(root, 5, ".bidx"); + try (FileChannel channel = FileChannel.open(lane5Index, StandardOpenOption.WRITE)) { + channel.truncate(channel.size() - 7); + channel.force(false); + } + Files.delete(segment(root, 13, ".bidx")); + byte[] extraMixedFrame = third.getLanes().stream() + .filter(lane -> lane.getLaneId() == 0).findFirst().get().getFrame(); + try (FileChannel channel = FileChannel.open(segment(root, 0, ".dat"), + StandardOpenOption.WRITE)) { + channel.position(channel.size()); + ByteBuffer bytes = ByteBuffer.wrap(extraMixedFrame); + while (bytes.hasRemaining()) { + channel.write(bytes); + } + channel.force(false); + } + + try (StateArchiveFiveLaneSegmentWriterV3 recovered = + StateArchiveFiveLaneSegmentWriterV3.recover(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000, 2)) { + assertEquals(1, recovered.getAppendHead().getBlockNumber()); + assertEquals(5, recovered.getCurrentSegments().size()); + assertTrue(recovered.getCurrentSegments().stream() + .allMatch(segment -> segment.getCurrentLastBlock() == 1 + && segment.getBlockFrameCount() == 1)); + assertTrue(Files.isRegularFile(segment(root, 13, ".bidx"))); + } + + Map repairedSizes = fileSizes(root); + try (StateArchiveFiveLaneSegmentWriterV3 reopened = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + assertEquals(1, reopened.getAppendHead().getBlockNumber()); + } + assertEquals(repairedSizes, fileSizes(root)); + } + + @Test + public void resumesDurableRecoveryIntentAtEveryMutationBoundary() throws Exception { + for (RecoveryStage stage : RecoveryStage.values()) { + Path root = temporaryFolder.newFolder("five-lane-intent-" + stage).toPath(); + byte[] baseline = hash(70); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + EncodedBundle first = codec.encode(diff(1, 10), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle second = codec.encode(diff(2, 10), first.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + prepareDamagedTail(root, baseline, first, second); + + RecoveryPoint authorized = point(second); + RecoveryPoint common = point(first); + StateArchiveFiveLaneSegmentWriterV3.RecoveryFaultHook fault = (actual, laneId) -> { + if (actual == stage) { + throw new java.io.IOException("injected recovery fault at " + stage); + } + }; + assertThrows(java.io.IOException.class, + () -> StateArchiveFiveLaneSegmentWriterV3.recover(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000, + authorized, common, fault)); + + Path intent = root.resolve(StateArchiveFiveLaneRecoveryIntentV3.FILE_NAME); + Path temporary = root.resolve(StateArchiveFiveLaneRecoveryIntentV3.TEMP_FILE_NAME); + if (stage == RecoveryStage.TEMPORARY_FORCED) { + assertFalse(Files.exists(intent)); + assertTrue(Files.isRegularFile(temporary)); + try (StateArchiveFiveLaneSegmentWriterV3 recovered = + StateArchiveFiveLaneSegmentWriterV3.recover(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000, + authorized, common, StateArchiveFiveLaneSegmentWriterV3.RecoveryFaultHook.NONE)) { + assertEquals(1, recovered.getAppendHead().getBlockNumber()); + } + } else { + assertFalse(Files.exists(temporary)); + assertEquals(stage != RecoveryStage.INTENT_DELETED, Files.exists(intent)); + try (StateArchiveFiveLaneSegmentWriterV3 recovered = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + assertEquals(1, recovered.getAppendHead().getBlockNumber()); + assertTrue(recovered.getCurrentSegments().stream() + .allMatch(segment -> segment.getCurrentLastBlock() == 1)); + } + } + assertFalse(Files.exists(intent)); + assertFalse(Files.exists(temporary)); + Map recoveredSizes = fileSizes(root); + try (StateArchiveFiveLaneSegmentWriterV3 reopened = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + assertEquals(1, reopened.getAppendHead().getBlockNumber()); + } + assertEquals(recoveredSizes, fileSizes(root)); + } + } + + @Test + public void resumesDeletePairPlanWithExplicitMissingLane() throws Exception { + Path root = temporaryFolder.newFolder("five-lane-delete-intent").toPath(); + byte[] baseline = hash(60); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + EncodedBundle first = codec.encode(diff(1, 10), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + writer.append(first); + } + Files.delete(segment(root, 22, ".bidx")); + Files.delete(segment(root, 22, ".dat")); + + assertThrows(java.io.IOException.class, + () -> StateArchiveFiveLaneSegmentWriterV3.recover(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000, point(first), null, + (stage, laneId) -> { + if (stage == RecoveryStage.LANE_INDEX_APPLIED && laneId == 0) { + throw new java.io.IOException("injected delete-pair fault"); + } + })); + assertTrue(Files.isRegularFile( + root.resolve(StateArchiveFiveLaneRecoveryIntentV3.FILE_NAME))); + assertFalse(Files.exists(segment(root, 0, ".bidx"))); + assertTrue(Files.isRegularFile(segment(root, 0, ".dat"))); + + try (StateArchiveFiveLaneSegmentWriterV3 recovered = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + assertEquals(null, recovered.getAppendHead()); + assertTrue(recovered.getCurrentSegments().isEmpty()); + } + assertTrue(filesWithSuffix(root, ".dat").isEmpty()); + assertTrue(filesWithSuffix(root, ".bidx").isEmpty()); + assertFalse(Files.exists(root.resolve(StateArchiveFiveLaneRecoveryIntentV3.FILE_NAME))); + + Path emptyRoot = temporaryFolder.newFolder("five-lane-delete-empty-intent").toPath(); + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(emptyRoot, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + writer.append(first); + } + Files.delete(segment(emptyRoot, 22, ".bidx")); + Files.delete(segment(emptyRoot, 22, ".dat")); + assertThrows(java.io.IOException.class, + () -> StateArchiveFiveLaneSegmentWriterV3.recover(emptyRoot, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000, point(first), null, + (stage, laneId) -> { + if (stage == RecoveryStage.LANE_DATA_APPLIED && laneId == 13) { + throw new java.io.IOException("injected final delete-pair fault"); + } + })); + assertTrue(filesWithSuffix(emptyRoot, ".dat").isEmpty()); + assertTrue(Files.isRegularFile( + emptyRoot.resolve(StateArchiveFiveLaneRecoveryIntentV3.FILE_NAME))); + try (StateArchiveFiveLaneSegmentWriterV3 recovered = + new StateArchiveFiveLaneSegmentWriterV3(emptyRoot, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + assertEquals(null, recovered.getAppendHead()); + } + assertFalse(Files.exists( + emptyRoot.resolve(StateArchiveFiveLaneRecoveryIntentV3.FILE_NAME))); + } + + @Test + public void durableIntentRejectsTargetPrefixDrift() throws Exception { + Path root = temporaryFolder.newFolder("five-lane-intent-drift").toPath(); + byte[] baseline = hash(50); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + EncodedBundle first = codec.encode(diff(1, 10), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle second = codec.encode(diff(2, 10), first.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + prepareDamagedTail(root, baseline, first, second); + assertThrows(java.io.IOException.class, + () -> StateArchiveFiveLaneSegmentWriterV3.recover(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000, point(second), point(first), + (stage, laneId) -> { + if (stage == RecoveryStage.INTENT_PUBLISHED) { + throw new java.io.IOException("injected published-intent fault"); + } + })); + Path lane0 = segment(root, 0, ".dat"); + try (FileChannel channel = FileChannel.open(lane0, + StandardOpenOption.READ, StandardOpenOption.WRITE)) { + long offset = StateArchiveFileFormatV3.PART_HEADER_LENGTH + 40; + channel.position(offset); + ByteBuffer value = ByteBuffer.allocate(1); + channel.read(value); + value.flip(); + value.put(0, (byte) (value.get(0) ^ 1)); + channel.position(offset); + channel.write(value); + channel.force(false); + } + assertThrows(IllegalArgumentException.class, + () -> new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)); + assertTrue(Files.isRegularFile( + root.resolve(StateArchiveFiveLaneRecoveryIntentV3.FILE_NAME))); + } + + @Test + public void forcesAndReverifiesCompleteFiveLaneDurabilityProof() throws Exception { + Path root = temporaryFolder.newFolder("five-lane-sync").toPath(); + byte[] baseline = hash(40); + byte[] commonTarget = hash(100); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + EncodedBundle first = codec.encode(diff(1, 10), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle second = codec.encode(diff(2, 10), first.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle third = codec.encode(diff(3, 10), second.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + ArchiveDurabilityProof proof; + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + writer.append(first); + writer.append(second); + proof = writer.sync(7, point(second), commonTarget); + assertEquals(7, proof.getCheckpointSequence()); + assertEquals(5, proof.getFileTails().size()); + assertArrayEquals(StateArchiveFileFormatV3.compositeFormatDigest(), + proof.getFormatIdentity()); + assertArrayEquals(StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + proof.getDescriptorDigest()); + writer.verifyDurabilityProof(proof); + assertEquals(proof, writer.sync(8, point(second), commonTarget)); + writer.append(third); + } + + try (StateArchiveFiveLaneSegmentWriterV3 reopened = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + reopened.verifyDurabilityProof(proof); + assertEquals(7, reopened.getLastDurabilityProof().getCheckpointSequence()); + assertEquals(3, reopened.getAppendHead().getBlockNumber()); + ArchiveDurabilityProof next = reopened.sync(8, point(third), hash(101)); + assertEquals(8, next.getCheckpointSequence()); + assertTrue(next.getFileTails().stream() + .allMatch(tail -> tail.getMarkerOffset() > proof.getFileTails().stream() + .filter(previous -> previous.getLaneId() == tail.getLaneId()) + .findFirst().get().getMarkerOffset())); + reopened.verifyDurabilityProof(next); + } + } + + @Test + public void neverReturnsProofBeforeEveryMarkerForceAndRereadCompletes() + throws Exception { + for (SyncStage expected : SyncStage.values()) { + Path root = temporaryFolder.newFolder("five-lane-sync-fault-" + expected).toPath(); + byte[] baseline = hash(30); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + EncodedBundle first = codec.encode(diff(1, 10), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + writer.append(first); + assertThrows(java.io.IOException.class, + () -> writer.sync(1, point(first), hash(99), (actual, laneId) -> { + if (actual == expected) { + throw new java.io.IOException("injected sync fault at " + expected); + } + })); + assertThrows(IllegalStateException.class, () -> writer.append(first)); + } + } + } + + private static void prepareDamagedTail(Path root, byte[] baseline, + EncodedBundle first, EncodedBundle second) throws Exception { + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + writer.append(first); + writer.append(second); + } + try (FileChannel channel = FileChannel.open(segment(root, 22, ".dat"), + StandardOpenOption.WRITE)) { + channel.truncate(channel.size() - 10); + channel.force(false); + } + } + + private static RecoveryPoint point(EncodedBundle bundle) { + BlockSnapshotMeta meta = bundle.getDiff().getMeta(); + return new RecoveryPoint(meta.getEpoch(), meta.getBlockNumber(), meta.getTimestamp(), + meta.getBlockHash(), meta.getParentHash(), bundle.getResultHistoryDigest()); + } + + private static BlockReverseDiff diff(int blockNumber, int valueLength) { + byte[] value = new byte[valueLength]; + java.util.Arrays.fill(value, (byte) blockNumber); + List groups = valueLength == 0 ? Collections.emptyList() + : Collections.singletonList(new DbGroup(StateArchiveFileFormatV3.dbName(1), + Collections.singletonList(new Entry(new byte[]{1}, OldValue.present(value))))); + return new BlockReverseDiff(new BlockSnapshotMeta(blockNumber, blockNumber, + hash(blockNumber), hash(blockNumber - 1), blockNumber * 3_000L), groups); + } + + private static List filesWithSuffix(Path root, String suffix) throws Exception { + try (Stream files = Files.walk(root)) { + return files.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith(suffix)) + .collect(Collectors.toList()); + } + } + + private static Path segment(Path root, int laneId, String suffix) { + return root.resolve("segments/shard-000000") + .resolve(String.format("lane-%04d-seg-%020d%s", laneId, 0, suffix)); + } + + private static Map fileSizes(Path root) throws Exception { + Map sizes = new HashMap<>(); + try (Stream files = Files.walk(root)) { + for (Path path : files.filter(Files::isRegularFile).collect(Collectors.toList())) { + sizes.put(root.relativize(path), Files.size(path)); + } + } + return sizes; + } + + private static byte[] hash(int suffix) { + byte[] result = new byte[32]; + result[31] = (byte) suffix; + return result; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3Test.java new file mode 100644 index 00000000000..3bfa46413c3 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3Test.java @@ -0,0 +1,223 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.BlockIndexHeader; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.DurableMarker; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SealedSegment; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SegmentHeader; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SegmentSeal; + +public class StateArchiveSegmentFormatV3Test { + + @Test + public void freezesSegmentLayoutAndCompositeFormatIdentity() { + assertEquals( + "534c44330003000000000040000202000140008000200100003000c00000000077359400" + + "00000000040000000000040000010001000000000000000000000000", + Hex.toHexString(StateArchiveFileFormatV3.segmentLayoutDescriptor())); + assertEquals("d0d9c2111ddb5a30b2a97632e9c776da7eadbf81c1d08ec56ddaacab7a3ccddf", + Hex.toHexString(StateArchiveFileFormatV3.segmentLayoutDigest())); + assertEquals("c5464d3c1c3b2ee2d5622dabd83169db2140ac05d7fe995866bd294524b8f6a4", + Hex.toHexString(StateArchiveFileFormatV3.compositeFormatDigest())); + } + + @Test + public void roundTripsByteExactSegmentHeaderAndRejectsCorruption() { + SegmentHeader input = new SegmentHeader(22, 7, 101, hash(1), hash(2), + StateArchiveFileFormatV3.COMPRESSION_RAW_DEFLATE_LEVEL_1); + byte[] encoded = StateArchiveSegmentFormatV3.encodeHeader(input); + assertEquals(StateArchiveFileFormatV3.PART_HEADER_LENGTH, encoded.length); + + ByteBuffer bytes = ByteBuffer.wrap(encoded); + assertEquals(StateArchiveFileFormatV3.SEGMENT_MAGIC, bytes.getInt(0)); + assertEquals(22, Short.toUnsignedInt(bytes.getShort(14))); + assertEquals(7, bytes.getLong(20)); + assertEquals(101, bytes.getLong(28)); + assertEquals(StateArchiveFileFormatV3.SEGMENT_TARGET_BYTES, bytes.getLong(44)); + assertArrayEquals(StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + slice(encoded, 60, 32)); + assertArrayEquals(StateArchiveFileFormatV3.compositeFormatDigest(), + slice(encoded, 92, 32)); + + SegmentHeader decoded = StateArchiveSegmentFormatV3.decodeHeader(encoded); + assertEquals(22, decoded.getLaneId()); + assertEquals(7, decoded.getSegmentSeq()); + assertEquals(101, decoded.getActualFirstBlock()); + assertArrayEquals(hash(1), decoded.getPreviousSegmentDigest()); + assertArrayEquals(hash(2), decoded.getPreviousHistoryDigest()); + assertArrayEquals(slice(encoded, 476, 32), decoded.getHeaderDigest()); + + byte[] corruptReserved = encoded.clone(); + corruptReserved[200] = 1; + assertThrows(IllegalArgumentException.class, + () -> StateArchiveSegmentFormatV3.decodeHeader(corruptReserved)); + byte[] corruptDigest = encoded.clone(); + corruptDigest[507] ^= 1; + assertThrows(IllegalArgumentException.class, + () -> StateArchiveSegmentFormatV3.decodeHeader(corruptDigest)); + } + + @Test + public void rotatesOnlyBeforeTheBlockAfterTargetIsReached() { + long header = StateArchiveFileFormatV3.PART_HEADER_LENGTH; + long target = 1_000; + assertFalse(StateArchiveSegmentFormatV3.shouldRotate(0, target, target)); + assertFalse(StateArchiveSegmentFormatV3.shouldRotate(1, target - 1, target)); + assertTrue(StateArchiveSegmentFormatV3.shouldRotate(1, target, target)); + assertTrue(StateArchiveSegmentFormatV3.shouldRotate(1, target + 500, target)); + assertThrows(IllegalArgumentException.class, + () -> StateArchiveSegmentFormatV3.shouldRotate(1, header - 1, target)); + } + + @Test + public void roundTripsExactSealedSegmentMapRecordAndRejectsInvalidRanges() { + SealedSegment input = new SealedSegment(4, 9, 100, 102, 3, 8_000, 224, + hash(1), hash(2), hash(3), hash(4)); + byte[] encoded = StateArchiveSegmentFormatV3.encodeSealedMapRecord(input); + assertEquals(192, encoded.length); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + assertEquals(4, Short.toUnsignedInt(bytes.getShort(0))); + assertEquals(2, Short.toUnsignedInt(bytes.getShort(2))); + assertEquals(9, bytes.getLong(8)); + assertEquals(100, bytes.getLong(16)); + assertEquals(102, bytes.getLong(24)); + assertEquals(3, bytes.getLong(32)); + assertArrayEquals(hash(1), slice(encoded, 56, 32)); + assertArrayEquals(hash(4), slice(encoded, 152, 32)); + + SealedSegment decoded = StateArchiveSegmentFormatV3.decodeSealedMapRecord(encoded); + assertEquals(input.getLaneId(), decoded.getLaneId()); + assertEquals(input.getSegmentSeq(), decoded.getSegmentSeq()); + assertEquals(input.getFirstBlock(), decoded.getFirstBlock()); + assertEquals(input.getLastBlock(), decoded.getLastBlock()); + assertEquals(input.getBlockFrameCount(), decoded.getBlockFrameCount()); + assertArrayEquals(input.getSegmentContentDigest(), decoded.getSegmentContentDigest()); + + byte[] invalid = encoded.clone(); + ByteBuffer.wrap(invalid).putLong(32, 4); + assertThrows(IllegalArgumentException.class, + () -> StateArchiveSegmentFormatV3.decodeSealedMapRecord(invalid)); + } + + @Test + public void freezesSealDomainSegmentChainAndLaneBaselines() { + SegmentSeal input = new SegmentSeal(0, 0, 10, 11, 2, 3, + 64, 900, 1_412, 1_780, hash(1), hash(2), hash(3), hash(4), hash(5)); + byte[] encoded = StateArchiveSegmentFormatV3.encodeSeal(input); + assertEquals(368, encoded.length); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + assertEquals(StateArchiveFileFormatV3.PART_SEAL_FRAME_TYPE, bytes.getShort(8)); + assertEquals(0, Short.toUnsignedInt(bytes.getShort(32))); + assertEquals(0, bytes.getLong(36)); + assertEquals(10, bytes.getLong(44)); + assertEquals(11, bytes.getLong(52)); + assertEquals(1_412, bytes.getLong(92)); + assertEquals(1_780, bytes.getLong(100)); + + SegmentSeal decoded = StateArchiveSegmentFormatV3.decodeSeal(encoded); + assertEquals(11, decoded.getActualLastBlock()); + assertArrayEquals(slice(encoded, 320, 32), decoded.getEncodedFrameDigest()); + byte[] chain = StateArchiveSegmentFormatV3.segmentChainDigest(0, 0, + hash(6), decoded.getSegmentContentDigest(), decoded.getEncodedFrameDigest()); + assertEquals( + "3147cb009a99874675be38717a31e10655fe0be89fe5278bcb2503caa9385d2a" + + ":8f19c3370ce7750bcfad451d7d14de70cef356f1dd84a1ffda8c826cf2f70ae8" + + ":8a34caee97c24ec70adf60393672986423070d506935d9593e000594310a6798" + + ":a33fecda61e5cf29483fe4df26defa471ce3e5e6e0905c9ebfebeffe8932e0e8", + Hex.toHexString(StateArchiveSegmentFormatV3.laneBaselineDigest(0)) + ":" + + Hex.toHexString(StateArchiveSegmentFormatV3.laneBaselineDigest(4)) + ":" + + Hex.toHexString(decoded.getEncodedFrameDigest()) + ":" + + Hex.toHexString(chain)); + + byte[] corrupt = encoded.clone(); + corrupt[319] ^= 1; + assertThrows(IllegalArgumentException.class, + () -> StateArchiveSegmentFormatV3.decodeSeal(corrupt)); + } + + @Test + public void freezesCompleteBlockIndexHeader() { + BlockIndexHeader input = new BlockIndexHeader(22, 7, hash(9)); + byte[] encoded = StateArchiveSegmentFormatV3.encodeBlockIndexHeader(input); + assertEquals(128, encoded.length); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + assertEquals(StateArchiveFileFormatV3.BLOCK_INDEX_MAGIC, bytes.getInt(0)); + assertEquals(128, Short.toUnsignedInt(bytes.getShort(8))); + assertEquals(32, Short.toUnsignedInt(bytes.getShort(10))); + assertEquals(22, Short.toUnsignedInt(bytes.getShort(12))); + assertEquals(7, bytes.getLong(16)); + assertArrayEquals(hash(9), slice(encoded, 24, 32)); + assertArrayEquals(StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + slice(encoded, 56, 32)); + + BlockIndexHeader decoded = StateArchiveSegmentFormatV3.decodeBlockIndexHeader(encoded); + assertEquals(22, decoded.getLaneId()); + assertEquals(7, decoded.getSegmentSeq()); + assertArrayEquals(hash(9), decoded.getDataSegmentHeaderDigest()); + assertArrayEquals(slice(encoded, 92, 32), decoded.getHeaderDigest()); + assertEquals( + "5342493300030000008000200016000200000000000000070000000000000000" + + "0000000000000000000000000000000000000000000000099a9e46c5b7fea88a" + + "262bc7e4e52582b0c0d42bd6e5e454001c1d571b77076c4a00000000c055e205" + + "9140d1afec27340529f6685ebcda60627e2a594616f08aa53bb6d37708bc208f", + Hex.toHexString(encoded)); + + byte[] corrupt = encoded.clone(); + corrupt[88] = 1; + assertThrows(IllegalArgumentException.class, + () -> StateArchiveSegmentFormatV3.decodeBlockIndexHeader(corrupt)); + } + + @Test + public void roundTripsFiveLaneDurableMarkerAndRejectsDrift() { + DurableMarker input = new DurableMarker(7, 10, 11, 10, 11, hash(1), hash(2), + 512, 1_748, 2, 64, 900, hash(3), hash(4), 22, 5); + byte[] encoded = StateArchiveSegmentFormatV3.encodeDurableMarker(input); + assertEquals(336, encoded.length); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + assertEquals(StateArchiveFileFormatV3.DURABLE_MARKER_FRAME_TYPE, bytes.getShort(8)); + assertEquals(288, bytes.getInt(12)); + assertEquals(7, bytes.getLong(32)); + assertEquals(10, bytes.getLong(56)); + assertEquals(11, bytes.getLong(64)); + assertArrayEquals(StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + slice(encoded, 104, 32)); + assertEquals(22, Short.toUnsignedInt(bytes.getShort(272))); + assertEquals(5, bytes.getLong(276)); + + DurableMarker decoded = StateArchiveSegmentFormatV3.decodeDurableMarker(encoded); + assertEquals(7, decoded.getCheckpointSequence()); + assertEquals(10, decoded.getFirstBlock()); + assertEquals(11, decoded.getLastBlock()); + assertEquals(512, decoded.getCoveredStartOffset()); + assertEquals(1_748, decoded.getMarkerEndOffset()); + assertEquals(22, decoded.getLaneId()); + assertEquals(5, decoded.getSegmentSeq()); + assertArrayEquals(slice(encoded, 288, 32), decoded.getEncodedFrameDigest()); + + byte[] corrupt = encoded.clone(); + corrupt[240] ^= 1; + assertThrows(IllegalArgumentException.class, + () -> StateArchiveSegmentFormatV3.decodeDurableMarker(corrupt)); + } + + private static byte[] hash(int suffix) { + byte[] result = new byte[32]; + result[31] = (byte) suffix; + return result; + } + + private static byte[] slice(byte[] bytes, int offset, int length) { + byte[] result = new byte[length]; + System.arraycopy(bytes, offset, result, 0, length); + return result; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StorageRowKeyCodecTest.java b/framework/src/test/java/org/tron/core/db2/archive/StorageRowKeyCodecTest.java index 2b0693dc0ee..9e4d606f54a 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StorageRowKeyCodecTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StorageRowKeyCodecTest.java @@ -39,7 +39,7 @@ public void matchesLegacyNormalVersionAndCreate2GoldenVectors() { @Test public void latestVmStorageUsesTheSharedCodec() { - Storage storage = new Storage(ADDRESS, null); + Storage storage = new Storage(ADDRESS, null, key -> null); storage.setContractVersion(1); storage.generateAddrHash(TRANSACTION_HASH); DataWord slot = new DataWord(SLOT); diff --git a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java index 12233182cce..bcdd6e593b2 100644 --- a/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/ChainbaseCheckpointMaterializerTest.java @@ -36,8 +36,11 @@ import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.archive.HistoricalRangeOverlay; import org.tron.core.db2.archive.OldValue; +import org.tron.core.db2.archive.StateArchiveAppendCheckpointMaterializerV3; import org.tron.core.db2.archive.StateArchiveCheckpointMaterializer; import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; +import org.tron.core.db2.archive.StateArchiveFileFormatV3; +import org.tron.core.db2.archive.StateArchiveFiveLaneDurabilityProofV3; import org.tron.core.db2.archive.StateArchiveHotCheckpointMaterializer; import org.tron.core.db2.archive.StateArchiveHotStore; import org.tron.core.db2.common.DB; @@ -500,6 +503,56 @@ public void hotRuntimePreparesV2BeforeWalAndCompletesBothBarriers() throws Excep runtime.close(); } + @Test + public void appendRuntimePreparesSap3BeforeWalAndReopensPublishedTarget() throws Exception { + java.nio.file.Path root = temporaryFolder.newFolder("append-runtime-v3").toPath(); + java.nio.file.Path history = root.resolve("history"); + byte[] format = hash(96); + byte[] baselineHistory = hash(70); + V2Snapshots snapshots = new V2Snapshots(); + StateArchiveAppendCheckpointMaterializerV3 append = + new StateArchiveAppendCheckpointMaterializerV3(history, format, Engine.LEVELDB, + baselineHistory, StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000); + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator( + new CommonCheckpointFile(root.resolve("wal")), + new ChainbaseCheckpointMaterializer(root.resolve("chainbase"), format, + snapshots.databases), + new PublishingMaterializer(Authority.PATH_STATE), append); + AtomicLong clock = new AtomicLong(); + List timings = new ArrayList<>(); + CommonCheckpointRuntime runtime = new CommonCheckpointRuntime( + new CommonCheckpointRuntimeOwner(coordinator), snapshots.databases, history, + format, Engine.LEVELDB, + (blockNumber, blockHash) -> new TestLatest(snapshots.code, blockNumber, blockHash), + target -> () -> { }, null, append, null, () -> clock.addAndGet(1_000L), + timings::add); + + assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.NO_CHECKPOINT, + runtime.recoverBeforeServing()); + CommonCheckpointTarget target = runtime.checkpointAndRebase(1); + + assertEquals(snapshots.meta, target.getLastBlock()); + assertEquals(1, target.getArchiveBinding().getBlockCount()); + assertEquals(Status.PUBLISHED, append.inspect(target)); + assertEquals(760, java.nio.file.Files.size( + history.resolve(StateArchiveFiveLaneDurabilityProofV3.FILE_NAME))); + assertFalse(java.nio.file.Files.exists( + root.resolve("wal").resolve(CommonCheckpointFile.FILE_NAME))); + assertSame(snapshots.codeDatabase.getHead().getRoot(), snapshots.codeDatabase.getHead()); + assertSame(snapshots.propertiesDatabase.getHead().getRoot(), + snapshots.propertiesDatabase.getHead()); + assertEquals(1, timings.size()); + assertEquals(1, timings.get(0).getHotPrepareUs()); + assertThrows(IOException.class, () -> runtime.pinPoint(1)); + runtime.close(); + + try (StateArchiveAppendCheckpointMaterializerV3 reopened = + new StateArchiveAppendCheckpointMaterializerV3(history, format, Engine.LEVELDB, + baselineHistory, StateArchiveFileFormatV3.COMPRESSION_NONE, 10_000)) { + assertEquals(Status.PUBLISHED, reopened.inspect(target)); + } + } + @Test public void hotRuntimeRetriesAfterPrepareButBeforeWalPublication() throws Exception { java.nio.file.Path root = temporaryFolder.newFolder("hot-runtime-retry").toPath(); From 4f143bd6b8cb03320f5baac093338e0f615b675f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 15:45:05 +0800 Subject: [PATCH 132/161] feat(chainbase): add archive catalog serving --- .../PersistentServingKeyIndexGeneration.java | 8 +- .../archive/ServingIndexIncrementalPlan.java | 62 +++ ...ArchiveAppendCheckpointMaterializerV3.java | 85 +++- .../db2/archive/StateArchiveFileFormatV3.java | 4 + .../StateArchiveFiveLaneSegmentWriterV3.java | 283 ++++++++++- .../archive/StateArchiveHistoryCatalogV3.java | 439 ++++++++++++++++++ .../archive/StateArchiveSegmentFormatV3.java | 183 ++++++++ ...ArchiveServingIndexBuildCoordinatorV3.java | 327 +++++++++++++ .../main/java/org/tron/core/db/Manager.java | 34 ++ .../org/tron/core/net/TronNetDelegate.java | 2 +- ...iveAppendCheckpointMaterializerV3Test.java | 12 + ...rchiveCatalogAndServingCornerCaseTest.java | 168 +++++++ .../StateArchiveSegmentFormatV3Test.java | 35 ++ .../StateArchiveServingIndexSpeedTest.java | 82 ++++ 14 files changed, 1712 insertions(+), 12 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHistoryCatalogV3.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveCatalogAndServingCornerCaseTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveServingIndexSpeedTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java index d9543022f2a..94ee1fe1eca 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PersistentServingKeyIndexGeneration.java @@ -195,6 +195,13 @@ public static PersistentServingKeyIndexGeneration buildExact(Path directory, static PersistentServingKeyIndexGeneration buildExact(Path directory, String generationId, ServingIndexIncrementalPlan plan, byte[] latestSourceIdentityDigest, ExactWriteFaultHook faultHook) throws IOException { + return buildExact(directory, generationId, plan, latestSourceIdentityDigest, + configuredEngine(), faultHook); + } + + static PersistentServingKeyIndexGeneration buildExact(Path directory, String generationId, + ServingIndexIncrementalPlan plan, byte[] latestSourceIdentityDigest, Engine engine, + ExactWriteFaultHook faultHook) throws IOException { Objects.requireNonNull(directory, "directory"); Objects.requireNonNull(plan, "plan"); Objects.requireNonNull(faultHook, "faultHook"); @@ -203,7 +210,6 @@ static PersistentServingKeyIndexGeneration buildExact(Path directory, String gen throw new IllegalArgumentException("Serving generation directory already exists"); } Files.createDirectories(directory); - Engine engine = configuredEngine(); StateArchiveIndexEngineManifest.openOrCreate(directory, engine); byte[] sourceDigest = rollSourceDigest(plan.getSourceSeedDigest(), plan.getSourceStepDigests()); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/ServingIndexIncrementalPlan.java b/chainbase/src/main/java/org/tron/core/db2/archive/ServingIndexIncrementalPlan.java index 2ae07171315..0c472ff5ab7 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/ServingIndexIncrementalPlan.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/ServingIndexIncrementalPlan.java @@ -101,6 +101,68 @@ public static ServingIndexIncrementalPlan plan(long indexedThrough, byte[] headH Collections.unmodifiableMap(immutableChanges)); } + /** Plans an exact-27 increment directly from Common-committed captured reverse diffs. */ + static ServingIndexIncrementalPlan planCommittedDiffs(long indexedThrough, byte[] headHash, + List committedDiffs) { + if (indexedThrough < 0) { + throw new IllegalArgumentException("indexedThrough must not be negative"); + } + requireHash(headHash, "headHash"); + List participants = exactParticipants( + new ArrayList<>(ArchiveStoreScope.getStateDatabases())); + List diffs = new ArrayList<>(Objects.requireNonNull(committedDiffs, + "committedDiffs")); + Map> changes = new LinkedHashMap<>(); + participants.forEach(database -> changes.put(database, new ArrayList<>())); + MessageDigest seed = sha256(); + updateLong(seed, indexedThrough); + seed.update(headHash); + updateParticipants(seed, participants); + byte[] sourceSeed = seed.digest(); + MessageDigest delta = sha256(); + delta.update(sourceSeed); + List steps = new ArrayList<>(); + long previousBlock = indexedThrough; + byte[] previousHash = Arrays.copyOf(headHash, headHash.length); + BlockHistoryCodec sourceCodec = new BlockHistoryCodec(); + for (BlockReverseDiff diff : diffs) { + BlockSnapshotMeta meta = Objects.requireNonNull(diff, "committedDiff").getMeta(); + if (meta.getEpoch() != previousBlock + 1 || meta.getBlockNumber() != previousBlock + 1 + || !Arrays.equals(meta.getParentHash(), previousHash)) { + throw new IllegalArgumentException("Serving committed diff suffix is not contiguous"); + } + String previousDatabase = null; + for (BlockReverseDiff.DbGroup group : diff.getGroups()) { + List database = changes.get(group.getDbName()); + if (database == null || previousDatabase != null + && previousDatabase.compareTo(group.getDbName()) >= 0) { + throw new IllegalArgumentException("Serving committed diff Store coverage is invalid"); + } + byte[] previousKey = null; + for (BlockReverseDiff.Entry entry : group.getEntries()) { + byte[] key = entry.getKey(); + if (previousKey != null && BlockReverseDiff.compareUnsigned(previousKey, key) >= 0) { + throw new IllegalArgumentException("Serving committed diff keys are not unique"); + } + database.add(new KeyChange(key, meta.getEpoch())); + previousKey = key; + } + previousDatabase = group.getDbName(); + } + byte[] step = sha256().digest(sourceCodec.encode(diff)); + steps.add(step); + delta.update(step); + previousBlock = meta.getBlockNumber(); + previousHash = meta.getBlockHash(); + } + Map> immutable = new LinkedHashMap<>(); + changes.forEach((database, databaseChanges) -> immutable.put(database, + Collections.unmodifiableList(new ArrayList<>(databaseChanges)))); + return new ServingIndexIncrementalPlan(indexedThrough, headHash, previousBlock, + previousHash, delta.digest(), sourceSeed, steps, participants, + Collections.unmodifiableMap(immutable)); + } + public long getIndexedFrom() { return indexedFrom; } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java index 8619dc623ed..da94d5bd383 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -29,6 +30,10 @@ public final class StateArchiveAppendCheckpointMaterializerV3 private final StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); private final StateArchiveFiveLaneSegmentWriterV3 writer; + private final StateArchiveServingIndexBuildCoordinatorV3 servingCoordinator; + private StateArchiveServingIndexBuildCoordinatorV3.LiveServingIndexer liveServingIndexer; + private List stagedServingDiffs; + private CommonCheckpointTarget stagedServingTarget; private boolean closed; public StateArchiveAppendCheckpointMaterializerV3(Path directory, @@ -47,6 +52,9 @@ public StateArchiveAppendCheckpointMaterializerV3(Path directory, this.compressionId = compressionId; this.writer = new StateArchiveFiveLaneSegmentWriterV3(directory, baselineHistoryDigest, compressionId, rotationTargetBytes); + this.servingCoordinator = new StateArchiveServingIndexBuildCoordinatorV3(directory, + bindingEngine, 1_000); + recoverServingIndex(); } @Override @@ -85,6 +93,8 @@ public synchronized CommonCheckpointTarget prepare(CommonCheckpointCapture captu return target; } List diffs = admittedDiffs(admitted.getArchiveDiffs()); + stagedServingDiffs = diffs; + stagedServingTarget = target; if (!admitted.getArchiveBinding().equals(planCheckpoint(diffs))) { throw new IOException("Append-file Archive checkpoint binding differs"); } @@ -177,19 +187,50 @@ public synchronized void publish(CommonCheckpointTarget target) throws IOExcepti CommonCheckpointTarget admitted = requireTarget(target); Status status = inspect(admitted); if (status == Status.PUBLISHED) { + dispatchServingIfStaged(admitted); return; } if (status != Status.MATERIALIZED) { throw new IOException("Append-file Archive target is not materialized"); } StateArchiveCheckpointMaterializer.publishReadableTarget(directory, admitted); + dispatchServingIfStaged(admitted); + } + + /** Explicit sync-lifecycle handoff; it never infers completion from peer/head timing. */ + public synchronized void completeServingInitialSync(CommonCheckpointTarget boundary) + throws IOException { + requireOpen(); + liveServingIndexer = servingCoordinator.completeInitialSync(boundary); + } + + public synchronized StateArchiveServingIndexBuildCoordinatorV3.BuildProgress + servingIndexStatus() { + return servingCoordinator.status(); } @Override public synchronized void close() throws IOException { if (!closed) { closed = true; - writer.close(); + IOException failure = null; + try { + servingCoordinator.close(); + } catch (IOException closeFailure) { + failure = closeFailure; + } + try { + writer.close(); + } catch (IOException closeFailure) { + if (failure == null) { + failure = closeFailure; + } else { + failure.addSuppressed(closeFailure); + } + } + if (failure != null) { + throw failure; + } } } @@ -272,6 +313,48 @@ private short writerCompressionId() { return compressionId; } + private void dispatchServingIfStaged(CommonCheckpointTarget target) throws IOException { + if (stagedServingTarget == null || !stagedServingTarget.equals(target) + || stagedServingDiffs == null) { + return; + } + if (liveServingIndexer == null) { + servingCoordinator.offerCommittedRange(stagedServingDiffs, target); + } else { + liveServingIndexer.indexNow(stagedServingDiffs, target); + } + stagedServingDiffs = null; + stagedServingTarget = null; + } + + private void recoverServingIndex() throws IOException { + Optional published = + StateArchiveCheckpointMaterializer.loadReadableTargetIfPresent(directory); + if (!published.isPresent()) { + return; + } + CommonCheckpointTarget boundary = published.get(); + long indexed = servingCoordinator.status().getIndexedThrough(); + long target = boundary.getLastBlock().getBlockNumber(); + if (indexed > target) { + throw new IOException("Append-file serving index is ahead of Common W"); + } + if (indexed == target) { + servingCoordinator.recoverCommittedRange(Collections.emptyList(), boundary, true); + return; + } + long cursor = indexed >= 0 ? indexed : writer.getHistoryStartBlock() - 1; + if (cursor < 0) { + throw new IOException("Append-file serving recovery source is missing"); + } + while (cursor < target) { + long batchEnd = Math.min(target, cursor + 1_000); + List batch = writer.readCommittedDiffs(cursor, batchEnd); + servingCoordinator.recoverCommittedRange(batch, boundary, batchEnd == target); + cursor = batchEnd; + } + } + private void requireOpen() throws IOException { if (closed) { throw new IOException("Append-file Archive materializer is closed"); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFileFormatV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFileFormatV3.java index 76ed4ed5ab0..3d2e066227b 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFileFormatV3.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFileFormatV3.java @@ -22,6 +22,8 @@ public final class StateArchiveFileFormatV3 { public static final int BLOCK_INDEX_MAGIC = 0x53424933; public static final int RECOVERY_INTENT_MAGIC = 0x53524933; public static final int RECOVERY_INTENT_TRAILER_MAGIC = 0x33495253; + public static final int SEGMENT_MANIFEST_MAGIC = 0x53414d33; + public static final int SEGMENT_MANIFEST_TRAILER_MAGIC = 0x334d4153; public static final short MAJOR_VERSION = 3; public static final short MINOR_VERSION = 0; @@ -58,6 +60,8 @@ public final class StateArchiveFileFormatV3 { public static final int MANIFEST_HEADER_LENGTH = 256; public static final int MANIFEST_PART_RECORD_LENGTH = 160; public static final int MANIFEST_TRAILER_LENGTH = 48; + public static final int MANIFEST_TOTAL_LENGTH = MANIFEST_HEADER_LENGTH + + MANIFEST_TRAILER_LENGTH; public static final int SEGMENT_LAYOUT_DESCRIPTOR_LENGTH = 64; public static final int SEGMENT_MAP_ENTRY_LENGTH = 192; public static final int RECOVERY_INTENT_LAYOUT_DESCRIPTOR_LENGTH = 32; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java index 9bdb43aa6d2..c7707634a7e 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java @@ -32,6 +32,7 @@ import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.DurableMarker; import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SealedSegment; import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SegmentHeader; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SegmentManifest; import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SegmentSeal; /** Default-off five-lane append writer for State Archive v3 segment data and block indexes. */ @@ -42,6 +43,7 @@ public final class StateArchiveFiveLaneSegmentWriterV3 implements AutoCloseable private static final int PREVIOUS_HISTORY_DIGEST_OFFSET = 152; private static final int RESULT_HISTORY_DIGEST_OFFSET = 248; private static final int ENTRY_COUNT_OFFSET = 304; + private static final int COVERAGE_BITMAP_OFFSET = 280; private static final int RAW_PAYLOAD_LENGTH_OFFSET = 312; private static final int COMPRESSION_ID_OFFSET = 322; private static final int ENCODED_DIGEST_FROM_END = 48; @@ -55,6 +57,8 @@ public final class StateArchiveFiveLaneSegmentWriterV3 implements AutoCloseable new StateArchiveFiveLaneBlockCodecV3(); private final Map lanes = new HashMap<>(); private final List sealedSegments = new ArrayList<>(); + private final StateArchiveHistoryCatalogV3 catalog; + private boolean structuralChanged; private BlockSnapshotMeta appendHead; private byte[] resultHistoryDigest; private boolean failed; @@ -136,6 +140,7 @@ private StateArchiveFiveLaneSegmentWriterV3(Path archiveRoot, this.archiveRoot = archiveRoot; this.segmentRoot = archiveRoot.resolve("segments"); Files.createDirectories(segmentRoot); + this.catalog = StateArchiveHistoryCatalogV3.openOrEmpty(archiveRoot); requireNoLegacyIntent(); Intent existingIntent = loadIntent(); if (existingIntent != null) { @@ -279,18 +284,32 @@ private void append(EncodedBundle bundle, long checkpointSequence, } appendLaneFrame(state, meta, lane); } + appendHead = meta; + resultHistoryDigest = decoded.getResultHistoryDigest(); + if (structuralChanged || !catalog.isPublished()) { + publishCatalog(); + } } catch (IOException | RuntimeException failure) { failed = true; throw failure; } - appendHead = meta; - resultHistoryDigest = decoded.getResultHistoryDigest(); } public synchronized BlockSnapshotMeta getAppendHead() { return appendHead; } + public synchronized long getHistoryStartBlock() { + long first = Long.MAX_VALUE; + for (CurrentSegment segment : getCurrentSegments()) { + first = Math.min(first, segment.getFirstBlock()); + } + for (SealedSegment segment : sealedSegments) { + first = Math.min(first, segment.getFirstBlock()); + } + return first == Long.MAX_VALUE ? -1 : first; + } + public synchronized byte[] getResultHistoryDigest() { return resultHistoryDigest == null ? Arrays.copyOf(baselineHistoryDigest, baselineHistoryDigest.length) : Arrays.copyOf(resultHistoryDigest, @@ -306,6 +325,72 @@ public synchronized List getSealedSegments() { return Collections.unmodifiableList(new ArrayList<>(sealedSegments)); } + /** Replays complete five-lane bundles from Catalog-selected authority for serving repair. */ + public synchronized List readCommittedDiffs(long fromExclusive, long through) + throws IOException { + requireUsable(); + if (fromExclusive < 0 || through < fromExclusive || appendHead == null + || through > appendHead.getBlockNumber()) { + throw new IllegalArgumentException("Invalid State Archive committed read range"); + } + Map> bundles = new java.util.TreeMap<>(); + for (Path path : listDataFiles(false)) { + try (FileChannel data = FileChannel.open(path, StandardOpenOption.READ)) { + long offset = StateArchiveFileFormatV3.PART_HEADER_LENGTH; + while (offset < data.size()) { + byte[] envelope = readExact(data, offset, + StateArchiveFileFormatV3.FRAME_ENVELOPE_LENGTH); + ByteBuffer fields = ByteBuffer.wrap(envelope); + if (fields.getInt(0) != StateArchiveFileFormatV3.FRAME_MAGIC) { + throw new IOException("State Archive serving source frame magic mismatch"); + } + short frameType = fields.getShort(8); + long length = fields.getLong(16); + if (length <= 0 || length > Integer.MAX_VALUE || length > data.size() - offset) { + throw new IOException("State Archive serving source frame length mismatch"); + } + byte[] frame = readExact(data, offset, (int) length); + if (frameType == StateArchiveFileFormatV3.BLOCK_FRAME_TYPE) { + long block = blockNumber(frame); + if (block > fromExclusive && block <= through) { + int laneId = laneIdFromFrame(frame); + byte[] previous = bundles.computeIfAbsent(block, ignored -> new HashMap<>()) + .put(laneId, frame); + if (previous != null) { + throw new IOException("Duplicate State Archive serving source lane frame"); + } + } + } + offset += length; + } + } + } + List result = new ArrayList<>(); + for (long block = fromExclusive + 1; block <= through; block++) { + Map laneFrames = bundles.get(block); + if (laneFrames == null + || laneFrames.size() != StateArchiveFileFormatV3.fiveLaneIds().length) { + throw new IOException("Incomplete State Archive serving source bundle"); + } + List ordered = new ArrayList<>(); + for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { + ordered.add(laneFrames.get(laneId)); + } + result.add(codec.decode(ordered).getDiff()); + } + return Collections.unmodifiableList(result); + } + + private static int laneIdFromFrame(byte[] frame) throws IOException { + long coverage = ByteBuffer.wrap(frame).getLong(COVERAGE_BITMAP_OFFSET); + for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { + if (coverage == StateArchiveFileFormatV3.laneCoverage(laneId)) { + return laneId; + } + } + throw new IOException("State Archive serving source lane coverage mismatch"); + } + public synchronized ArchiveDurabilityProof getLastDurabilityProof() { return lastDurabilityProof; } @@ -539,6 +624,7 @@ private LaneState openNewSegment(int laneId, long firstBlock, decodedHeader.getHeaderDigest(), previousHistory, data, index, newContentDigest(headerBytes)); lanes.put(laneId, state); + structuralChanged = true; return state; } catch (IOException | RuntimeException failure) { data.close(); @@ -651,16 +737,28 @@ private void seal(LaneState state) throws IOException { + state.blockFrameCount * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH) { throw new IllegalStateException("State Archive sealed block index length mismatch"); } + byte[] previousSegmentDigest = state.segmentSeq == 0 + ? StateArchiveSegmentFormatV3.laneBaselineDigest(state.laneId) + : previousChainDigest(state.laneId, state.segmentSeq - 1); + SegmentManifest manifest = new SegmentManifest(state.laneId, state.segmentSeq, + state.firstBlock, state.lastBlock, state.blockFrameCount, state.entryCount, + state.logicalPayloadBytes, state.encodedBlockFrameBytes, + state.dataEndOffset + sealLength, state.index.size(), previousSegmentDigest, + state.endHistoryDigest); + byte[] encodedManifest = StateArchiveSegmentFormatV3.encodeManifest(manifest); + SegmentManifest decodedManifest = StateArchiveSegmentFormatV3.decodeManifest(encodedManifest); + publishManifest(state.laneId, state.segmentSeq, encodedManifest); sealedSegments.add(new SealedSegment(state.laneId, state.segmentSeq, state.firstBlock, state.lastBlock, state.blockFrameCount, state.dataEndOffset + sealLength, state.index.size(), state.headerDigest, - contentDigest, decodedSeal.getEncodedFrameDigest(), new byte[32])); + contentDigest, decodedSeal.getEncodedFrameDigest(), decodedManifest.getManifestDigest())); state.close(); lanes.remove(state.laneId); + structuralChanged = true; } private void reopen(Long recoveryBoundary) throws IOException { - List dataFiles = listDataFiles(); + List dataFiles = listDataFiles(recoveryBoundary != null || activeRecoveryIntent != null); if (dataFiles.isEmpty()) { resultHistoryDigest = Arrays.copyOf(baselineHistoryDigest, baselineHistoryDigest.length); @@ -723,6 +821,7 @@ private void reopen(Long recoveryBoundary) throws IOException { } if (!recovering) { rebuildLastDurabilityProof(scannedSegments, bundles); + validateCatalogSelection(); } if (recovering) { Intent intent = activeRecoveryIntent; @@ -791,6 +890,8 @@ private void reopen(Long recoveryBoundary) throws IOException { try { verifyRecoveredIntent(intent); recoveryFaultHook.after(RecoveryStage.TARGET_VERIFIED, -1); + structuralChanged = true; + publishCatalog(); clearIntent(); activeRecoveryIntent = null; } catch (IOException | RuntimeException failure) { @@ -1130,7 +1231,7 @@ private ScannedSegment scanSegment(Path path, ParsedName name, throw new IllegalArgumentException("Truncated State Archive block index header"); } ScannedSegment scanned = new ScannedSegment(path, data, index, indexPath, header, - headerBytes); + headerBytes, isCatalogSelectedCurrent(name)); long offset = StateArchiveFileFormatV3.PART_HEADER_LENGTH; while (offset < data.size()) { if (data.size() - offset < StateArchiveFileFormatV3.FRAME_ENVELOPE_LENGTH) { @@ -1304,13 +1405,104 @@ private byte[] previousChainDigest(int laneId, long sequence) { throw new IllegalStateException("Missing previous State Archive sealed segment"); } - private List listDataFiles() throws IOException { + private List listDataFiles(boolean recovering) throws IOException { + if (catalog.isPublished()) { + List selected = new ArrayList<>(); + for (SealedSegment segment : catalog.selected().getSealed()) { + selected.add(dataPath(segment.getLaneId(), segment.getSegmentSeq())); + } + for (CurrentSegment segment : catalog.selected().getCurrent()) { + selected.add(dataPath(segment.getLaneId(), segment.getSegmentSeq())); + Path successor = dataPath(segment.getLaneId(), segment.getSegmentSeq() + 1); + if (Files.isRegularFile(successor)) { + selected.add(successor); + } + } + for (Path path : selected) { + if (!Files.isRegularFile(path)) { + if (!recovering) { + throw new IOException("State Archive Catalog selected segment is missing"); + } + } + } + selected.removeIf(path -> !Files.isRegularFile(path)); + selected.sort(Comparator.comparing((Path path) -> parseName(path).laneId) + .thenComparingLong(path -> parseName(path).segmentSeq)); + return selected; + } try (Stream paths = Files.walk(segmentRoot)) { - return paths.filter(Files::isRegularFile) + List discovered = paths.filter(Files::isRegularFile) .filter(path -> path.getFileName().toString().endsWith(".dat")) .sorted(Comparator.comparing((Path path) -> parseName(path).laneId) .thenComparingLong(path -> parseName(path).segmentSeq)) .collect(Collectors.toList()); + if (!discovered.isEmpty()) { + throw new IOException("State Archive Catalog CURRENT is missing"); + } + return discovered; + } + } + + private void publishCatalog() throws IOException { + catalog.publish(rotationTargetBytes, getCurrentSegments(), getSealedSegments()); + structuralChanged = false; + } + + private boolean isCatalogSelectedCurrent(ParsedName name) { + return catalog.isPublished() && catalog.selected().getCurrent().stream().anyMatch(segment -> + segment.getLaneId() == name.laneId && segment.getSegmentSeq() == name.segmentSeq); + } + + private void validateCatalogSelection() throws IOException { + if (!catalog.isPublished()) { + if (!lanes.isEmpty() || !sealedSegments.isEmpty()) { + throw new IOException("State Archive Catalog selection is missing"); + } + return; + } + List expectedCurrent = catalog.selected().getCurrent(); + List actualCurrent = getCurrentSegments(); + List expectedSealed = catalog.selected().getSealed(); + for (SealedSegment expected : expectedSealed) { + SealedSegment actual = sealedSegments.stream().filter(candidate -> + candidate.getLaneId() == expected.getLaneId() + && candidate.getSegmentSeq() == expected.getSegmentSeq()) + .findFirst().orElse(null); + if (actual == null || !Arrays.equals(StateArchiveSegmentFormatV3.encodeSealedMapRecord( + expected), StateArchiveSegmentFormatV3.encodeSealedMapRecord(actual))) { + throw new IOException("State Archive Catalog sealed segment identity mismatch"); + } + } + for (CurrentSegment expected : expectedCurrent) { + CurrentSegment same = actualCurrent.stream().filter(actual -> + actual.getLaneId() == expected.getLaneId() + && actual.getSegmentSeq() == expected.getSegmentSeq()).findFirst().orElse(null); + if (same != null) { + if (same.getFirstBlock() != expected.getFirstBlock() + || !Arrays.equals(same.getHeaderDigest(), expected.getHeaderDigest())) { + throw new IOException("State Archive Catalog current segment identity mismatch"); + } + continue; + } + SealedSegment promoted = sealedSegments.stream().filter(actual -> + actual.getLaneId() == expected.getLaneId() + && actual.getSegmentSeq() == expected.getSegmentSeq()).findFirst().orElse(null); + if (promoted == null + || !Arrays.equals(promoted.getSegmentHeaderDigest(), expected.getHeaderDigest())) { + throw new IOException("State Archive Catalog selected current segment disappeared"); + } + CurrentSegment successor = actualCurrent.stream().filter(actual -> + actual.getLaneId() == expected.getLaneId() + && actual.getSegmentSeq() == expected.getSegmentSeq() + 1) + .findFirst().orElse(null); + if (successor != null && successor.getFirstBlock() != promoted.getLastBlock() + 1) { + throw new IOException("State Archive Catalog rotation successor is discontinuous"); + } + } + if (actualCurrent.size() == StateArchiveFileFormatV3.fiveLaneIds().length + && sealedSegments.size() > expectedSealed.size()) { + structuralChanged = true; + publishCatalog(); } } @@ -1335,6 +1527,28 @@ private Path indexPath(int laneId, long sequence) { name.substring(0, name.length() - 4) + ".bidx"); } + private Path manifestPath(int laneId, long sequence) { + String name = dataPath(laneId, sequence).getFileName().toString(); + return dataPath(laneId, sequence).resolveSibling( + name.substring(0, name.length() - 4) + ".manifest"); + } + + private void publishManifest(int laneId, long sequence, byte[] encoded) throws IOException { + Path target = manifestPath(laneId, sequence); + Path temporary = target.resolveSibling(target.getFileName() + ".tmp"); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + writeFully(channel, ByteBuffer.wrap(encoded)); + channel.force(true); + } + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("State Archive manifest requires atomic publication", unsupported); + } + syncDirectory(target.getParent()); + } + private static ParsedName parseName(Path path) { String name = path.getFileName().toString(); if (!name.matches("lane-[0-9]{4}-seg-[0-9]{20}\\.dat")) { @@ -1439,6 +1653,21 @@ private static void syncDirectory(Path directory) throws IOException { } } + private static void publishRecoveredManifest(Path target, byte[] encoded) throws IOException { + Path temporary = target.resolveSibling(target.getFileName() + ".tmp"); + try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + writeFully(channel, ByteBuffer.wrap(encoded)); + channel.force(true); + } + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("State Archive recovered manifest requires atomic move", unsupported); + } + syncDirectory(target.getParent()); + } + private static byte[] requireHash(byte[] value, String name) { if (value == null || value.length != StateArchiveFileFormatV3.HASH_LENGTH) { throw new IllegalArgumentException("Invalid State Archive " + name + " length"); @@ -1774,6 +2003,7 @@ private static final class ScannedSegment { private final FileChannel index; private final Path indexPath; private final SegmentHeader header; + private final boolean catalogSelectedCurrent; private final MessageDigest contentDigest; private final List expectedIndex = new ArrayList<>(); private long firstBlock = -1; @@ -1789,6 +2019,7 @@ private static final class ScannedSegment { private byte[] endHistory; private byte[] content; private SegmentSeal seal; + private byte[] manifestDigest; private long markedCount; private long markedLogicalBytes; private long markedEncodedBytes; @@ -1799,12 +2030,14 @@ private static final class ScannedSegment { private boolean tailDamaged; private ScannedSegment(Path dataPath, FileChannel data, FileChannel index, - Path indexPath, SegmentHeader header, byte[] headerBytes) { + Path indexPath, SegmentHeader header, byte[] headerBytes, + boolean catalogSelectedCurrent) { this.dataPath = dataPath; this.data = data; this.index = index; this.indexPath = indexPath; this.header = header; + this.catalogSelectedCurrent = catalogSelectedCurrent; this.contentDigest = newContentDigest(headerBytes); } @@ -1903,6 +2136,38 @@ private void finish(boolean recovering) throws IOException { || !Arrays.equals(seal.getSegmentContentDigest(), content)) { throw new IllegalArgumentException("State Archive segment seal mismatch"); } + Path manifestPath = dataPath.resolveSibling( + dataPath.getFileName().toString().replace(".dat", ".manifest")); + if (!Files.isRegularFile(manifestPath)) { + if (!catalogSelectedCurrent) { + throw new IllegalArgumentException("State Archive sealed manifest is missing"); + } + SegmentManifest recovered = new SegmentManifest(header.getLaneId(), + header.getSegmentSeq(), firstBlock, lastBlock, count, entryCount, logicalBytes, + encodedBytes, data.size(), StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + + count * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH, + header.getPreviousSegmentDigest(), endHistory); + publishRecoveredManifest(manifestPath, + StateArchiveSegmentFormatV3.encodeManifest(recovered)); + } + SegmentManifest manifest = StateArchiveSegmentFormatV3.decodeManifest( + Files.readAllBytes(manifestPath)); + long indexBytes = StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + + count * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH; + if (manifest.getLaneId() != header.getLaneId() + || manifest.getSegmentSeq() != header.getSegmentSeq() + || manifest.getFirstBlock() != firstBlock || manifest.getLastBlock() != lastBlock + || manifest.getBlockFrameCount() != count || manifest.getEntryCount() != entryCount + || manifest.getLogicalPayloadBytes() != logicalBytes + || manifest.getEncodedBlockFrameBytes() != encodedBytes + || manifest.getDataFileBytes() != data.size() + || manifest.getBlockIndexBytes() != indexBytes + || !Arrays.equals(manifest.getPreviousSegmentDigest(), + header.getPreviousSegmentDigest()) + || !Arrays.equals(manifest.getFinalHistoryDigest(), endHistory)) { + throw new IllegalArgumentException("State Archive sealed manifest identity mismatch"); + } + manifestDigest = manifest.getManifestDigest(); data.close(); index.close(); } @@ -2103,7 +2368,7 @@ private SealedSegment sealedMap() { StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + count * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH, header.getHeaderDigest(), content, - seal.getEncodedFrameDigest(), new byte[32]); + seal.getEncodedFrameDigest(), manifestDigest); } } } diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHistoryCatalogV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHistoryCatalogV3.java new file mode 100644 index 00000000000..a0a8bbcf8e8 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveHistoryCatalogV3.java @@ -0,0 +1,439 @@ +package org.tron.core.db2.archive; + +import com.google.common.hash.Hashing; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.CurrentSegment; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SealedSegment; + +/** Atomic structural catalog for the five-lane append-file history. */ +final class StateArchiveHistoryCatalogV3 { + + static final String DIRECTORY = "catalog"; + static final String CURRENT = "CURRENT"; + private static final String GENERATIONS = "generations"; + private static final int GENERATION_MAGIC = 0x53434733; // SCG3 + private static final int CURRENT_MAGIC = 0x53435533; // SCU3 + private static final int GENERATION_TRAILER_MAGIC = 0x33474353; // 3GCS + private static final int CURRENT_TRAILER_MAGIC = 0x33554353; // 3UCS + private static final int HEADER_LENGTH = 256; + private static final int CURRENT_RECORD_LENGTH = 112; + private static final int TRAILER_LENGTH = 48; + private static final int CURRENT_LENGTH = 96; + + private final Path root; + private final Path generations; + private Generation selected; + + private StateArchiveHistoryCatalogV3(Path archiveRoot, Generation selected) { + root = archiveRoot.resolve(DIRECTORY); + generations = root.resolve(GENERATIONS); + this.selected = selected; + } + + static StateArchiveHistoryCatalogV3 openOrEmpty(Path archiveRoot) throws IOException { + Path root = archiveRoot.resolve(DIRECTORY); + Path current = root.resolve(CURRENT); + if (!Files.exists(current)) { + return new StateArchiveHistoryCatalogV3(archiveRoot, null); + } + CurrentPointer pointer = decodeCurrent(Files.readAllBytes(current)); + Path generationPath = root.resolve(GENERATIONS).resolve(fileName(pointer.generation)); + if (!Files.isRegularFile(generationPath)) { + throw new IOException("State Archive Catalog selected generation is missing"); + } + byte[] encoded = Files.readAllBytes(generationPath); + Generation generation = decodeGeneration(encoded); + if (generation.generation != pointer.generation + || !Arrays.equals(generation.digest, pointer.digest)) { + throw new IOException("State Archive Catalog CURRENT identity mismatch"); + } + return new StateArchiveHistoryCatalogV3(archiveRoot, generation); + } + + boolean isPublished() { + return selected != null; + } + + Generation selected() { + if (selected == null) { + throw new IllegalStateException("State Archive Catalog has no selected generation"); + } + return selected; + } + + void publish(long segmentTargetBytes, List current, + List sealed) throws IOException { + long generation = selected == null ? 0 : selected.generation + 1; + byte[] previous = selected == null ? new byte[32] : selected.digest; + Generation replacement = new Generation(generation, previous, segmentTargetBytes, + current, sealed, null); + byte[] encoded = encodeGeneration(replacement); + Generation verified = decodeGeneration(encoded); + Files.createDirectories(generations); + Path temporary = generations.resolve(fileName(generation) + ".tmp"); + Path target = generations.resolve(fileName(generation)); + writeForced(temporary, encoded); + if (Files.isRegularFile(target)) { + Generation orphan = decodeGeneration(Files.readAllBytes(target)); + if (!Arrays.equals(orphan.digest, verified.digest)) { + throw new IOException("State Archive Catalog orphan generation identity differs"); + } + Files.delete(temporary); + } else { + atomicMove(temporary, target); + syncDirectory(generations); + } + byte[] currentBytes = encodeCurrent(generation, verified.digest); + Path currentTemporary = root.resolve(CURRENT + ".tmp"); + writeForced(currentTemporary, currentBytes); + atomicMove(currentTemporary, root.resolve(CURRENT)); + syncDirectory(root); + selected = verified; + } + + private static byte[] encodeGeneration(Generation generation) { + generation.validate(); + int totalLength = Math.addExact(HEADER_LENGTH + TRAILER_LENGTH, + Math.addExact(generation.current.size() * CURRENT_RECORD_LENGTH, + generation.sealed.size() * StateArchiveFileFormatV3.SEGMENT_MAP_ENTRY_LENGTH)); + ByteBuffer bytes = ByteBuffer.allocate(totalLength); + bytes.putInt(GENERATION_MAGIC); + bytes.putShort(StateArchiveFileFormatV3.MAJOR_VERSION); + bytes.putShort(StateArchiveFileFormatV3.MINOR_VERSION); + bytes.putInt(HEADER_LENGTH); + bytes.putInt(0); + bytes.putLong(totalLength); + bytes.putLong(generation.generation); + bytes.putLong(generation.segmentTargetBytes); + bytes.putInt(generation.current.size()); + bytes.putInt(generation.sealed.size()); + bytes.putInt(CURRENT_RECORD_LENGTH); + bytes.putInt(StateArchiveFileFormatV3.SEGMENT_MAP_ENTRY_LENGTH); + bytes.put(generation.previousDigest); + bytes.put(StateArchiveFileFormatV3.compositeFormatDigest()); + bytes.put(StateArchiveFileFormatV3.fiveLaneDescriptorDigest()); + bytes.put(laneSetDigest()); + bytes.put(new byte[72]); + for (CurrentSegment segment : generation.current) { + bytes.put(encodeCurrentRecord(segment)); + } + for (SealedSegment segment : generation.sealed) { + bytes.put(StateArchiveSegmentFormatV3.encodeSealedMapRecord(segment)); + } + int digestOffset = totalLength - TRAILER_LENGTH; + if (bytes.position() != digestOffset) { + throw new IllegalStateException("Invalid State Archive Catalog generation layout"); + } + bytes.put(StateArchiveFileFormatV3.sha256(Arrays.copyOf(bytes.array(), digestOffset))); + bytes.putLong(totalLength); + bytes.putInt(crc32c(bytes.array(), 0, totalLength - 8)); + bytes.putInt(GENERATION_TRAILER_MAGIC); + return bytes.array(); + } + + private static Generation decodeGeneration(byte[] encoded) throws IOException { + try { + if (encoded == null || encoded.length < HEADER_LENGTH + TRAILER_LENGTH) { + throw new IllegalArgumentException("Catalog generation is truncated"); + } + ByteBuffer bytes = ByteBuffer.wrap(encoded); + require(bytes.getInt() == GENERATION_MAGIC, "Catalog generation magic mismatch"); + require(bytes.getShort() == StateArchiveFileFormatV3.MAJOR_VERSION, + "Catalog generation major version mismatch"); + require(bytes.getShort() == StateArchiveFileFormatV3.MINOR_VERSION, + "Catalog generation minor version mismatch"); + require(bytes.getInt() == HEADER_LENGTH, "Catalog generation header length mismatch"); + require(bytes.getInt() == 0, "Catalog generation flags mismatch"); + require(bytes.getLong() == encoded.length, "Catalog generation length mismatch"); + long generation = bytes.getLong(); + long segmentTargetBytes = bytes.getLong(); + int currentCount = bytes.getInt(); + int sealedCount = bytes.getInt(); + require(bytes.getInt() == CURRENT_RECORD_LENGTH, + "Catalog current record length mismatch"); + require(bytes.getInt() == StateArchiveFileFormatV3.SEGMENT_MAP_ENTRY_LENGTH, + "Catalog sealed record length mismatch"); + byte[] previous = read(bytes, 32); + require(Arrays.equals(read(bytes, 32), StateArchiveFileFormatV3.compositeFormatDigest()), + "Catalog format identity mismatch"); + require(Arrays.equals(read(bytes, 32), StateArchiveFileFormatV3.fiveLaneDescriptorDigest()), + "Catalog descriptor identity mismatch"); + require(Arrays.equals(read(bytes, 32), laneSetDigest()), + "Catalog lane set identity mismatch"); + requireZero(bytes, 72); + require(generation >= 0 && segmentTargetBytes > StateArchiveFileFormatV3.PART_HEADER_LENGTH + && (currentCount == 0 + || currentCount == StateArchiveFileFormatV3.fiveLaneIds().length) + && sealedCount >= 0, "Catalog generation header is invalid"); + long expectedLength = HEADER_LENGTH + TRAILER_LENGTH + + (long) currentCount * CURRENT_RECORD_LENGTH + + (long) sealedCount * StateArchiveFileFormatV3.SEGMENT_MAP_ENTRY_LENGTH; + require(expectedLength == encoded.length, "Catalog generation record count mismatch"); + List current = new ArrayList<>(); + for (int index = 0; index < currentCount; index++) { + current.add(decodeCurrentRecord(read(bytes, CURRENT_RECORD_LENGTH))); + } + List sealed = new ArrayList<>(); + for (int index = 0; index < sealedCount; index++) { + sealed.add(StateArchiveSegmentFormatV3.decodeSealedMapRecord( + read(bytes, StateArchiveFileFormatV3.SEGMENT_MAP_ENTRY_LENGTH))); + } + int digestOffset = encoded.length - TRAILER_LENGTH; + byte[] digest = read(bytes, 32); + require(Arrays.equals(digest, + StateArchiveFileFormatV3.sha256(Arrays.copyOf(encoded, digestOffset))), + "Catalog generation digest mismatch"); + require(bytes.getLong() == encoded.length, "Catalog repeated length mismatch"); + require(bytes.getInt() == crc32c(encoded, 0, encoded.length - 8), + "Catalog generation checksum mismatch"); + require(bytes.getInt() == GENERATION_TRAILER_MAGIC, + "Catalog generation trailer mismatch"); + return new Generation(generation, previous, segmentTargetBytes, current, sealed, digest); + } catch (IllegalArgumentException invalid) { + throw new IOException("State Archive Catalog generation is corrupt", invalid); + } + } + + private static byte[] encodeCurrentRecord(CurrentSegment segment) { + return ByteBuffer.allocate(CURRENT_RECORD_LENGTH) + .putShort((short) segment.getLaneId()) + .putShort(StateArchiveFileFormatV3.laneKind(segment.getLaneId())) + .putInt(0).putLong(segment.getSegmentSeq()).putLong(segment.getFirstBlock()) + .putLong(segment.getCurrentLastBlock()).putLong(segment.getDataEndOffset()) + .putLong(segment.getBlockFrameCount()).put(segment.getHeaderDigest()) + .put(parentDigest(segment.getLaneId(), segment.getSegmentSeq())).array(); + } + + private static CurrentSegment decodeCurrentRecord(byte[] encoded) { + ByteBuffer bytes = ByteBuffer.wrap(encoded); + int laneId = Short.toUnsignedInt(bytes.getShort()); + require(bytes.getShort() == StateArchiveFileFormatV3.laneKind(laneId), + "Catalog current lane kind mismatch"); + require(bytes.getInt() == 0, "Catalog current flags mismatch"); + long sequence = bytes.getLong(); + CurrentSegment current = new CurrentSegment(laneId, sequence, bytes.getLong(), + bytes.getLong(), bytes.getLong(), bytes.getLong(), read(bytes, 32)); + require(Arrays.equals(read(bytes, 32), parentDigest(laneId, sequence)), + "Catalog current parent digest mismatch"); + return current; + } + + private static byte[] parentDigest(int laneId, long sequence) { + return StateArchiveFileFormatV3.sha256( + ByteBuffer.allocate(Short.BYTES + Long.BYTES).putShort((short) laneId) + .putLong(sequence).array()); + } + + private static byte[] laneSetDigest() { + int[] lanes = StateArchiveFileFormatV3.fiveLaneIds(); + ByteBuffer bytes = ByteBuffer.allocate(lanes.length * Short.BYTES); + for (int lane : lanes) { + bytes.putShort((short) lane); + } + return StateArchiveFileFormatV3.sha256(bytes.array()); + } + + private static byte[] encodeCurrent(long generation, byte[] digest) { + ByteBuffer bytes = ByteBuffer.allocate(CURRENT_LENGTH); + bytes.putInt(CURRENT_MAGIC).putShort(StateArchiveFileFormatV3.MAJOR_VERSION) + .putShort(StateArchiveFileFormatV3.MINOR_VERSION).putLong(generation).put(digest) + .put(new byte[36]); + bytes.putInt(crc32c(bytes.array(), 0, CURRENT_LENGTH - 12)); + bytes.putInt(CURRENT_TRAILER_MAGIC); + return bytes.array(); + } + + private static CurrentPointer decodeCurrent(byte[] encoded) throws IOException { + try { + require(encoded != null && encoded.length == CURRENT_LENGTH, + "Catalog CURRENT length mismatch"); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + require(bytes.getInt() == CURRENT_MAGIC, "Catalog CURRENT magic mismatch"); + require(bytes.getShort() == StateArchiveFileFormatV3.MAJOR_VERSION, + "Catalog CURRENT major version mismatch"); + require(bytes.getShort() == StateArchiveFileFormatV3.MINOR_VERSION, + "Catalog CURRENT minor version mismatch"); + long generation = bytes.getLong(); + byte[] digest = read(bytes, 32); + requireZero(bytes, 36); + require(bytes.getInt() == crc32c(encoded, 0, CURRENT_LENGTH - 12), + "Catalog CURRENT checksum mismatch"); + require(bytes.getInt() == CURRENT_TRAILER_MAGIC, "Catalog CURRENT trailer mismatch"); + require(generation >= 0, "Catalog CURRENT generation is invalid"); + return new CurrentPointer(generation, digest); + } catch (IllegalArgumentException invalid) { + throw new IOException("State Archive Catalog CURRENT is corrupt", invalid); + } + } + + private static void writeForced(Path path, byte[] encoded) throws IOException { + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer bytes = ByteBuffer.wrap(encoded); + while (bytes.hasRemaining()) { + channel.write(bytes); + } + channel.force(true); + } + } + + private static void atomicMove(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException unsupported) { + throw new IOException("State Archive Catalog requires atomic publication", unsupported); + } + } + + private static void syncDirectory(Path directory) throws IOException { + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + private static byte[] read(ByteBuffer bytes, int length) { + byte[] value = new byte[length]; + bytes.get(value); + return value; + } + + private static void requireZero(ByteBuffer bytes, int length) { + for (int index = 0; index < length; index++) { + require(bytes.get() == 0, "Catalog reserved bytes are non-zero"); + } + } + + private static void require(boolean condition, String message) { + if (!condition) { + throw new IllegalArgumentException(message); + } + } + + private static int crc32c(byte[] bytes, int offset, int length) { + return Hashing.crc32c().hashBytes(bytes, offset, length).asInt(); + } + + private static String fileName(long generation) { + return String.format("catalog-%020d.bin", generation); + } + + static final class Generation { + private final long generation; + private final byte[] previousDigest; + private final long segmentTargetBytes; + private final List current; + private final List sealed; + private final byte[] digest; + + private Generation(long generation, byte[] previousDigest, long segmentTargetBytes, + List current, List sealed, byte[] digest) { + this.generation = generation; + this.previousDigest = Arrays.copyOf(Objects.requireNonNull(previousDigest), 32); + this.segmentTargetBytes = segmentTargetBytes; + this.current = sortedCurrent(current); + this.sealed = sortedSealed(sealed); + this.digest = digest == null ? null : Arrays.copyOf(digest, digest.length); + validate(); + } + + private void validate() { + require(previousDigest.length == 32 && (digest == null || digest.length == 32), + "Catalog digest length mismatch"); + int[] expected = StateArchiveFileFormatV3.fiveLaneIds(); + require(current.isEmpty() || current.size() == expected.length, + "Catalog must contain zero or five current lanes"); + require(!current.isEmpty() || sealed.isEmpty(), + "Catalog cannot omit current lanes after sealed history"); + for (int index = 0; index < expected.length; index++) { + require(current.isEmpty() || current.get(index).getLaneId() == expected[index], + "Catalog current lane set mismatch"); + } + int previousLane = -1; + long previousSequence = -1; + long previousLast = -1; + for (SealedSegment segment : sealed) { + if (segment.getLaneId() != previousLane) { + previousLane = segment.getLaneId(); + previousSequence = -1; + previousLast = -1; + } + require(segment.getSegmentSeq() == previousSequence + 1, + "Catalog sealed sequence has a gap"); + if (previousLast >= 0) { + require(segment.getFirstBlock() == previousLast + 1, + "Catalog sealed block range has a gap"); + } + previousSequence = segment.getSegmentSeq(); + previousLast = segment.getLastBlock(); + } + for (CurrentSegment segment : current) { + long lastSequence = -1; + long lastBlock = -1; + for (SealedSegment sealedSegment : sealed) { + if (sealedSegment.getLaneId() == segment.getLaneId()) { + lastSequence = sealedSegment.getSegmentSeq(); + lastBlock = sealedSegment.getLastBlock(); + } + } + require(segment.getSegmentSeq() == lastSequence + 1, + "Catalog current sequence does not follow sealed segments"); + if (lastBlock >= 0) { + require(segment.getFirstBlock() == lastBlock + 1, + "Catalog current block range does not follow sealed segments"); + } + } + } + + long getGeneration() { + return generation; + } + + List getCurrent() { + return current; + } + + List getSealed() { + return sealed; + } + + byte[] getDigest() { + return Arrays.copyOf(digest, digest.length); + } + } + + private static List sortedCurrent(List input) { + List copy = new ArrayList<>(Objects.requireNonNull(input)); + copy.sort(Comparator.comparingInt(CurrentSegment::getLaneId)); + return Collections.unmodifiableList(copy); + } + + private static List sortedSealed(List input) { + List copy = new ArrayList<>(Objects.requireNonNull(input)); + copy.sort(Comparator.comparingInt(SealedSegment::getLaneId) + .thenComparingLong(SealedSegment::getSegmentSeq)); + return Collections.unmodifiableList(copy); + } + + private static final class CurrentPointer { + private final long generation; + private final byte[] digest; + + private CurrentPointer(long generation, byte[] digest) { + this.generation = generation; + this.digest = digest; + } + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3.java index be6af79f667..188682d81cc 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3.java @@ -16,6 +16,8 @@ public final class StateArchiveSegmentFormatV3 { private static final int MARKER_CRC_OFFSET = 328; private static final int BLOCK_INDEX_HEADER_DIGEST_OFFSET = 92; private static final int BLOCK_INDEX_HEADER_CRC_OFFSET = 124; + private static final int MANIFEST_DIGEST_OFFSET = 256; + private static final int MANIFEST_CRC_OFFSET = 296; private StateArchiveSegmentFormatV3() { } @@ -469,6 +471,104 @@ public static SealedSegment decodeSealedMapRecord(byte[] encoded) { return result; } + /** Encodes the frozen 304-byte SAM3 sealed-segment manifest. */ + public static byte[] encodeManifest(SegmentManifest manifest) { + Objects.requireNonNull(manifest, "manifest"); + manifest.validate(); + ByteBuffer bytes = ByteBuffer.allocate(StateArchiveFileFormatV3.MANIFEST_TOTAL_LENGTH); + bytes.putInt(StateArchiveFileFormatV3.SEGMENT_MANIFEST_MAGIC); + bytes.putShort(StateArchiveFileFormatV3.MAJOR_VERSION); + bytes.putShort(StateArchiveFileFormatV3.MINOR_VERSION); + bytes.putInt(StateArchiveFileFormatV3.MANIFEST_HEADER_LENGTH); + bytes.putInt(0); + bytes.putLong(StateArchiveFileFormatV3.MANIFEST_TOTAL_LENGTH); + bytes.putShort(StateArchiveFileFormatV3.laneKind(manifest.laneId)); + bytes.putShort((short) manifest.laneId); + bytes.putShort(StateArchiveFileFormatV3.SEGMENT_LAYOUT_ID); + bytes.putShort((short) 0); + bytes.putLong(manifest.segmentSeq); + bytes.putLong(manifest.firstBlock); + bytes.putLong(manifest.lastBlock); + bytes.putLong(manifest.blockFrameCount); + bytes.putLong(manifest.entryCount); + bytes.putLong(manifest.logicalPayloadBytes); + bytes.putLong(manifest.encodedBlockFrameBytes); + bytes.putLong(manifest.dataFileBytes); + bytes.putLong(manifest.blockIndexBytes); + bytes.put(StateArchiveFileFormatV3.fiveLaneDescriptorDigest()); + bytes.put(StateArchiveFileFormatV3.compositeFormatDigest()); + bytes.put(manifest.previousSegmentDigest); + bytes.put(manifest.finalHistoryDigest); + bytes.put(new byte[24]); + if (bytes.position() != MANIFEST_DIGEST_OFFSET) { + throw new IllegalStateException("Invalid State Archive manifest header layout"); + } + bytes.put(StateArchiveFileFormatV3.sha256( + Arrays.copyOf(bytes.array(), MANIFEST_DIGEST_OFFSET))); + bytes.putLong(StateArchiveFileFormatV3.MANIFEST_TOTAL_LENGTH); + bytes.putInt(crc32c(bytes.array(), 0, MANIFEST_CRC_OFFSET)); + bytes.putInt(StateArchiveFileFormatV3.SEGMENT_MANIFEST_TRAILER_MAGIC); + if (bytes.hasRemaining()) { + throw new IllegalStateException("Invalid State Archive manifest length"); + } + return bytes.array(); + } + + /** Decodes and validates the frozen 304-byte SAM3 sealed-segment manifest. */ + public static SegmentManifest decodeManifest(byte[] encoded) { + requireLength(encoded, StateArchiveFileFormatV3.MANIFEST_TOTAL_LENGTH, + "segment manifest"); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + requireInt(bytes, StateArchiveFileFormatV3.SEGMENT_MANIFEST_MAGIC, "manifest magic"); + requireShort(bytes, StateArchiveFileFormatV3.MAJOR_VERSION, "manifest major version"); + requireShort(bytes, StateArchiveFileFormatV3.MINOR_VERSION, "manifest minor version"); + requireInt(bytes, StateArchiveFileFormatV3.MANIFEST_HEADER_LENGTH, + "manifest header length"); + requireInt(bytes, 0, "manifest flags"); + requireLong(bytes, StateArchiveFileFormatV3.MANIFEST_TOTAL_LENGTH, + "manifest total length"); + short laneKind = bytes.getShort(); + int laneId = Short.toUnsignedInt(bytes.getShort()); + if (laneKind != StateArchiveFileFormatV3.laneKind(laneId)) { + throw new IllegalArgumentException("State Archive manifest lane kind mismatch"); + } + requireShort(bytes, StateArchiveFileFormatV3.SEGMENT_LAYOUT_ID, + "manifest segment layout"); + requireShort(bytes, (short) 0, "manifest reserved field"); + long segmentSeq = requireNonNegative(bytes.getLong(), "manifest segment sequence"); + long firstBlock = requireNonNegative(bytes.getLong(), "manifest first block"); + long lastBlock = requireNonNegative(bytes.getLong(), "manifest last block"); + long blockFrameCount = requireNonNegative(bytes.getLong(), "manifest block count"); + long entryCount = requireNonNegative(bytes.getLong(), "manifest entry count"); + long logicalPayloadBytes = requireNonNegative(bytes.getLong(), + "manifest logical payload bytes"); + long encodedBlockFrameBytes = requireNonNegative(bytes.getLong(), + "manifest encoded frame bytes"); + long dataFileBytes = requireNonNegative(bytes.getLong(), "manifest data bytes"); + long blockIndexBytes = requireNonNegative(bytes.getLong(), + "manifest block index bytes"); + requireArray(getBytes(bytes, 32), StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + "manifest descriptor digest"); + requireArray(getBytes(bytes, 32), StateArchiveFileFormatV3.compositeFormatDigest(), + "manifest composite format digest"); + byte[] previousSegmentDigest = getBytes(bytes, 32); + byte[] finalHistoryDigest = getBytes(bytes, 32); + requireZero(bytes, 24, "manifest reserved bytes"); + byte[] manifestDigest = getBytes(bytes, 32); + requireArray(manifestDigest, StateArchiveFileFormatV3.sha256( + Arrays.copyOf(encoded, MANIFEST_DIGEST_OFFSET)), "manifest digest"); + requireLong(bytes, StateArchiveFileFormatV3.MANIFEST_TOTAL_LENGTH, + "manifest repeated length"); + if (bytes.getInt() != crc32c(encoded, 0, MANIFEST_CRC_OFFSET)) { + throw new IllegalArgumentException("State Archive manifest checksum mismatch"); + } + requireInt(bytes, StateArchiveFileFormatV3.SEGMENT_MANIFEST_TRAILER_MAGIC, + "manifest trailer magic"); + return new SegmentManifest(laneId, segmentSeq, firstBlock, lastBlock, blockFrameCount, + entryCount, logicalPayloadBytes, encodedBlockFrameBytes, dataFileBytes, blockIndexBytes, + previousSegmentDigest, finalHistoryDigest, manifestDigest); + } + private static void requireCompression(short compressionId) { if (compressionId != StateArchiveFileFormatV3.COMPRESSION_NONE && compressionId != StateArchiveFileFormatV3.COMPRESSION_RAW_DEFLATE_LEVEL_1) { @@ -991,6 +1091,89 @@ public byte[] getHeaderDigest() { } } + public static final class SegmentManifest { + private final int laneId; + private final long segmentSeq; + private final long firstBlock; + private final long lastBlock; + private final long blockFrameCount; + private final long entryCount; + private final long logicalPayloadBytes; + private final long encodedBlockFrameBytes; + private final long dataFileBytes; + private final long blockIndexBytes; + private final byte[] previousSegmentDigest; + private final byte[] finalHistoryDigest; + private final byte[] manifestDigest; + + public SegmentManifest(int laneId, long segmentSeq, long firstBlock, long lastBlock, + long blockFrameCount, long entryCount, long logicalPayloadBytes, + long encodedBlockFrameBytes, long dataFileBytes, long blockIndexBytes, + byte[] previousSegmentDigest, byte[] finalHistoryDigest) { + this(laneId, segmentSeq, firstBlock, lastBlock, blockFrameCount, entryCount, + logicalPayloadBytes, encodedBlockFrameBytes, dataFileBytes, blockIndexBytes, + previousSegmentDigest, finalHistoryDigest, null); + } + + private SegmentManifest(int laneId, long segmentSeq, long firstBlock, long lastBlock, + long blockFrameCount, long entryCount, long logicalPayloadBytes, + long encodedBlockFrameBytes, long dataFileBytes, long blockIndexBytes, + byte[] previousSegmentDigest, byte[] finalHistoryDigest, byte[] manifestDigest) { + this.laneId = laneId; + this.segmentSeq = segmentSeq; + this.firstBlock = firstBlock; + this.lastBlock = lastBlock; + this.blockFrameCount = blockFrameCount; + this.entryCount = entryCount; + this.logicalPayloadBytes = logicalPayloadBytes; + this.encodedBlockFrameBytes = encodedBlockFrameBytes; + this.dataFileBytes = dataFileBytes; + this.blockIndexBytes = blockIndexBytes; + this.previousSegmentDigest = requireHash(previousSegmentDigest, + "manifest previous segment digest"); + this.finalHistoryDigest = requireHash(finalHistoryDigest, + "manifest final history digest"); + this.manifestDigest = manifestDigest == null ? null + : requireHash(manifestDigest, "manifest digest"); + validate(); + } + + private void validate() { + StateArchiveFileFormatV3.laneKind(laneId); + if (segmentSeq < 0 || firstBlock < 0 || lastBlock < firstBlock + || blockFrameCount <= 0 || lastBlock - firstBlock + 1 != blockFrameCount + || entryCount < 0 || logicalPayloadBytes < 0 || encodedBlockFrameBytes < 0 + || dataFileBytes < StateArchiveFileFormatV3.PART_HEADER_LENGTH + + StateArchiveFileFormatV3.SEAL_HEADER_LENGTH + + StateArchiveFileFormatV3.FRAME_TRAILER_LENGTH + || blockIndexBytes != StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH + + blockFrameCount * StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH) { + throw new IllegalArgumentException("Invalid State Archive segment manifest state"); + } + } + + public int getLaneId() { return laneId; } + public long getSegmentSeq() { return segmentSeq; } + public long getFirstBlock() { return firstBlock; } + public long getLastBlock() { return lastBlock; } + public long getBlockFrameCount() { return blockFrameCount; } + public long getEntryCount() { return entryCount; } + public long getLogicalPayloadBytes() { return logicalPayloadBytes; } + public long getEncodedBlockFrameBytes() { return encodedBlockFrameBytes; } + public long getDataFileBytes() { return dataFileBytes; } + public long getBlockIndexBytes() { return blockIndexBytes; } + public byte[] getPreviousSegmentDigest() { + return Arrays.copyOf(previousSegmentDigest, previousSegmentDigest.length); + } + public byte[] getFinalHistoryDigest() { + return Arrays.copyOf(finalHistoryDigest, finalHistoryDigest.length); + } + public byte[] getManifestDigest() { + return manifestDigest == null ? null : Arrays.copyOf(manifestDigest, + manifestDigest.length); + } + } + public static final class CurrentSegment { private final int laneId; private final long segmentSeq; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java new file mode 100644 index 00000000000..0fea69f82ec --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java @@ -0,0 +1,327 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import org.bouncycastle.util.encoders.Hex; +import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Single-owner bulk-to-live coordinator for the append-file v3 exact-27 serving index. */ +public final class StateArchiveServingIndexBuildCoordinatorV3 implements AutoCloseable { + + static final String DIRECTORY = "serving-index-v3"; + private final Path archiveRoot; + private final Path catalogRoot; + private final Engine engine; + private final int bulkStartBlocks; + private final List pending = new ArrayList<>(); + private PersistentServingKeyIndexCatalog catalog; + private CommonCheckpointTarget committedHead; + private byte[] latestSourceIdentity; + private long indexedThrough = -1; + private byte[] indexedHash; + private long buildSequence; + private long syncSession; + private Mode mode = Mode.RECOVERING; + private boolean closed; + + public StateArchiveServingIndexBuildCoordinatorV3(Path archiveRoot, Engine engine, + int bulkStartBlocks) throws IOException { + this.archiveRoot = Objects.requireNonNull(archiveRoot, "archiveRoot"); + this.catalogRoot = archiveRoot.resolve(DIRECTORY); + this.engine = Objects.requireNonNull(engine, "engine"); + if (bulkStartBlocks <= 0) { + throw new IllegalArgumentException("bulkStartBlocks must be positive"); + } + this.bulkStartBlocks = bulkStartBlocks; + if (Files.isRegularFile(catalogRoot.resolve("current"), LinkOption.NOFOLLOW_LINKS)) { + catalog = PersistentServingKeyIndexCatalog.open(catalogRoot, engine, stage -> { }); + try (PersistentServingKeyIndexGeneration current = catalog.pin()) { + indexedThrough = current.getIndexedThrough(); + indexedHash = current.getHeadHash(); + } + } + mode = Mode.BULK_CATCH_UP; + } + + /** Accepts only a Common-published contiguous range; bulk flushes at the configured threshold. */ + public synchronized BuildProgress offerCommittedRange(List diffs, + CommonCheckpointTarget target) throws IOException { + requireOpen(); + if (mode != Mode.BULK_CATCH_UP) { + throw new IllegalStateException("Serving bulk input is closed after handoff"); + } + admit(diffs, target); + if (pending.size() >= bulkStartBlocks) { + flushPending(); + } + return progress(); + } + + synchronized void recoverCommittedRange(List supplied, + CommonCheckpointTarget publishedHead, boolean finalRange) throws IOException { + requireOpen(); + if (mode != Mode.BULK_CATCH_UP) { + throw new IllegalStateException("Serving recovery requires bulk mode"); + } + List diffs = new ArrayList<>(Objects.requireNonNull(supplied, "diffs")); + if (diffs.isEmpty()) { + if (finalRange) { + CommonCheckpointTarget target = Objects.requireNonNull(publishedHead, "publishedHead"); + if (indexedThrough != target.getLastBlock().getBlockNumber() + || !Arrays.equals(indexedHash, target.getLastBlock().getBlockHash())) { + throw new IOException("Serving recovery zero-action boundary mismatch"); + } + committedHead = target; + } + return; + } + BlockSnapshotMeta previous = pending.isEmpty() ? null : pending.get(pending.size() - 1).getMeta(); + if (previous == null && indexedThrough >= 0) { + BlockSnapshotMeta first = diffs.get(0).getMeta(); + if (first.getBlockNumber() != indexedThrough + 1 + || !Arrays.equals(first.getParentHash(), indexedHash)) { + throw new IOException("Serving recovery suffix does not extend durable I"); + } + } + for (BlockReverseDiff diff : diffs) { + if (previous != null && (diff.getMeta().getBlockNumber() + != previous.getBlockNumber() + 1 + || !Arrays.equals(diff.getMeta().getParentHash(), previous.getBlockHash()))) { + throw new IOException("Serving recovery suffix has a gap"); + } + previous = diff.getMeta(); + } + pending.addAll(diffs); + latestSourceIdentity = StateArchiveFileFormatV3.sha256( + new BlockHistoryCodec().encode(diffs.get(diffs.size() - 1))); + if (finalRange) { + CommonCheckpointTarget target = Objects.requireNonNull(publishedHead, "publishedHead"); + if (!previous.equals(target.getLastBlock())) { + throw new IOException("Serving recovery suffix differs from published W"); + } + committedHead = target; + latestSourceIdentity = target.getPayloadDigest(); + } + if (pending.size() >= bulkStartBlocks || finalRange) { + flushPending(); + } + } + + /** Drains the FIFO through the exact Common boundary and returns a generation-bound live handle. */ + public synchronized LiveServingIndexer completeInitialSync(CommonCheckpointTarget boundary) + throws IOException { + requireOpen(); + if (mode != Mode.BULK_CATCH_UP || committedHead == null + || !committedHead.equals(Objects.requireNonNull(boundary, "boundary"))) { + throw new IllegalArgumentException("Serving handoff boundary is not the committed head"); + } + mode = Mode.HANDOFF_DRAINING; + try { + flushPending(); + if (indexedThrough != boundary.getLastBlock().getBlockNumber() + || !Arrays.equals(indexedHash, boundary.getLastBlock().getBlockHash())) { + throw new IOException("Serving handoff did not reach the exact Common boundary"); + } + mode = Mode.LIVE_IMMEDIATE; + syncSession++; + return new LiveServingIndexer(syncSession, buildSequence, + catalog.getCurrentGenerationId()); + } catch (IOException | RuntimeException failure) { + mode = Mode.CATCH_UP_REQUIRED; + throw failure; + } + } + + public synchronized BuildProgress status() { + requireOpen(); + return progress(); + } + + @Override + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + mode = Mode.CLOSED; + if (catalog != null) { + catalog.close(); + } + } + } + + private void admit(List supplied, CommonCheckpointTarget target) { + List diffs = new ArrayList<>(Objects.requireNonNull(supplied, "diffs")); + CommonCheckpointTarget admittedTarget = Objects.requireNonNull(target, "target"); + if (diffs.isEmpty() || diffs.contains(null) + || !diffs.get(0).getMeta().equals(admittedTarget.getFirstBlock()) + || !diffs.get(diffs.size() - 1).getMeta().equals(admittedTarget.getLastBlock())) { + throw new IllegalArgumentException("Serving committed range differs from Common target"); + } + BlockSnapshotMeta expectedParent; + if (!pending.isEmpty()) { + expectedParent = pending.get(pending.size() - 1).getMeta(); + } else if (committedHead != null) { + expectedParent = committedHead.getLastBlock(); + } else if (indexedThrough >= 0) { + expectedParent = new BlockSnapshotMeta(indexedThrough, indexedThrough, indexedHash, + new byte[32], 0); + } else { + expectedParent = null; + } + BlockSnapshotMeta previous = expectedParent; + for (BlockReverseDiff diff : diffs) { + BlockSnapshotMeta meta = diff.getMeta(); + if (previous != null && (meta.getBlockNumber() != previous.getBlockNumber() + 1 + || !Arrays.equals(meta.getParentHash(), previous.getBlockHash()))) { + throw new IllegalArgumentException("Serving committed ranges are not contiguous"); + } + previous = meta; + } + pending.addAll(diffs); + committedHead = admittedTarget; + latestSourceIdentity = admittedTarget.getPayloadDigest(); + } + + private void flushPending() throws IOException { + if (pending.isEmpty()) { + return; + } + long base = indexedThrough >= 0 ? indexedThrough + : pending.get(0).getMeta().getBlockNumber() - 1; + byte[] baseHash = indexedHash == null ? pending.get(0).getMeta().getParentHash() : indexedHash; + ServingIndexIncrementalPlan plan = ServingIndexIncrementalPlan.planCommittedDiffs( + base, baseHash, pending); + byte[] sourceIdentity = Objects.requireNonNull(latestSourceIdentity, + "latestSourceIdentity"); + String generationId = generationId(plan.getIndexedThrough(), plan.getHeadHash()); + Path shadow = archiveRoot.resolve(".serving-index-v3-" + UUID.randomUUID()); + try { + if (catalog == null) { + try (PersistentServingKeyIndexGeneration ignored = + PersistentServingKeyIndexGeneration.buildExact(shadow, generationId, plan, + sourceIdentity, engine, () -> { })) { + // Full descriptor and exact-27 coverage are verified again by catalog creation. + } + catalog = PersistentServingKeyIndexCatalog.create(catalogRoot, shadow); + } else { + String expected = catalog.getCurrentGenerationId(); + try (PersistentServingKeyIndexGeneration current = catalog.pin(); + PersistentServingKeyIndexGeneration ignored = current.extendExact(shadow, + generationId, plan, sourceIdentity)) { + // Verify immutable generation before CAS publication. + } + if (!catalog.publish(expected, shadow)) { + throw new IOException("Serving generation changed during FIFO publication"); + } + } + indexedThrough = plan.getIndexedThrough(); + indexedHash = plan.getHeadHash(); + buildSequence++; + pending.clear(); + } catch (IOException | RuntimeException failure) { + mode = Mode.CATCH_UP_REQUIRED; + throw failure; + } + } + + private String generationId(long blockNumber, byte[] hash) { + return String.format("append-v3-%020d-%s-%08d", blockNumber, + Hex.toHexString(Arrays.copyOf(hash, 6)), buildSequence + 1); + } + + private BuildProgress progress() { + long committed = committedHead == null ? indexedThrough + : committedHead.getLastBlock().getBlockNumber(); + return new BuildProgress(mode, indexedThrough, committed, pending.size(), buildSequence); + } + + private void requireOpen() { + if (closed) { + throw new IllegalStateException("Serving coordinator is closed"); + } + } + + public enum Mode { + RECOVERING, + BULK_CATCH_UP, + HANDOFF_DRAINING, + LIVE_IMMEDIATE, + CATCH_UP_REQUIRED, + DEGRADED, + CLOSED + } + + public static final class BuildProgress { + private final Mode mode; + private final long indexedThrough; + private final long committedThrough; + private final int pendingBlocks; + private final long buildSequence; + + private BuildProgress(Mode mode, long indexedThrough, long committedThrough, + int pendingBlocks, long buildSequence) { + this.mode = mode; + this.indexedThrough = indexedThrough; + this.committedThrough = committedThrough; + this.pendingBlocks = pendingBlocks; + this.buildSequence = buildSequence; + } + + public Mode getMode() { return mode; } + public long getIndexedThrough() { return indexedThrough; } + public long getCommittedThrough() { return committedThrough; } + public int getPendingBlocks() { return pendingBlocks; } + public long getBuildSequence() { return buildSequence; } + } + + public final class LiveServingIndexer { + private final long session; + private long sequence; + private String generation; + private boolean valid = true; + + private LiveServingIndexer(long session, long sequence, String generation) { + this.session = session; + this.sequence = sequence; + this.generation = generation; + } + + /** Publishes every block in the committed range as an individual durable generation. */ + public BuildProgress indexNow(List diffs, CommonCheckpointTarget target) + throws IOException { + synchronized (StateArchiveServingIndexBuildCoordinatorV3.this) { + requireOpen(); + if (!valid || mode != Mode.LIVE_IMMEDIATE || session != syncSession + || sequence != buildSequence || !generation.equals(catalog.getCurrentGenerationId())) { + throw new IllegalStateException("Serving live handle is stale"); + } + List admitted = new ArrayList<>(Objects.requireNonNull(diffs, "diffs")); + try { + admit(admitted, target); + for (BlockReverseDiff diff : admitted) { + List remainder = new ArrayList<>(pending); + pending.clear(); + pending.add(diff); + flushPending(); + pending.addAll(remainder.subList(1, remainder.size())); + } + sequence = buildSequence; + generation = catalog.getCurrentGenerationId(); + return progress(); + } catch (IOException | RuntimeException failure) { + valid = false; + mode = Mode.CATCH_UP_REQUIRED; + throw failure; + } + } + } + } +} diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 5b19770ba23..925674c9de0 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -255,6 +255,8 @@ public class Manager { private PathStateRuntimeAttachment pathStateRuntime; @Getter private CommonCheckpointRuntimeAttachment commonCheckpointRuntime; + private StateArchiveAppendCheckpointMaterializerV3 stateArchiveAppendMaterializer; + private boolean stateArchiveServingLive; private StateArchiveRuntimeOwner.ServingIndexFaultHook stateArchiveServingIndexFaultHook = stage -> { }; private StateArchiveRuntimeOwner.ReadableStateFaultHook stateArchiveReadableStateFaultHook = @@ -983,6 +985,7 @@ private void initCommonCheckpoint() { attachPathStateBlockFinalRuntime(); snapshots.attachCommonCheckpointRuntime(attachment); commonCheckpointRuntime = attachment; + stateArchiveAppendMaterializer = admittedAppendMaterializer; pathOwner = null; attachment = null; hotStore = null; @@ -2329,9 +2332,21 @@ public void pushBlock(final BlockCapsule block) DupTransactionException, TransactionExpirationException, BadNumberBlockException, BadBlockException, NonCommonBlockException, ReceiptCheckErrException, VMIllegalException, ZksnarkException, EventBloomException { + pushBlock(block, false); + } + + /** Saves a block while preserving the explicit network sync/live source transition. */ + public void pushBlock(final BlockCapsule block, boolean syncSource) + throws ValidateSignatureException, ContractValidateException, ContractExeException, + UnLinkedBlockException, ValidateScheduleException, AccountResourceInsufficientException, + TaposException, TooBigTransactionException, TooBigTransactionResultException, + DupTransactionException, TransactionExpirationException, + BadNumberBlockException, BadBlockException, NonCommonBlockException, + ReceiptCheckErrException, VMIllegalException, ZksnarkException, EventBloomException { setBlockWaitLock(true); try { synchronized (this) { + updateStateArchiveServingMode(syncSource); Metrics.histogramObserve(blockedTimer.get()); blockedTimer.remove(); if (Metrics.enabled()) { @@ -3793,6 +3808,25 @@ private void closeCommonCheckpoint() { ((SnapshotManager) revokingStore).detachCommonCheckpointRuntime(runtime); runtime.close(); commonCheckpointRuntime = null; + stateArchiveAppendMaterializer = null; + stateArchiveServingLive = false; + } + + private void updateStateArchiveServingMode(boolean syncSource) { + StateArchiveAppendCheckpointMaterializerV3 materializer = stateArchiveAppendMaterializer; + if (materializer == null || syncSource || stateArchiveServingLive) { + return; + } + try { + java.util.Optional published = + materializer.loadPublishedTargetIfPresent(); + if (published.isPresent()) { + materializer.completeServingInitialSync(published.get()); + stateArchiveServingLive = true; + } + } catch (java.io.IOException failure) { + throw new IllegalStateException("State Archive serving-index handoff failed", failure); + } } private void closePathStateRoot() { diff --git a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java index 23050f5218d..1414ea9afa6 100644 --- a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java +++ b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java @@ -282,7 +282,7 @@ public void processBlock(BlockCapsule block, boolean isSync) throws P2pException MetricKeys.Histogram.LOCK_ACQUIRE_LATENCY, MetricLabels.BLOCK)); Histogram.Timer timer = Metrics.histogramStartTimer( MetricKeys.Histogram.BLOCK_PROCESS_LATENCY, String.valueOf(isSync)); - dbManager.pushBlock(block); + dbManager.pushBlock(block, isSync); Metrics.histogramObserve(timer); freshBlockId.put(blockId, System.currentTimeMillis()); logger.info("Success process block {}", blockId.getString()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java index 5f78fe1d42f..4d9e37ecce7 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java @@ -65,6 +65,9 @@ public void preparesBeforeWalPublishesAndReopensExactTarget() throws Exception { try (StateArchiveAppendCheckpointMaterializerV3 reopened = materializer( root, format, baseline, 10_000)) { assertEquals(Status.PUBLISHED, reopened.inspect(target)); + assertEquals(1, reopened.servingIndexStatus().getIndexedThrough()); + assertEquals(StateArchiveServingIndexBuildCoordinatorV3.Mode.BULK_CATCH_UP, + reopened.servingIndexStatus().getMode()); reopened.materialize(payload, target); reopened.publish(target); } @@ -134,6 +137,13 @@ public void advancesTwoPublishedTargetsAndPreservesFreshHotBindingBytes() throws firstPayload, Collections.singletonList(first), firstDescriptor)); archive.publish(firstTarget); assertEquals(Status.PUBLISHED, archive.inspect(firstTarget)); + assertEquals(StateArchiveServingIndexBuildCoordinatorV3.Mode.BULK_CATCH_UP, + archive.servingIndexStatus().getMode()); + assertEquals(1, archive.servingIndexStatus().getPendingBlocks()); + archive.completeServingInitialSync(firstTarget); + assertEquals(StateArchiveServingIndexBuildCoordinatorV3.Mode.LIVE_IMMEDIATE, + archive.servingIndexStatus().getMode()); + assertEquals(1, archive.servingIndexStatus().getIndexedThrough()); BlockReverseDiff second = diff(2, 16); StateArchiveHotBatchDescriptor secondDescriptor = archive.planCheckpoint( @@ -145,6 +155,8 @@ public void advancesTwoPublishedTargetsAndPreservesFreshHotBindingBytes() throws assertEquals(Status.MATERIALIZED, archive.inspect(secondTarget)); archive.publish(secondTarget); assertEquals(Status.PUBLISHED, archive.inspect(secondTarget)); + assertEquals(2, archive.servingIndexStatus().getIndexedThrough()); + assertEquals(0, archive.servingIndexStatus().getPendingBlocks()); assertThrows(java.io.IOException.class, () -> archive.inspect(firstTarget)); } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCatalogAndServingCornerCaseTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCatalogAndServingCornerCaseTest.java new file mode 100644 index 00000000000..3fca8d3ed23 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCatalogAndServingCornerCaseTest.java @@ -0,0 +1,168 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.OptionalLong; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.StateArchiveServingIndexBuildCoordinatorV3.LiveServingIndexer; +import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class StateArchiveCatalogAndServingCornerCaseTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void bulkBatchCutsProduceTheSameExactIndexIdentity() throws Exception { + List all = diffs(1, 6, 0); + Path oneBatch = temporaryFolder.newFolder("one-batch").toPath(); + Path splitBatch = temporaryFolder.newFolder("split-batch").toPath(); + try (StateArchiveServingIndexBuildCoordinatorV3 one = + new StateArchiveServingIndexBuildCoordinatorV3(oneBatch, Engine.LEVELDB, 6); + StateArchiveServingIndexBuildCoordinatorV3 split = + new StateArchiveServingIndexBuildCoordinatorV3(splitBatch, Engine.LEVELDB, 3)) { + one.offerCommittedRange(all, target(all, 1)); + split.offerCommittedRange(all.subList(0, 2), target(all.subList(0, 2), 2)); + split.offerCommittedRange(all.subList(2, 3), target(all.subList(2, 3), 3)); + split.offerCommittedRange(all.subList(3, 6), target(all.subList(3, 6), 4)); + assertEquals(6, one.status().getIndexedThrough()); + assertEquals(6, split.status().getIndexedThrough()); + } + try (PersistentServingKeyIndexCatalog one = PersistentServingKeyIndexCatalog.open( + oneBatch.resolve(StateArchiveServingIndexBuildCoordinatorV3.DIRECTORY), + Engine.LEVELDB, stage -> { }); + PersistentServingKeyIndexCatalog split = PersistentServingKeyIndexCatalog.open( + splitBatch.resolve(StateArchiveServingIndexBuildCoordinatorV3.DIRECTORY), + Engine.LEVELDB, stage -> { }); + PersistentServingKeyIndexGeneration oneGeneration = one.pin(); + PersistentServingKeyIndexGeneration splitGeneration = split.pin()) { + assertArrayEquals(oneGeneration.getAuthoritativePrefixDigest(), + splitGeneration.getAuthoritativePrefixDigest()); + assertEquals(oneGeneration.getKeyChangeCount(), splitGeneration.getKeyChangeCount()); + OptionalLong oneChange = oneGeneration.firstChangeAfter("code", new byte[]{3}, 0, 6); + OptionalLong splitChange = splitGeneration.firstChangeAfter("code", new byte[]{3}, 0, 6); + assertEquals(oneChange, splitChange); + } + } + + @Test + public void gapInvalidatesLiveHandleWithoutAdvancingI() throws Exception { + Path root = temporaryFolder.newFolder("live-gap").toPath(); + List first = diffs(1, 1, 0); + try (StateArchiveServingIndexBuildCoordinatorV3 coordinator = + new StateArchiveServingIndexBuildCoordinatorV3(root, Engine.LEVELDB, 1)) { + CommonCheckpointTarget firstTarget = target(first, 1); + coordinator.offerCommittedRange(first, firstTarget); + LiveServingIndexer live = coordinator.completeInitialSync(firstTarget); + List gap = diffs(3, 1, 2); + assertThrows(IllegalArgumentException.class, + () -> live.indexNow(gap, target(gap, 3))); + assertEquals(1, coordinator.status().getIndexedThrough()); + assertEquals(StateArchiveServingIndexBuildCoordinatorV3.Mode.CATCH_UP_REQUIRED, + coordinator.status().getMode()); + List successor = diffs(2, 1, 1); + assertThrows(IllegalStateException.class, + () -> live.indexNow(successor, target(successor, 2))); + } + } + + @Test + public void catalogIgnoresOrphanButRejectsCorruptCurrentAndMissingManifest() throws Exception { + Path root = temporaryFolder.newFolder("catalog-corners").toPath(); + byte[] baseline = hash(0); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + BlockReverseDiff first = diff(1, 0, 1_400); + StateArchiveFiveLaneBlockCodecV3.EncodedBundle firstBundle = codec.encode(first, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + StateArchiveFiveLaneBlockCodecV3.EncodedBundle secondBundle = codec.encode( + diff(2, 1, 0), firstBundle.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + Path current = root.resolve(StateArchiveHistoryCatalogV3.DIRECTORY) + .resolve(StateArchiveHistoryCatalogV3.CURRENT); + byte[] preRotationCurrent; + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)) { + writer.append(firstBundle); + preRotationCurrent = Files.readAllBytes(current); + writer.append(secondBundle); + } + Path generations = root.resolve(StateArchiveHistoryCatalogV3.DIRECTORY) + .resolve("generations"); + Files.write(generations.resolve("catalog-99999999999999999999.bin"), new byte[]{1}); + try (StateArchiveFiveLaneSegmentWriterV3 reopened = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)) { + assertEquals(2, reopened.getAppendHead().getBlockNumber()); + } + Path manifest; + try (java.util.stream.Stream paths = Files.walk(root.resolve("segments"))) { + manifest = paths.filter(path -> path.getFileName().toString().endsWith(".manifest")) + .findFirst().orElseThrow(AssertionError::new); + } + Files.delete(manifest); + Files.write(current, preRotationCurrent); + try (StateArchiveFiveLaneSegmentWriterV3 recoveredPublication = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)) { + assertEquals(2, recoveredPublication.getAppendHead().getBlockNumber()); + } + + byte[] validCurrent = Files.readAllBytes(current); + byte[] corruptCurrent = validCurrent.clone(); + corruptCurrent[20] ^= 1; + Files.write(current, corruptCurrent); + assertThrows(IOException.class, () -> new StateArchiveFiveLaneSegmentWriterV3(root, + baseline, StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)); + Files.write(current, validCurrent); + + Files.delete(manifest); + assertThrows(IllegalArgumentException.class, + () -> new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)); + } + + private static List diffs(int first, int count, int parent) { + List result = new ArrayList<>(); + for (int block = first; block < first + count; block++) { + result.add(diff(block, block == first ? parent : block - 1, 8)); + } + return result; + } + + private static BlockReverseDiff diff(int block, int parent, int valueLength) { + byte[] value = new byte[valueLength]; + Arrays.fill(value, (byte) block); + List groups = valueLength == 0 ? Collections.emptyList() + : Collections.singletonList(new DbGroup("code", Collections.singletonList( + new Entry(new byte[]{(byte) (block & 7)}, OldValue.present(value))))); + return new BlockReverseDiff(new BlockSnapshotMeta(block, block, hash(block), + hash(parent), block * 3_000L), groups); + } + + private static CommonCheckpointTarget target(List diffs, int salt) { + return CommonCheckpointTarget.restore(hash(70), hash(80 + salt), + diffs.get(0).getMeta(), diffs.get(diffs.size() - 1).getMeta(), hash(90), hash(91)); + } + + private static byte[] hash(int value) { + byte[] result = new byte[32]; + result[27] = (byte) value; + result[31] = (byte) value; + return result; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3Test.java index 3bfa46413c3..7dcfbf3277d 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3Test.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveSegmentFormatV3Test.java @@ -13,6 +13,7 @@ import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.DurableMarker; import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SealedSegment; import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SegmentHeader; +import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SegmentManifest; import org.tron.core.db2.archive.StateArchiveSegmentFormatV3.SegmentSeal; public class StateArchiveSegmentFormatV3Test { @@ -107,6 +108,40 @@ public void roundTripsExactSealedSegmentMapRecordAndRejectsInvalidRanges() { () -> StateArchiveSegmentFormatV3.decodeSealedMapRecord(invalid)); } + @Test + public void roundTripsFrozenSealedManifestAndRejectsEveryTrailerAuthority() { + SegmentManifest input = new SegmentManifest(22, 7, 100, 102, 3, 9, + 300, 2_000, 2_880, 224, hash(1), hash(2)); + byte[] encoded = StateArchiveSegmentFormatV3.encodeManifest(input); + assertEquals(304, encoded.length); + ByteBuffer bytes = ByteBuffer.wrap(encoded); + assertEquals(StateArchiveFileFormatV3.SEGMENT_MANIFEST_MAGIC, bytes.getInt(0)); + assertEquals(256, bytes.getInt(8)); + assertEquals(304, bytes.getLong(16)); + assertEquals(22, Short.toUnsignedInt(bytes.getShort(26))); + assertEquals(7, bytes.getLong(32)); + assertEquals(100, bytes.getLong(40)); + assertEquals(102, bytes.getLong(48)); + assertArrayEquals(StateArchiveFileFormatV3.fiveLaneDescriptorDigest(), + slice(encoded, 104, 32)); + assertArrayEquals(StateArchiveFileFormatV3.compositeFormatDigest(), + slice(encoded, 136, 32)); + + SegmentManifest decoded = StateArchiveSegmentFormatV3.decodeManifest(encoded); + assertEquals(7, decoded.getSegmentSeq()); + assertEquals(9, decoded.getEntryCount()); + assertArrayEquals(hash(1), decoded.getPreviousSegmentDigest()); + assertArrayEquals(hash(2), decoded.getFinalHistoryDigest()); + assertArrayEquals(slice(encoded, 256, 32), decoded.getManifestDigest()); + + for (int offset : new int[]{232, 260, 291, 299, 303}) { + byte[] corrupt = encoded.clone(); + corrupt[offset] ^= 1; + assertThrows(IllegalArgumentException.class, + () -> StateArchiveSegmentFormatV3.decodeManifest(corrupt)); + } + } + @Test public void freezesSealDomainSegmentChainAndLaneBaselines() { SegmentSeal input = new SegmentSeal(0, 0, 10, 11, 2, 3, diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveServingIndexSpeedTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveServingIndexSpeedTest.java new file mode 100644 index 00000000000..8a74a749f6b --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveServingIndexSpeedTest.java @@ -0,0 +1,82 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.BlockReverseDiff.DbGroup; +import org.tron.core.db2.archive.BlockReverseDiff.Entry; +import org.tron.core.db2.archive.StateArchiveServingIndexBuildCoordinatorV3.BuildProgress; +import org.tron.core.db2.archive.StateArchiveServingIndexBuildCoordinatorV3.LiveServingIndexer; +import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +/** Bounded mechanism benchmark; it records throughput without imposing a host-sensitive gate. */ +public class StateArchiveServingIndexSpeedTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void measuresBulkThenLiveDurablePublication() throws Exception { + Path root = temporaryFolder.newFolder("serving-speed").toPath(); + List bulk = diffs(1, 1_000); + long bulkStart = System.nanoTime(); + long liveNanos = 0; + try (StateArchiveServingIndexBuildCoordinatorV3 coordinator = + new StateArchiveServingIndexBuildCoordinatorV3(root, Engine.LEVELDB, 1_000)) { + BuildProgress bulkProgress = coordinator.offerCommittedRange(bulk, + target(bulk, 1)); + long bulkEnd = System.nanoTime(); + assertEquals(1_000, bulkProgress.getIndexedThrough()); + LiveServingIndexer live = coordinator.completeInitialSync(target(bulk, 1)); + for (int block = 1_001; block <= 1_020; block++) { + List one = diffs(block, 1); + long started = System.nanoTime(); + live.indexNow(one, target(one, block)); + liveNanos += System.nanoTime() - started; + } + assertEquals(1_020, coordinator.status().getIndexedThrough()); + double bulkSeconds = (bulkEnd - bulkStart) / 1_000_000_000.0; + double liveMillis = liveNanos / 20.0 / 1_000_000.0; + System.out.printf("STATE_ARCHIVE_SERVING_SPEED bulk_blocks=1000 bulk_seconds=%.6f " + + "bulk_blocks_per_second=%.2f live_blocks=20 live_mean_ms=%.3f%n", + bulkSeconds, 1_000.0 / bulkSeconds, liveMillis); + assertTrue(bulkSeconds > 0); + assertTrue(liveMillis > 0); + } + } + + private static List diffs(int first, int count) { + List result = new ArrayList<>(); + for (int block = first; block < first + count; block++) { + BlockSnapshotMeta meta = new BlockSnapshotMeta(block, block, hash(block), + hash(block - 1), block * 3_000L); + DbGroup group = new DbGroup("code", Collections.singletonList( + new Entry(new byte[]{(byte) (block & 31)}, OldValue.absent()))); + result.add(new BlockReverseDiff(meta, Collections.singletonList(group))); + } + return result; + } + + private static CommonCheckpointTarget target(List diffs, int salt) { + return CommonCheckpointTarget.restore(hash(70), hash(80 + salt), + diffs.get(0).getMeta(), diffs.get(diffs.size() - 1).getMeta(), hash(90), hash(91)); + } + + private static byte[] hash(int value) { + byte[] result = new byte[32]; + result[24] = (byte) (value >>> 24); + result[25] = (byte) (value >>> 16); + result[26] = (byte) (value >>> 8); + result[27] = (byte) value; + result[31] = (byte) value; + return result; + } +} From a0282e02ea5ff56162f353dea46d9ea9b2e96342 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 16:01:23 +0800 Subject: [PATCH 133/161] fix(chainbase): seal previously marked segments --- .../StateArchiveFiveLaneSegmentWriterV3.java | 5 +++- ...ateArchiveFiveLaneSegmentWriterV3Test.java | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java index c7707634a7e..95fe3473df1 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java @@ -271,7 +271,10 @@ private void append(EncodedBundle bundle, long checkpointSequence, LaneState state = lanes.get(lane.getLaneId()); if (state != null && StateArchiveSegmentFormatV3.shouldRotate( state.blockFrameCount, state.dataEndOffset, rotationTargetBytes)) { - if (checkpointSequence >= 0) { + // A fully marked segment belongs to the preceding checkpoint. Seal it without + // adding its old marker to the new checkpoint's durability proof. + if (checkpointSequence >= 0 + && state.markedBlockFrameCount < state.blockFrameCount) { addPendingTail(markRotation(state, checkpointSequence, commonTargetDigest, faultHook)); } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java index 37beb9d75bc..771fe0892d9 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java @@ -87,6 +87,33 @@ public void independentlyRotatesOvershotLaneAndReopensCompleteBundle() throws Ex } } + @Test + public void rotatesPreviouslyPublishedTailAtNextCheckpointBoundary() throws Exception { + Path root = temporaryFolder.newFolder("published-tail-rotation").toPath(); + byte[] baseline = hash(89); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + EncodedBundle first = codec.encode(diff(1, 1_400), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE); + EncodedBundle second = codec.encode(diff(2, 0), first.getResultHistoryDigest(), + StateArchiveFileFormatV3.COMPRESSION_NONE); + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)) { + writer.appendForCheckpoint(first, 11, hash(109)); + writer.sync(11, point(first), hash(109)); + writer.appendForCheckpoint(second, 12, hash(110)); + ArchiveDurabilityProof proof = writer.sync(12, point(second), hash(110)); + assertEquals(5, proof.getFileTails().size()); + assertFalse(writer.getSealedSegments().isEmpty()); + } + try (StateArchiveFiveLaneSegmentWriterV3 reopened = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 1_500)) { + assertEquals(2, reopened.getAppendHead().getBlockNumber()); + assertEquals(2, reopened.readCommittedDiffs(0, 2).size()); + } + } + @Test public void provesSealedAndCurrentTailsAcrossIndependentRotation() throws Exception { Path root = temporaryFolder.newFolder("five-lane-rotation-proof").toPath(); From 799edd403c28cf5106e52bf9529ad39e73ae3e22 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 16:25:36 +0800 Subject: [PATCH 134/161] perf(chainbase): bound archive recovery reads --- .../StateArchiveFiveLaneSegmentWriterV3.java | 129 ++++++++++++++---- ...ateArchiveFiveLaneSegmentWriterV3Test.java | 70 ++++++++++ 2 files changed, 169 insertions(+), 30 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java index 95fe3473df1..ff8ddf788c1 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java @@ -17,7 +17,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Objects; +import java.util.TreeMap; import java.util.stream.Collectors; import java.util.stream.Stream; import org.tron.core.db2.archive.StateArchiveFiveLaneBlockCodecV3.DecodedBundle; @@ -71,6 +73,9 @@ public final class StateArchiveFiveLaneSegmentWriterV3 implements AutoCloseable private final List pendingRotationTails = new ArrayList<>(); private long activeCheckpointSequence = -1; private byte[] activeCommonTargetDigest; + private Map> servingSegments; + private long servingReadFrames; + private long servingReadBytes; public StateArchiveFiveLaneSegmentWriterV3(Path archiveRoot, byte[] baselineHistoryDigest, short compressionId) throws IOException { @@ -336,41 +341,48 @@ public synchronized List readCommittedDiffs(long fromExclusive || through > appendHead.getBlockNumber()) { throw new IllegalArgumentException("Invalid State Archive committed read range"); } - Map> bundles = new java.util.TreeMap<>(); - for (Path path : listDataFiles(false)) { - try (FileChannel data = FileChannel.open(path, StandardOpenOption.READ)) { - long offset = StateArchiveFileFormatV3.PART_HEADER_LENGTH; - while (offset < data.size()) { - byte[] envelope = readExact(data, offset, - StateArchiveFileFormatV3.FRAME_ENVELOPE_LENGTH); - ByteBuffer fields = ByteBuffer.wrap(envelope); - if (fields.getInt(0) != StateArchiveFileFormatV3.FRAME_MAGIC) { - throw new IOException("State Archive serving source frame magic mismatch"); - } - short frameType = fields.getShort(8); - long length = fields.getLong(16); - if (length <= 0 || length > Integer.MAX_VALUE || length > data.size() - offset) { - throw new IOException("State Archive serving source frame length mismatch"); - } - byte[] frame = readExact(data, offset, (int) length); - if (frameType == StateArchiveFileFormatV3.BLOCK_FRAME_TYPE) { - long block = blockNumber(frame); - if (block > fromExclusive && block <= through) { - int laneId = laneIdFromFrame(frame); - byte[] previous = bundles.computeIfAbsent(block, ignored -> new HashMap<>()) - .put(laneId, frame); - if (previous != null) { - throw new IOException("Duplicate State Archive serving source lane frame"); - } - } - } - offset += length; + servingReadFrames = 0; + servingReadBytes = 0; + if (fromExclusive == through) { + return Collections.emptyList(); + } + Map> bundles = new TreeMap<>(); + if (servingSegments == null) { + servingSegments = new HashMap<>(); + for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { + servingSegments.put(laneId, new TreeMap<>()); + } + for (SealedSegment segment : sealedSegments) { + servingSegments.get(segment.getLaneId()).put(segment.getFirstBlock(), segment); + } + } + long first = fromExclusive + 1; + for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { + NavigableMap segments = servingSegments.get(laneId); + Map.Entry selected = segments.floorEntry(first); + if (selected == null) { + selected = segments.ceilingEntry(first); + } + while (selected != null && selected.getKey() <= through) { + SealedSegment segment = selected.getValue(); + if (segment.getLastBlock() >= first) { + readIndexedRange(laneId, segment.getSegmentSeq(), segment.getFirstBlock(), + Math.max(first, segment.getFirstBlock()), + Math.min(through, segment.getLastBlock()), segment.getSegmentHeaderDigest(), + bundles); } + selected = segments.higherEntry(selected.getKey()); + } + LaneState current = lanes.get(laneId); + if (current != null && current.firstBlock <= through && current.lastBlock >= first) { + readIndexedRange(laneId, current.segmentSeq, current.firstBlock, + Math.max(first, current.firstBlock), Math.min(through, current.lastBlock), + current.headerDigest, bundles); } } List result = new ArrayList<>(); for (long block = fromExclusive + 1; block <= through; block++) { - Map laneFrames = bundles.get(block); + Map laneFrames = bundles.remove(block); if (laneFrames == null || laneFrames.size() != StateArchiveFileFormatV3.fiveLaneIds().length) { throw new IOException("Incomplete State Archive serving source bundle"); @@ -384,6 +396,61 @@ public synchronized List readCommittedDiffs(long fromExclusive return Collections.unmodifiableList(result); } + synchronized long getServingReadFrames() { + return servingReadFrames; + } + + synchronized long getServingReadBytes() { + return servingReadBytes; + } + + private void readIndexedRange(int laneId, long sequence, long segmentFirst, + long first, long last, byte[] headerDigest, + Map> bundles) throws IOException { + try (FileChannel data = FileChannel.open(dataPath(laneId, sequence), + StandardOpenOption.READ); + FileChannel index = FileChannel.open(indexPath(laneId, sequence), + StandardOpenOption.READ)) { + BlockIndexHeader header = StateArchiveSegmentFormatV3.decodeBlockIndexHeader( + readExact(index, 0, StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH)); + if (header.getLaneId() != laneId || header.getSegmentSeq() != sequence + || !Arrays.equals(headerDigest, header.getDataSegmentHeaderDigest())) { + throw new IOException("State Archive serving block index identity mismatch"); + } + long dataBytes = data.size(); + int entryBytes = StateArchiveFileFormatV3.BLOCK_INDEX_ENTRY_LENGTH; + byte[] entries = null; + for (long block = first; block <= last; block++) { + int entryInBatch = (int) ((block - first) % 256); + if (entryInBatch == 0) { + long offset = Math.addExact(StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH, + Math.multiplyExact(block - segmentFirst, entryBytes)); + int count = (int) Math.min(256, last - block + 1); + entries = readExact(index, offset, count * entryBytes); + } + BlockIndexEntry entry = StateArchiveSegmentFormatV3.decodeBlockIndexEntry( + Arrays.copyOfRange(entries, entryInBatch * entryBytes, + (entryInBatch + 1) * entryBytes)); + if (entry.getBlockNumber() != block + || entry.getFrameOffset() > dataBytes - entry.getFrameLength()) { + throw new IOException("State Archive serving block index range mismatch"); + } + byte[] frame = readExact(data, entry.getFrameOffset(), entry.getFrameLength()); + servingReadFrames++; + servingReadBytes += frame.length; + if (blockNumber(frame) != block || laneIdFromFrame(frame) != laneId + || ByteBuffer.wrap(frame).getLong(frame.length - ENCODED_DIGEST_FROM_END) + != entry.getEncodedFrameDigestPrefix()) { + throw new IOException("State Archive serving block index frame mismatch"); + } + if (bundles.computeIfAbsent(block, ignored -> new HashMap<>()).put(laneId, frame) + != null) { + throw new IOException("Duplicate State Archive serving source lane frame"); + } + } + } + } + private static int laneIdFromFrame(byte[] frame) throws IOException { long coverage = ByteBuffer.wrap(frame).getLong(COVERAGE_BITMAP_OFFSET); for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { @@ -761,6 +828,7 @@ private void seal(LaneState state) throws IOException { } private void reopen(Long recoveryBoundary) throws IOException { + servingSegments = null; List dataFiles = listDataFiles(recoveryBoundary != null || activeRecoveryIntent != null); if (dataFiles.isEmpty()) { resultHistoryDigest = Arrays.copyOf(baselineHistoryDigest, @@ -1447,6 +1515,7 @@ private List listDataFiles(boolean recovering) throws IOException { } private void publishCatalog() throws IOException { + servingSegments = null; catalog.publish(rotationTargetBytes, getCurrentSegments(), getSealedSegments()); structuralChanged = false; } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java index 771fe0892d9..ad4424023b2 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3Test.java @@ -87,6 +87,76 @@ public void independentlyRotatesOvershotLaneAndReopensCompleteBundle() throws Ex } } + @Test + public void indexedRangesReadOnlyRequestedFramesAcrossGrowingHistory() throws Exception { + Path root = temporaryFolder.newFolder("bounded-serving-read").toPath(); + byte[] baseline = hash(89); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 800_000)) { + byte[] previous = baseline; + long shortHistoryBytes = 0; + for (int block = 1; block <= 2_048; block++) { + EncodedBundle bundle = codec.encode(diff(block, 1_400), previous, + StateArchiveFileFormatV3.COMPRESSION_NONE); + writer.appendForCheckpoint(bundle, 1, hash(109)); + previous = bundle.getResultHistoryDigest(); + if (block == 64) { + assertEquals(8, writer.readCommittedDiffs(56, 64).size()); + assertEquals(40, writer.getServingReadFrames()); + shortHistoryBytes = writer.getServingReadBytes(); + } + } + long start = System.nanoTime(); + long totalFrames = 0; + for (int first = 0; first < 2_048; first += 64) { + List read = writer.readCommittedDiffs(first, first + 64); + assertEquals(64, read.size()); + assertEquals(320, writer.getServingReadFrames()); + totalFrames += writer.getServingReadFrames(); + for (int offset = 0; offset < read.size(); offset++) { + assertArrayEquals(new BlockHistoryCodec().encode(diff(first + offset + 1, 1_400)), + new BlockHistoryCodec().encode(read.get(offset))); + } + } + assertEquals(10_240, totalFrames); + assertEquals(1_000, writer.readCommittedDiffs(1_000, 2_000).size()); + assertEquals(5_000, writer.getServingReadFrames()); + assertEquals(8, writer.readCommittedDiffs(2_040, 2_048).size()); + assertEquals(40, writer.getServingReadFrames()); + assertEquals(shortHistoryBytes, writer.getServingReadBytes()); + // Out-of-range data is neither opened nor read, even when that segment is unavailable. + Files.move(segment(root, 0, ".dat"), root.resolve("unrelated-segment.dat")); + assertEquals(8, writer.readCommittedDiffs(2_040, 2_048).size()); + System.out.printf("SERVING_RANGE_READ blocks=2048 batches=32 frames=%d seconds=%.6f%n", + totalFrames, (System.nanoTime() - start) / 1_000_000_000.0); + } + } + + @Test + public void indexedRangesRejectCorruptSelectedIndexWithoutScanningData() throws Exception { + Path root = temporaryFolder.newFolder("corrupt-serving-index").toPath(); + byte[] baseline = hash(89); + StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); + try (StateArchiveFiveLaneSegmentWriterV3 writer = + new StateArchiveFiveLaneSegmentWriterV3(root, baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE, 80_000)) { + writer.append(codec.encode(diff(1, 1_400), baseline, + StateArchiveFileFormatV3.COMPRESSION_NONE)); + assertEquals(1, writer.readCommittedDiffs(0, 1).size()); + try (FileChannel index = FileChannel.open(segment(root, 0, ".bidx"), + StandardOpenOption.WRITE)) { + ByteBuffer wrongBlock = ByteBuffer.allocate(8); + wrongBlock.putLong(2).flip(); + index.write(wrongBlock, StateArchiveFileFormatV3.BLOCK_INDEX_HEADER_LENGTH); + } + assertThrows(java.io.IOException.class, () -> writer.readCommittedDiffs(0, 1)); + assertEquals(0, writer.getServingReadFrames()); + assertTrue(writer.readCommittedDiffs(1, 1).isEmpty()); + } + } + @Test public void rotatesPreviouslyPublishedTailAtNextCheckpointBoundary() throws Exception { Path root = temporaryFolder.newFolder("published-tail-rotation").toPath(); From 5a58f3fd298fe0decf81e7657bec2df9b3d8fae0 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 16:27:23 +0800 Subject: [PATCH 135/161] feat(chainbase): materialize p66 snapshot --- .../core/db2/archive/BlockChangeView.java | 12 +- .../PhysicalSnapshotPathStateCollector.java | 35 ++ .../core/db2/core/CommonCheckpointFormat.java | 6 + .../core/P66CoupledMutationMaterializer.java | 120 ++++ .../tron/core/db2/core/SnapshotManager.java | 147 ++++- .../org/tron/core/db2/core/SnapshotRoot.java | 11 +- .../tron/core/store/AccountAssetStore.java | 99 +++- .../org/tron/core/config/args/Storage.java | 4 + .../tron/core/config/args/StorageConfig.java | 5 + common/src/main/resources/reference.conf | 2 + .../core/config/args/StorageConfigTest.java | 14 + .../java/org/tron/core/config/args/Args.java | 1 + .../main/java/org/tron/core/db/Manager.java | 23 +- .../P66SnapshotManagerIntegrationTest.java | 47 ++ .../db2/core/P66SnapshotPipelineTest.java | 545 ++++++++++++++++++ 15 files changed, 1043 insertions(+), 28 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/PhysicalSnapshotPathStateCollector.java create mode 100644 chainbase/src/main/java/org/tron/core/db2/core/P66CoupledMutationMaterializer.java create mode 100644 framework/src/test/java/org/tron/core/db2/core/P66SnapshotManagerIntegrationTest.java create mode 100644 framework/src/test/java/org/tron/core/db2/core/P66SnapshotPipelineTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java b/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java index 0e7e11ebfbb..86997922cae 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/BlockChangeView.java @@ -5,8 +5,11 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import org.tron.core.db2.archive.BlockSnapshotMeta; import org.tron.core.db2.common.Value; +import org.tron.core.db2.common.WrappedByteArray; import org.tron.core.db2.core.Chainbase; import org.tron.core.db2.core.Snapshot; import org.tron.core.db2.core.SnapshotImpl; @@ -61,6 +64,8 @@ public List getDatabases() { public static final class DatabaseChanges { private final String dbName; private final Snapshot previous; + private final ConcurrentMap previousValues = new ConcurrentHashMap<>(); private final List changes; private DatabaseChanges(String dbName, Snapshot previous, List changes) { @@ -74,7 +79,12 @@ public String getDbName() { } public byte[] getPrevious(byte[] key) { - return previous.get(key); + PostValue value = previousValues.computeIfAbsent( + WrappedByteArray.copyOf(key), ignored -> { + byte[] bytes = previous.get(key); + return bytes == null ? PostValue.absent() : PostValue.present(bytes); + }); + return value.isPresent() ? value.getValue() : null; } public List getChanges() { diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/PhysicalSnapshotPathStateCollector.java b/chainbase/src/main/java/org/tron/core/db2/archive/PhysicalSnapshotPathStateCollector.java new file mode 100644 index 00000000000..8990d5d40c4 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/PhysicalSnapshotPathStateCollector.java @@ -0,0 +1,35 @@ +package org.tron.core.db2.archive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.tron.core.db2.stateroot.PathStateBlockTransition; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateMutation; +import org.tron.core.db2.stateroot.PathStateTransitionCollector; + +/** Consumes exact physical mutations after P66 materialization; no projection or scans. */ +public final class PhysicalSnapshotPathStateCollector implements PathStateTransitionCollector { + @Override + public PathStateBlockTransition collect(BlockChangeView view) { + boolean enabled = SnapshotOldValueCollector.resolveTargetAssetOptimization(view); + List mutations = new ArrayList<>(); + for (BlockChangeView.DatabaseChanges database : view.getDatabases()) { + for (BlockChangeView.Change change : database.getChanges()) { + byte[] key = change.getKey(); + byte[] oldValue = database.getPrevious(key); + byte[] value = change.getPostValue().isPresent() + ? change.getPostValue().getValue() : null; + if (!Arrays.equals(oldValue, value)) { + mutations.add((value == null ? PathStateMutation.delete(database.getDbName(), key) + : PathStateMutation.put(database.getDbName(), key, value)) + .withPreviousPhysicalValue(oldValue)); + } + } + } + BlockSnapshotMeta meta = view.getMeta(); + return new PathStateBlockTransition(meta.getBlockNumber(), meta.getBlockHash(), + meta.getParentHash(), meta.getTimestamp(), enabled ? P66Phase.P66_ON : P66Phase.P66_OFF, + mutations); + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java index 32ad2c01789..efb180d94c3 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java @@ -14,6 +14,12 @@ public final class CommonCheckpointFormat { private CommonCheckpointFormat() { } + public static byte[] identity(boolean physicalSnapshot) { + return physicalSnapshot ? Hashing.sha256() + .hashString(ID + "/p66-physical-snapshot-v1", StandardCharsets.UTF_8).asBytes() + : identity(); + } + public static byte[] identity() { return Arrays.copyOf(DIGEST, DIGEST.length); } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/P66CoupledMutationMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/core/P66CoupledMutationMaterializer.java new file mode 100644 index 00000000000..d557a4a1403 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/core/P66CoupledMutationMaterializer.java @@ -0,0 +1,120 @@ +package org.tron.core.db2.core; + +import com.google.common.primitives.Bytes; +import com.google.common.primitives.Longs; +import com.google.common.primitives.UnsignedBytes; +import com.google.protobuf.InvalidProtocolBufferException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.tron.core.db2.common.Key; +import org.tron.core.db2.common.Value; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.protos.Protocol.Account; + +/** Materializes coupled physical writes once, inside the active revocable block layer. */ +public final class P66CoupledMutationMaterializer { + private static final byte[] FLAG = + "ALLOW_ASSET_OPTIMIZATION".getBytes(StandardCharsets.US_ASCII); + private final Chainbase accounts; + private final Chainbase assets; + private final Chainbase properties; + + public P66CoupledMutationMaterializer(Chainbase accounts, Chainbase assets, + Chainbase properties) { + this.accounts = accounts; + this.assets = assets; + this.properties = properties; + } + + public Statistics materialize() { + if (!Snapshot.isImpl(accounts.getHead()) || !Snapshot.isImpl(assets.getHead()) + || !Snapshot.isImpl(properties.getHead())) { + throw new IllegalStateException("P66 materialization requires active Snapshot layers"); + } + Statistics stats = new Statistics(); + byte[] flag = properties.getUnchecked(FLAG); + if (flag == null || flag.length != Long.BYTES + || (Longs.fromByteArray(flag) != 0 && Longs.fromByteArray(flag) != 1)) { + throw new IllegalStateException("P66 post-state property must be 0 or 1"); + } + byte[] previous = properties.getHead().getPrevious().get(FLAG); + if (previous != null && previous.length == Long.BYTES + && Longs.fromByteArray(previous) == 1 && Longs.fromByteArray(flag) == 0) { + throw new IllegalStateException("P66 phase cannot move backwards"); + } + if (Longs.fromByteArray(flag) == 0) { + return stats; + } + // Build the complete deterministic plan before modifying either Snapshot map. + Map accountPlan = new TreeMap<>(UnsignedBytes.lexicographicalComparator()); + Map assetPlan = new TreeMap<>(UnsignedBytes.lexicographicalComparator()); + List> changed = new ArrayList<>(); + ((SnapshotImpl) accounts.getHead()).getDb().forEach(changed::add); + for (Map.Entry entry : changed) { + byte[] address = entry.getKey().getBytes(); + if (address.length != 21) { + throw new IllegalStateException("P66 Account key must be exactly 21 bytes"); + } + stats.changedAccounts++; + if (entry.getValue().getOperator() == Value.Operator.DELETE) { + stats.prefixQueries++; + Map rows = assets.prefixQuery(address); + stats.prefixRows += rows.size(); + rows.forEach((key, value) -> assetPlan.put(key.getBytes(), null)); + continue; + } + Account account; + try { + account = Account.parseFrom(entry.getValue().getBytes()); + } catch (InvalidProtocolBufferException invalid) { + throw new IllegalStateException("Invalid P66 Account bytes", invalid); + } + if (!Arrays.equals(address, account.getAddress().toByteArray())) { + throw new IllegalStateException("P66 Account key/address mismatch"); + } + if (account.getAssetV2Map().isEmpty()) { + stats.emptyAssetSkips++; + } + boolean migrated = false; + for (Map.Entry asset : account.getAssetV2Map().entrySet()) { + byte[] key = Bytes.concat(address, asset.getKey().getBytes(StandardCharsets.UTF_8)); + long value = asset.getValue(); + if (!account.getAssetOptimized() && value == 0) { + continue; + } + assetPlan.put(key, value == 0 ? null : Longs.toByteArray(value)); + migrated |= !account.getAssetOptimized() && value != 0; + } + if (migrated) { + stats.migratedAccounts++; + } + accountPlan.put(address, account.toBuilder().clearAsset().clearAssetV2() + .setAssetOptimized(true).build().toByteArray()); + } + assetPlan.forEach((key, value) -> { + if (value == null) { + assets.delete(key); + stats.assetDeletes++; + } else { + assets.put(key, value); + stats.assetPuts++; + } + }); + accountPlan.forEach(accounts::put); + return stats; + } + + public static final class Statistics { + public long changedAccounts; + public long migratedAccounts; + public long emptyAssetSkips; + public long assetPuts; + public long assetDeletes; + public long prefixQueries; + public long prefixRows; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index ecfb4adf660..376f55c8988 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -20,9 +20,12 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -53,14 +56,14 @@ import org.tron.core.db2.archive.DurableHistoryMarkerRangeEvidence; import org.tron.core.db2.archive.HistoryCommitMarker; import org.tron.core.db2.archive.OldValueCollector; -import org.tron.core.db2.stateroot.PathStateBlockTransition; -import org.tron.core.db2.stateroot.PathStateRuntimeAttachment; -import org.tron.core.db2.stateroot.PathStateSnapshotDelta; import org.tron.core.db2.common.DB; import org.tron.core.db2.common.IRevokingDB; import org.tron.core.db2.common.Key; import org.tron.core.db2.common.Value; import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.stateroot.PathStateBlockTransition; +import org.tron.core.db2.stateroot.PathStateRuntimeAttachment; +import org.tron.core.db2.stateroot.PathStateSnapshotDelta; import org.tron.core.exception.RevokingStoreIllegalStateException; import org.tron.core.exception.TronError; import org.tron.core.store.CheckPointV2Store; @@ -70,6 +73,35 @@ public class SnapshotManager implements RevokingDatabase { public static final int DEFAULT_MIN_FLUSH_COUNT = 1; + private P66CoupledMutationMaterializer p66Materializer; + private ExecutorService artifactExecutor; + + public synchronized void installP66SnapshotLane(Chainbase assets) { + if (size != 0 || activeSession != 0 || p66Materializer != null + || dbs.stream().anyMatch(db -> "account-asset".equals(db.getDbName()))) { + throw new IllegalStateException("P66 Snapshot lane must be installed once before sessions"); + } + Chainbase accounts = requireDatabase("account"); + Chainbase properties = requireDatabase("properties"); + if (!Snapshot.isRoot(accounts.getHead()) || !Snapshot.isRoot(assets.getHead())) { + throw new IllegalStateException("P66 Snapshot installation requires root heads"); + } + add(assets); + ((SnapshotRoot) accounts.getHead()).useMaterializedCoupledMutations(); + p66Materializer = new P66CoupledMutationMaterializer(accounts, assets, properties); + artifactExecutor = new ThreadPoolExecutor(1, 1, 0L, + TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1), task -> { + Thread thread = new Thread(task, "block-final-archive"); + thread.setDaemon(true); + return thread; + }); + } + + private Chainbase requireDatabase(String name) { + return dbs.stream().filter(db -> name.equals(db.getDbName())).findFirst() + .orElseThrow(() -> new IllegalStateException("Missing P66 participant: " + name)); + } + private static final int DEFAULT_STACK_MAX_SIZE = 256; private static final long ONE_MINUTE_MILLS = 60*1000L; private static final String CHECKPOINT_V2_DIR = "checkpoint"; @@ -271,21 +303,91 @@ public synchronized void commit(BlockSnapshotMeta meta) { } } + if (p66Materializer != null) { + long p66Start = System.nanoTime(); + P66CoupledMutationMaterializer.Statistics stats = p66Materializer.materialize(); + logger.info("P66 Snapshot materialized: head={}, changedAccounts={}, migratedAccounts={}, " + + "emptyAssetSkips={}, assetPuts={}, assetDeletes={}, prefixQueries={}, " + + "prefixRows={}, activationAccountScans=0, duplicateRootMigrations=0, " + + "materializeMs={}", meta.getBlockNumber(), stats.changedAccounts, + stats.migratedAccounts, stats.emptyAssetSkips, stats.assetPuts, stats.assetDeletes, + stats.prefixQueries, stats.prefixRows, elapsedMillis(p66Start, System.nanoTime())); + } BlockChangeView changeView = null; if (oldValueCollector != null || pathStateRuntimeAttachment != null) { changeView = BlockChangeView.capture(meta, dbs); } long frozenNanos = System.nanoTime(); - BlockReverseDiff reverseDiff = null; - if (oldValueCollector != null) { - reverseDiff = Objects.requireNonNull( - oldValueCollector.collect(changeView), - "archive collector returned null"); - } - long archiveNanos = System.nanoTime(); - PathStateBlockTransition pathStateTransition = pathStateRuntimeAttachment == null ? null - : pathStateRuntimeAttachment.capture(changeView); - long pathNanos = System.nanoTime(); + BlockReverseDiff reverseDiff; + PathStateBlockTransition pathStateTransition; + long archiveNanos; + long pathNanos; + if (p66Materializer != null && oldValueCollector != null + && pathStateRuntimeAttachment != null) { + BlockChangeView frozen = changeView; + long parallelStart = System.nanoTime(); + long[] archiveElapsed = new long[1]; + Future archive = artifactExecutor.submit(() -> { + long start = System.nanoTime(); + try { + return Objects.requireNonNull(oldValueCollector.collect(frozen), + "archive collector returned null"); + } finally { + archiveElapsed[0] = System.nanoTime() - start; + } + }); + long pathStart = System.nanoTime(); + Throwable pathFailure = null; + pathStateTransition = null; + try { + pathStateTransition = Objects.requireNonNull(pathStateRuntimeAttachment.capture(frozen), + "PathState capture failed before block-final barrier"); + } catch (Throwable failure) { + pathFailure = failure; + } + long pathElapsed = System.nanoTime() - pathStart; + // Never cancel-and-revoke: the other reader must finish before releasing the Snapshot. + boolean interrupted = false; + Throwable archiveFailure = null; + reverseDiff = null; + while (true) { + try { + reverseDiff = archive.get(); + break; + } catch (InterruptedException failure) { + interrupted = true; + } catch (ExecutionException failure) { + archiveFailure = failure.getCause(); + break; + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + if (pathFailure != null || archiveFailure != null || interrupted) { + Throwable failure = pathFailure != null ? pathFailure : archiveFailure; + IllegalStateException rejected = new IllegalStateException( + "Block-final parallel barrier failed", failure); + if (pathFailure != null && archiveFailure != null) { + rejected.addSuppressed(archiveFailure); + } + pathStateRuntimeAttachment.fail(rejected); + throw rejected; + } + archiveNanos = System.nanoTime(); + pathNanos = archiveNanos; + logger.info("Block-final parallel barrier: head={}, archiveMs={}, pathMs={}, wallMs={}, " + + "archiveP66Projection=0, pathP66Projection=0", meta.getBlockNumber(), + TimeUnit.NANOSECONDS.toMillis(archiveElapsed[0]), + TimeUnit.NANOSECONDS.toMillis(pathElapsed), elapsedMillis(parallelStart, pathNanos)); + } else { + reverseDiff = oldValueCollector == null ? null : Objects.requireNonNull( + oldValueCollector.collect(changeView), "archive collector returned null"); + archiveNanos = System.nanoTime(); + pathStateTransition = pathStateRuntimeAttachment == null ? null + : pathStateRuntimeAttachment.capture(changeView); + pathNanos = System.nanoTime(); + } PathStateSnapshotDelta pathStateDelta = pathStateTransition == null ? null : pathStateRuntimeAttachment.preparedSnapshotDelta(pathStateTransition); @@ -301,12 +403,21 @@ public synchronized void commit(BlockSnapshotMeta meta) { stateDatabase ? reverseDiff : null, stateDatabase ? pathStateDelta : null); } long attachedNanos = System.nanoTime(); - --activeSession; + if (p66Materializer == null) { + --activeSession; + } if (pathStateRuntimeAttachment != null) { pathStateRuntimeAttachment.publish(pathStateTransition); + if (p66Materializer != null && pathStateRuntimeAttachment.getFailure() != null) { + throw new IllegalStateException("Block-final PathState publication failed", + pathStateRuntimeAttachment.getFailure()); + } + } + if (p66Materializer != null) { + --activeSession; } long completedNanos = System.nanoTime(); - if (changeView != null) { + if (changeView != null && p66Materializer == null) { logger.info("Block-final artifact stages: head={}, freezeMs={}, archiveMs={}, " + "pathCaptureMs={}, attachMs={}, publishMs={}, totalMs={}", meta.getBlockNumber(), elapsedMillis(startedNanos, frozenNanos), @@ -328,6 +439,9 @@ public synchronized byte[] previewPathStateRoot(BlockSnapshotMeta meta) { if (pathStateRuntimeAttachment == null) { return null; } + if (p66Materializer != null) { + p66Materializer.materialize(); + } return pathStateRuntimeAttachment.preview(BlockChangeView.capture( Objects.requireNonNull(meta, "meta"), dbs)); } @@ -566,6 +680,9 @@ public synchronized void disable() { @Override public void shutdown() { Closeable legacyArchiveSink = prepareArchiveShutdown(); + if (artifactExecutor != null) { + ExecutorServiceManager.shutdownAndAwaitTermination(artifactExecutor, "block-final-archive"); + } ExecutorServiceManager.shutdownAndAwaitTermination(pruneCheckpointThread, pruneName); flushServices.forEach((key, value) -> ExecutorServiceManager.shutdownAndAwaitTermination(value, "flush-service-" + key)); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotRoot.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotRoot.java index 09ebab8fb1f..ced36906998 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotRoot.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotRoot.java @@ -26,6 +26,11 @@ public class SnapshotRoot extends AbstractSnapshot { @Getter private Snapshot solidity; private boolean isAccountDB; + private boolean coupledMutationsMaterialized; + + void useMaterializedCoupledMutations() { + coupledMutationsMaterialized = true; + } private TronCache cache; private static final List CACHE_DBS = CommonParameter.getInstance() @@ -42,7 +47,7 @@ public SnapshotRoot(DB db) { } private boolean needOptAsset() { - return isAccountDB && ChainBaseManager.getInstance().getDynamicPropertiesStore() + return isAccountDB && !coupledMutationsMaterialized && ChainBaseManager.getInstance().getDynamicPropertiesStore() .getAllowAccountAssetOptimizationFromRoot() == 1; } @@ -242,7 +247,9 @@ public String getDbName() { @Override public Snapshot newInstance() { - return new SnapshotRoot(db.newInstance()); + SnapshotRoot replacement = new SnapshotRoot(db.newInstance()); + replacement.coupledMutationsMaterialized = coupledMutationsMaterialized; + return replacement; } @Override diff --git a/chainbase/src/main/java/org/tron/core/store/AccountAssetStore.java b/chainbase/src/main/java/org/tron/core/store/AccountAssetStore.java index 6e970a136d3..a08ec6b122d 100644 --- a/chainbase/src/main/java/org/tron/core/store/AccountAssetStore.java +++ b/chainbase/src/main/java/org/tron/core/store/AccountAssetStore.java @@ -2,21 +2,98 @@ import com.google.common.primitives.Bytes; import com.google.common.primitives.Longs; +import java.util.HashMap; +import java.util.Map; import org.apache.commons.lang3.ArrayUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.tron.common.storage.leveldb.LevelDbDataSourceImpl; +import org.tron.common.storage.rocksdb.RocksDbDataSourceImpl; import org.tron.common.utils.ByteArray; import org.tron.core.db.TronDatabase; +import org.tron.core.db2.common.DB; +import org.tron.core.db2.common.LevelDB; +import org.tron.core.db2.common.RocksDB; import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.core.Chainbase; +import org.tron.core.db2.core.SnapshotManager; +import org.tron.core.db2.core.SnapshotRoot; import org.tron.protos.Protocol; -import java.util.HashMap; -import java.util.Map; - @Component public class AccountAssetStore extends TronDatabase { + private volatile Chainbase snapshots; + + /** Shares the existing native DB; registers before recovery and before any sessions exist. */ + public synchronized void enableSnapshots(SnapshotManager manager) { + if (snapshots != null) { + throw new IllegalStateException("AccountAsset Snapshot lane already attached"); + } + DB engine; + if (dbSource instanceof LevelDbDataSourceImpl) { + engine = new LevelDB( + (LevelDbDataSourceImpl) dbSource); + } else if (dbSource instanceof RocksDbDataSourceImpl) { + engine = new RocksDB( + (RocksDbDataSourceImpl) dbSource); + } else { + throw new IllegalStateException("Unsupported AccountAsset Snapshot engine"); + } + Chainbase lane = new Chainbase( + new SnapshotRoot(engine)); + lane.setRegistrationSource(AccountAssetStore.class.getName()); + manager.installP66SnapshotLane(lane); + snapshots = lane; + } + + @Override + public void close() { + if (snapshots != null) { + snapshots.close(); + } + super.close(); + } + + @Override + public Map prefixQuery(byte[] key) { + return snapshots == null ? super.prefixQuery(key) : snapshots.prefixQuery(key); + } + + @Override + public void updateByBatch(Map rows) { + if (snapshots == null) { + super.updateByBatch(rows); + } else { + rows.forEach((key, value) -> { + if (value == null) { + snapshots.delete(key); + } else { + snapshots.put(key, value); + } + }); + } + } + + @Override + public void updateByBatchSynced(Map rows) { + if (snapshots != null) { + throw new IllegalStateException("AccountAsset durability belongs to Common checkpoint"); + } + super.updateByBatchSynced(rows); + } + + @Override + public byte[] getFromRoot(byte[] key) { + return dbSource.getData(key); + } + + @Override + public byte[] getUnchecked(byte[] key) { + return get(key); + } + @Autowired protected AccountAssetStore(@Value("account-asset") String dbName) { super(dbName); @@ -24,22 +101,30 @@ protected AccountAssetStore(@Value("account-asset") String dbName) { @Override public void put(byte[] key, byte[] item) { - dbSource.putData(key, item); + if (snapshots == null) { + dbSource.putData(key, item); + } else { + snapshots.put(key, item); + } } @Override public void delete(byte[] key) { - dbSource.deleteData(key); + if (snapshots == null) { + dbSource.deleteData(key); + } else { + snapshots.delete(key); + } } @Override public byte[] get(byte[] key) { - return dbSource.getData(key); + return snapshots == null ? dbSource.getData(key) : snapshots.getUnchecked(key); } @Override public boolean has(byte[] key) { - return dbSource.getData(key) != null; + return get(key) != null; } public void putAccount(Protocol.Account account) { diff --git a/common/src/main/java/org/tron/core/config/args/Storage.java b/common/src/main/java/org/tron/core/config/args/Storage.java index 5306581adee..364779f85c2 100644 --- a/common/src/main/java/org/tron/core/config/args/Storage.java +++ b/common/src/main/java/org/tron/core/config/args/Storage.java @@ -121,6 +121,10 @@ public class Storage { @Setter private boolean commonCheckpointEnabled; + @Getter + @Setter + private boolean p66SnapshotEnabled; + @Getter @Setter private String commonCheckpointDirectory; diff --git a/common/src/main/java/org/tron/core/config/args/StorageConfig.java b/common/src/main/java/org/tron/core/config/args/StorageConfig.java index ff360960e72..46ab3134125 100644 --- a/common/src/main/java/org/tron/core/config/args/StorageConfig.java +++ b/common/src/main/java/org/tron/core/config/args/StorageConfig.java @@ -240,6 +240,8 @@ public void validate() { @Setter public static class CommonCheckpointConfig { + private boolean p66SnapshotEnabled = false; + private boolean enabled = false; private String directory = "common-checkpoint"; @@ -435,6 +437,9 @@ public static StorageConfig fromConfig(Config config) { sc.snapshot.postProcess(); sc.stateArchive.postProcess(); sc.commonCheckpoint.postProcess(); + if (sc.commonCheckpoint.p66SnapshotEnabled && !sc.commonCheckpoint.enabled) { + throw new IllegalArgumentException("p66SnapshotEnabled requires commonCheckpoint.enabled"); + } sc.pathStateRoot.postProcess(); if (sc.commonCheckpoint.enabled && (!sc.stateArchive.enabled || !sc.pathStateRoot.enabled)) { diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 9d5b75270bb..9537d426459 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -195,6 +195,8 @@ storage { } # Three-authority checkpoint. Admits a verified format-v1 PathState baseline. commonCheckpoint.enabled = false + # P66 physical Snapshot + parallel block artifacts; fresh Common baseline only. + commonCheckpoint.p66SnapshotEnabled = false commonCheckpoint.directory = "common-checkpoint" # Experimental current-only, non-consensus path state root. Disabled by default. diff --git a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java index 1560d5519e7..022758ea4e2 100644 --- a/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/StorageConfigTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.typesafe.config.Config; @@ -141,10 +142,23 @@ public void testStateArchiveRejectsSmallSegments() { StorageConfig.fromConfig(withRef("storage.stateArchive.maxSegmentSize = 1024")); } + @Test + public void testP66SnapshotRequiresCommonAndAcceptsExplicitOptIn() { + assertThrows(IllegalArgumentException.class, () -> StorageConfig.fromConfig(withRef( + "storage.commonCheckpoint.p66SnapshotEnabled = true"))); + StorageConfig configured = StorageConfig.fromConfig(withRef( + "storage.stateArchive.enabled = true\n" + + "storage.pathStateRoot.enabled = true\n" + + "storage.commonCheckpoint.enabled = true\n" + + "storage.commonCheckpoint.p66SnapshotEnabled = true")); + assertTrue(configured.getCommonCheckpoint().isP66SnapshotEnabled()); + } + @Test public void testCommonCheckpointDefaultsAndAdmission() { StorageConfig defaults = StorageConfig.fromConfig(withRef()); assertFalse(defaults.getCommonCheckpoint().isEnabled()); + assertFalse(defaults.getCommonCheckpoint().isP66SnapshotEnabled()); assertEquals("common-checkpoint", defaults.getCommonCheckpoint().getDirectory()); StorageConfig configured = StorageConfig.fromConfig(withRef( diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 422fed0f8b5..b6d49516725 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -228,6 +228,7 @@ private static void applyStorageConfig(StorageConfig sc) { PARAMETER.storage.setStateArchiveHotStoreSettings(sc.getStateArchive().getHotStore()); PARAMETER.storage.setStateArchiveAppendFileSettings(sc.getStateArchive().getAppendFile()); PARAMETER.storage.setCommonCheckpointEnabled(sc.getCommonCheckpoint().isEnabled()); + PARAMETER.storage.setP66SnapshotEnabled(sc.getCommonCheckpoint().isP66SnapshotEnabled()); PARAMETER.storage.setCommonCheckpointDirectory(sc.getCommonCheckpoint().getDirectory()); PARAMETER.storage.setPathStateRootEnabled(sc.getPathStateRoot().isEnabled()); PARAMETER.storage.setPathStateRootMode(sc.getPathStateRoot().getMode()); diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 925674c9de0..9c6ed4fcd62 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -569,6 +569,12 @@ public void init() { accountStateCallBack.setChainBaseManager(chainBaseManager); trieService.setChainBaseManager(chainBaseManager); revokingStore.disable(); + if (Args.getInstance().getStorage().isP66SnapshotEnabled()) { + if (!Args.getInstance().getStorage().isCommonCheckpointEnabled()) { + throw new IllegalStateException("P66 Snapshot requires Common checkpoint"); + } + chainBaseManager.getAccountAssetStore().enableSnapshots((SnapshotManager) revokingStore); + } revokingStore.check(); transactionCache.initCache(); rewardViCalService.init(); @@ -784,7 +790,7 @@ private void initCommonCheckpoint() { storage.getStateArchiveDirectory()).normalize(); Path checkpointDirectory = Paths.get(Args.getInstance().getOutputDirectory(), storage.getCommonCheckpointDirectory()).normalize(); - byte[] formatIdentity = CommonCheckpointFormat.identity(); + byte[] formatIdentity = CommonCheckpointFormat.identity(storage.isP66SnapshotEnabled()); org.tron.core.config.args.StorageConfig.StateArchiveAppendFileConfig appendConfig = storage.getStateArchiveAppendFileSettings(); boolean appendEnabled = appendConfig != null && appendConfig.isEnabled(); @@ -806,6 +812,14 @@ private void initCommonCheckpoint() { boolean baselineExists = Files.isRegularFile( checkpointDirectory.resolve(CommonCheckpointBaselineFile.FILE_NAME), LinkOption.NOFOLLOW_LINKS); + if (storage.isP66SnapshotEnabled() && pathExisted && !baselineExists + && !baselineFile.hasBootstrapIntent(formatIdentity)) { + throw new IllegalStateException("P66 Snapshot mode requires a fresh Common baseline"); + } + if (baselineExists && !Arrays.equals(baselineFile.load().getFormatIdentity(), + formatIdentity)) { + throw new IllegalStateException("Common checkpoint Snapshot semantics differ"); + } if (!baselineExists) { requireEmptyOrMissing(archiveDirectory, "State Archive"); if (!pathExisted) { @@ -1312,8 +1326,11 @@ private void attachPathStateBlockFinalRuntime() throws java.io.IOException { throw new IllegalStateException( "Path-state block-final capture requires account-asset Store"); } - SnapshotPathStateTransitionCollector collector = new SnapshotPathStateTransitionCollector( - accountAssetStore::prefixQuery, this::scanPathStateActivationAccounts); + org.tron.core.db2.stateroot.PathStateTransitionCollector collector = + Args.getInstance().getStorage().isP66SnapshotEnabled() + ? new org.tron.core.db2.archive.PhysicalSnapshotPathStateCollector() + : new SnapshotPathStateTransitionCollector( + accountAssetStore::prefixQuery, this::scanPathStateActivationAccounts); org.tron.core.config.args.Storage storage = Args.getInstance().getStorage(); PathStateRuntimeAttachment attachment = storage.isCommonCheckpointEnabled() ? PathStateRuntimeAttachment.commonCheckpoint(collector, this::advancePathStateRoot, diff --git a/framework/src/test/java/org/tron/core/db2/core/P66SnapshotManagerIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/core/P66SnapshotManagerIntegrationTest.java new file mode 100644 index 00000000000..b785adf88be --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/P66SnapshotManagerIntegrationTest.java @@ -0,0 +1,47 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.common.primitives.Longs; +import org.junit.Test; +import org.tron.common.BaseMethodTest; +import org.tron.core.config.args.Args; +import org.tron.core.config.args.Storage; +import org.tron.core.db2.ISession; +import org.tron.core.store.AccountAssetStore; + +/** Exercises Manager startup registration, fresh bootstrap and Store routing via Spring. */ +public class P66SnapshotManagerIntegrationTest extends BaseMethodTest { + @Override + protected void beforeContext() { + Storage storage = Args.getInstance().getStorage(); + storage.setCommonCheckpointEnabled(true); + storage.setP66SnapshotEnabled(true); + storage.setStateArchiveEnabled(true); + storage.setPathStateRootEnabled(true); + storage.setPathStateRootEngine("LEVELDB"); + storage.setStateArchiveServingIndexEngine("LEVELDB"); + } + + @Test + public void managerBootstrapsPhysicalModeAndRegistersRevocableAccountAssetStore() { + SnapshotManager snapshots = context.getBean(SnapshotManager.class); + AccountAssetStore assets = chainBaseManager.getAccountAssetStore(); + assertEquals(1, snapshots.getDbs().stream() + .filter(db -> "account-asset".equals(db.getDbName())).count()); + byte[] key = new byte[22]; + key[0] = 0x41; + key[21] = '1'; + assertNull(assets.get(key)); + try (ISession session = snapshots.buildSession()) { + assets.put(key, Longs.toByteArray(19)); + assertArrayEquals(Longs.toByteArray(19), assets.get(key)); + assertNull(assets.getFromRoot(key)); + } + assertNull(assets.get(key)); + assertTrue(dbManager.getPathStateSnapshotHead() != null); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/core/P66SnapshotPipelineTest.java b/framework/src/test/java/org/tron/core/db2/core/P66SnapshotPipelineTest.java new file mode 100644 index 00000000000..ed4d17da668 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/P66SnapshotPipelineTest.java @@ -0,0 +1,545 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.primitives.Bytes; +import com.google.common.primitives.Longs; +import com.google.protobuf.ByteString; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.storage.leveldb.LevelDbDataSourceImpl; +import org.tron.core.config.args.Args; +import org.tron.core.db2.ISession; +import org.tron.core.db2.archive.BlockChangeView; +import org.tron.core.db2.archive.BlockReverseDiff; +import org.tron.core.db2.archive.BlockSnapshotMeta; +import org.tron.core.db2.archive.PhysicalSnapshotPathStateCollector; +import org.tron.core.db2.archive.SnapshotOldValueCollector; +import org.tron.core.db2.common.LevelDB; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.db2.stateroot.PathStateBlockTransition; +import org.tron.core.db2.stateroot.PathStateCanonicalizer; +import org.tron.core.db2.stateroot.PathStateCanonicalizer.P66Phase; +import org.tron.core.db2.stateroot.PathStateCheckpointMaterializer; +import org.tron.core.db2.stateroot.PathStateLayerLimits; +import org.tron.core.db2.stateroot.PathStateParticipantScope; +import org.tron.core.db2.stateroot.PathStatePhysicalOverlayHead; +import org.tron.core.db2.stateroot.PathStatePhysicalStoreSet; +import org.tron.core.db2.stateroot.PathStateRoot; +import org.tron.core.db2.stateroot.PathStateRootMetadata; +import org.tron.core.db2.stateroot.PathStateRuntimeAttachment; +import org.tron.core.db2.stateroot.PathStateSnapshotDelta; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; +import org.tron.protos.Protocol.Account; + +public class P66SnapshotPipelineTest { + private static final byte[] ADDRESS = address(1); + private static final byte[] ASSET = Bytes.concat(ADDRESS, new byte[]{'1'}); + private static final byte[] FLAG = "ALLOW_ASSET_OPTIMIZATION" + .getBytes(StandardCharsets.US_ASCII); + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeClass + public static void configure() { + Args.setParam(new String[]{"--output-directory", "output_p66_pipeline"}, "config-test.conf"); + } + + @AfterClass + public static void clear() { + Args.clearParam(); + } + + @Test + public void migratesOnceBeforeFreezeAndBothConsumersSeeExactPhysicalValues() throws Exception { + try (Fixture f = fixture()) { + Account before = account(false, 5); + f.accounts.getHead().put(ADDRESS, before.toByteArray()); + AtomicReference archiveView = new AtomicReference<>(); + AtomicReference pathView = new AtomicReference<>(); + CountDownLatch archiveEntered = new CountDownLatch(1); + CountDownLatch pathEntered = new CountDownLatch(1); + f.manager.installArchiveCollector(view -> { + archiveView.set(view); + archiveEntered.countDown(); + await(pathEntered); + return new SnapshotOldValueCollector().collect(view); + }, diff -> { }); + f.manager.attachPathStateRuntime(new PathStateRuntimeAttachment(view -> { + pathView.set(view); + pathEntered.countDown(); + await(archiveEntered); + return new PhysicalSnapshotPathStateCollector().collect(view); + }, transition -> { })); + try (ISession block = f.manager.buildSession()) { + f.accounts.put(ADDRESS, account(false, 9).toByteArray()); + block.commit(meta(1)); + } + assertTrue(archiveView.get() == pathView.get()); + assertEquals(9, Longs.fromByteArray(f.assets.getUnchecked(ASSET))); + Account after = Account.parseFrom(f.accounts.getUnchecked(ADDRESS)); + assertTrue(after.getAssetOptimized()); + assertTrue(after.getAssetV2Map().isEmpty()); + assertNull(f.assets.getHead().getRoot().get(ASSET)); + assertArrayEquals(before.toByteArray(), f.accounts.getHead().getRoot().get(ADDRESS)); + BlockReverseDiff diff = ((SnapshotImpl) f.accounts.getHead()).getPreparedArchiveBlock(); + assertEquals(2, diff.getGroups().size()); + assertArrayEquals(before.toByteArray(), diff.getGroups().stream() + .filter(group -> group.getDbName().equals("account")).findFirst().get() + .getEntries().get(0).getOldValue().getValue()); + f.manager.fastPop(); + assertArrayEquals(before.toByteArray(), f.accounts.getUnchecked(ADDRESS)); + assertNull(f.assets.getUnchecked(ASSET)); + } + } + + @Test + public void activationWithoutChangedAccountsDoesNotScanOrMigrate() throws Exception { + try (Fixture f = fixture()) { + f.properties.getHead().put(FLAG, Longs.toByteArray(0)); + f.accounts.getHead().put(ADDRESS, account(false, 7).toByteArray()); + try (ISession block = f.manager.buildSession()) { + f.properties.put(FLAG, Longs.toByteArray(1)); + P66CoupledMutationMaterializer.Statistics stats = f.materialize(); + assertEquals(0, stats.changedAccounts); + assertEquals(0, stats.prefixQueries); + block.commit(meta(1)); + } + assertFalse(Account.parseFrom(f.accounts.getUnchecked(ADDRESS)).getAssetOptimized()); + assertNull(f.assets.getUnchecked(ASSET)); + } + } + + @Test + public void offEmptyAndOptimizedUnrelatedWritesDoNotQueryAssets() throws Exception { + try (Fixture f = fixture()) { + f.properties.getHead().put(FLAG, Longs.toByteArray(0)); + try (ISession block = f.manager.buildSession()) { + f.properties.put(FLAG, Longs.toByteArray(0)); + f.accounts.put(ADDRESS, account(false, 7).toByteArray()); + assertEquals(0, f.materialize().assetPuts); + assertFalse(Account.parseFrom(f.accounts.getUnchecked(ADDRESS)).getAssetOptimized()); + } + f.properties.getHead().put(FLAG, Longs.toByteArray(1)); + for (boolean optimized : new boolean[]{false, true}) { + try (ISession block = f.manager.buildSession()) { + f.accounts.put(ADDRESS, account(optimized, 0).toBuilder().clearAssetV2() + .setBalance(77).build().toByteArray()); + P66CoupledMutationMaterializer.Statistics stats = f.materialize(); + assertEquals(1, stats.emptyAssetSkips); + assertEquals(0, stats.prefixQueries); + assertEquals(0, stats.assetPuts); + assertEquals(0, stats.assetDeletes); + } + } + } + } + + @Test + public void zeroDeleteAndAccountDeleteRevokeAlongsideNestedTransactionWrites() throws Exception { + try (Fixture f = fixture()) { + f.accounts.getHead().put(ADDRESS, account(true, 0).toBuilder().clearAssetV2() + .build().toByteArray()); + f.assets.getHead().put(ASSET, Longs.toByteArray(11)); + try (ISession block = f.manager.buildSession()) { + try (ISession transaction = f.manager.buildSession()) { + f.accounts.put(ADDRESS, account(true, 0).toByteArray()); + transaction.merge(); + } + assertEquals(1, f.materialize().assetDeletes); + assertNull(f.assets.getUnchecked(ASSET)); + } + assertEquals(11, Longs.fromByteArray(f.assets.getUnchecked(ASSET))); + try (ISession block = f.manager.buildSession()) { + byte[] second = Bytes.concat(ADDRESS, new byte[]{'2'}); + f.assets.put(second, Longs.toByteArray(8)); + f.accounts.delete(ADDRESS); + P66CoupledMutationMaterializer.Statistics stats = f.materialize(); + assertEquals(1, stats.prefixQueries); + assertEquals(2, stats.assetDeletes); + assertTrue(f.assets.prefixQuery(ADDRESS).isEmpty()); + } + assertEquals(11, Longs.fromByteArray(f.assets.getUnchecked(ASSET))); + } + } + + @Test + public void failedParallelBranchNeverAttachesOrPublishesAndJoinsBeforeRevoke() throws Exception { + try (Fixture f = fixture()) { + CountDownLatch pathEntered = new CountDownLatch(1); + AtomicInteger published = new AtomicInteger(); + f.manager.installArchiveCollector(view -> { + await(pathEntered); + throw new IllegalStateException("injected archive failure"); + }, diff -> { }); + f.manager.attachPathStateRuntime(new PathStateRuntimeAttachment(view -> { + pathEntered.countDown(); + return new PhysicalSnapshotPathStateCollector().collect(view); + }, transition -> published.incrementAndGet())); + try (ISession block = f.manager.buildSession()) { + f.accounts.put(ADDRESS, account(false, 9).toByteArray()); + assertThrows(IllegalStateException.class, () -> block.commit(meta(1))); + assertNull(((SnapshotImpl) f.accounts.getHead()).getBlockSnapshotMeta()); + assertEquals(0, published.get()); + } + assertNull(f.accounts.getUnchecked(ADDRESS)); + assertNull(f.assets.getUnchecked(ASSET)); + } + } + + @Test + public void interruptedJoinWaitsForReaderThenRevokesAndPreservesInterrupt() throws Exception { + try (Fixture f = fixture()) { + CountDownLatch archiveEntered = new CountDownLatch(1); + CountDownLatch releaseArchive = new CountDownLatch(1); + CountDownLatch finished = new CountDownLatch(1); + AtomicReference unexpected = new AtomicReference<>(); + f.manager.installArchiveCollector(view -> { + archiveEntered.countDown(); + await(releaseArchive); + return new SnapshotOldValueCollector().collect(view); + }, diff -> { }); + f.manager.attachPathStateRuntime(new PathStateRuntimeAttachment( + new PhysicalSnapshotPathStateCollector(), transition -> { + throw new AssertionError("must not publish interrupted block"); + })); + Thread committer = new Thread(() -> { + try (ISession block = f.manager.buildSession()) { + f.accounts.put(ADDRESS, account(false, 9).toByteArray()); + assertThrows(IllegalStateException.class, () -> block.commit(meta(1))); + assertTrue(Thread.currentThread().isInterrupted()); + } catch (Throwable failure) { + unexpected.set(failure); + } finally { + finished.countDown(); + } + }); + committer.start(); + try { + assertTrue(archiveEntered.await(5, TimeUnit.SECONDS)); + committer.interrupt(); + assertFalse("must retain Snapshot until reader exits", finished.await(100, + TimeUnit.MILLISECONDS)); + } finally { + releaseArchive.countDown(); + committer.join(5000); + } + assertFalse(committer.isAlive()); + assertNull(unexpected.get()); + assertNull(f.assets.getUnchecked(ASSET)); + assertNull(f.accounts.getUnchecked(ADDRESS)); + } + } + + @Test + public void malformedDeletionCannotTurnIntoUnboundedAssetPrefixScan() throws Exception { + try (Fixture f = fixture(); ISession block = f.manager.buildSession()) { + f.accounts.delete(new byte[0]); + assertThrows(IllegalStateException.class, f::materialize); + } + } + + @Test + public void accountAssetStoreUsesSnapshotForReadsWritesPrefixesAndRevoke() throws Exception { + String oldOutput = Args.getInstance().getOutputDirectory(); + Args.getInstance().outputDirectory = temporaryFolder.newFolder().toString(); + SnapshotManager manager = new SnapshotManager(""); + Path path = temporaryFolder.newFolder().toPath(); + Chainbase accounts = Fixture.database(path, "account"); + Chainbase properties = Fixture.database(path, "properties"); + TestAssetStore assetStore = new TestAssetStore(); + try { + manager.add(accounts); + manager.add(properties); + assetStore.enableSnapshots(manager); + properties.put(FLAG, Longs.toByteArray(1)); + manager.enable(); + try (ISession block = manager.buildSession()) { + accounts.put(ADDRESS, account(false, 17).toByteArray()); + block.commit(meta(1)); + } + assertEquals(17, assetStore.getBalance(Account.parseFrom(accounts.getUnchecked(ADDRESS)), + new byte[]{'1'})); + assertEquals(1, assetStore.prefixQuery(ADDRESS).size()); + assertNull(assetStore.getFromRoot(ASSET)); + try (ISession pending = manager.buildSession()) { + assetStore.updateByBatch(Collections.singletonMap(ASSET, Longs.toByteArray(23))); + assertEquals(23, Longs.fromByteArray(assetStore.get(ASSET))); + } + assertEquals(17, Longs.fromByteArray(assetStore.get(ASSET))); + assertThrows(IllegalStateException.class, () -> assetStore.updateByBatchSynced( + Collections.singletonMap(ASSET, Longs.toByteArray(44)))); + manager.fastPop(); + assertNull(assetStore.get(ASSET)); + assertTrue(assetStore.prefixQuery(ADDRESS).isEmpty()); + } finally { + manager.shutdown(); + accounts.close(); + properties.close(); + assetStore.close(); + Args.getInstance().outputDirectory = oldOutput; + } + } + + @Test + public void pathFailureStillJoinsArchiveBeforeReleasingFrozenSnapshot() throws Exception { + try (Fixture f = fixture()) { + CountDownLatch archiveEntered = new CountDownLatch(1); + CountDownLatch pathFailed = new CountDownLatch(1); + AtomicInteger archiveFinished = new AtomicInteger(); + f.manager.installArchiveCollector(view -> { + archiveEntered.countDown(); + await(pathFailed); + BlockReverseDiff result = new SnapshotOldValueCollector().collect(view); + archiveFinished.incrementAndGet(); + return result; + }, diff -> { }); + f.manager.attachPathStateRuntime(new PathStateRuntimeAttachment(view -> { + await(archiveEntered); + pathFailed.countDown(); + throw new IllegalStateException("injected PathState failure"); + }, transition -> { + throw new AssertionError("must not publish"); + })); + try (ISession block = f.manager.buildSession()) { + f.accounts.put(ADDRESS, account(false, 9).toByteArray()); + assertThrows(IllegalStateException.class, () -> block.commit(meta(1))); + assertEquals(1, archiveFinished.get()); + assertNull(((SnapshotImpl) f.accounts.getHead()).getBlockSnapshotMeta()); + } + assertNull(f.assets.getUnchecked(ASSET)); + } + } + + private static final class TestAssetStore extends org.tron.core.store.AccountAssetStore { + private TestAssetStore() { + super("account-asset"); + } + } + + @Test + public void realPathStateForkRewindCheckpointAndNativeReopenMatchPhysicalOracle() + throws Exception { + Path path = temporaryFolder.newFolder().toPath(); + Path oraclePath = temporaryFolder.newFolder().toPath(); + Path checkpoint = temporaryFolder.newFolder().toPath(); + PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); + PathStateRootMetadata baselineHead; + byte[] format = CommonCheckpointFormat.identity(true); + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(path, scope, + Engine.LEVELDB)) { + PathStateRoot root = stores.createRoot(); + root.put("properties", FLAG, Longs.toByteArray(1)); + stores.persistFlatSnapshot(root); + } + try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(path, scope, + Engine.LEVELDB)) { + PathStateRoot root = stores.buildRootFromFlat(); + baselineHead = PathStateRootMetadata.base(0, addressHash(0), addressHash(0), 0, + P66Phase.P66_ON, stores.getFormatDigest(), root.rootHash(), new byte[32]); + stores.publishCurrent(baselineHead); + } + CommonCheckpointBaseline baseline = new CommonCheckpointBaseline(format, + BlockSnapshotMeta.forBlock(0, addressHash(0), addressHash(0), 0), + baselineHead.getStateRoot()); + byte[] expected; + CommonCheckpointTarget target; + try (Fixture f = fixture(); + PathStatePhysicalOverlayHead head = PathStatePhysicalOverlayHead.open(path, + Engine.LEVELDB, new PathStateLayerLimits(16, 16L << 20))) { + head.admitFreshCommonBaseline(baseline); + PathStateRuntimeAttachment attachment = PathStateRuntimeAttachment.commonCheckpoint( + new PhysicalSnapshotPathStateCollector(), transition -> head.advance(transition), + head::preview, head::prepareSnapshotDelta); + attachment.synchronizeReadyHead(baselineHead); + f.manager.attachPathStateRuntime(attachment); + f.manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + try (ISession block = f.manager.buildSession()) { + f.accounts.put(ADDRESS, account(false, 9).toByteArray()); + block.commit(meta(1)); + } + f.manager.fastPop(); + attachment.synchronizeReadyHead(head.rewindTo(0, addressHash(0))); + BlockSnapshotMeta fork = BlockSnapshotMeta.forBlock(1, addressHash(7), addressHash(0), 3000); + try (ISession block = f.manager.buildSession()) { + f.accounts.put(ADDRESS, account(false, 13).toByteArray()); + block.commit(fork); + } + try (PathStatePhysicalStoreSet oracle = PathStatePhysicalStoreSet.open(oraclePath, scope, + Engine.LEVELDB)) { + PathStateRoot root = oracle.createRoot(); + root.put("properties", FLAG, Longs.toByteArray(1)); + root.put("account", ADDRESS, f.accounts.getUnchecked(ADDRESS)); + root.put("account-asset", ASSET, Longs.toByteArray(13)); + expected = root.rootHash(); + } + assertArrayEquals(expected, head.getHead().getStateRoot()); + PathStateSnapshotDelta attached = ((SnapshotImpl) f.accounts.getHead()) + .getPreparedPathStateDelta(); + assertArrayEquals(expected, attached.getStateRoot()); + CommonCheckpointPayload payload = new CommonCheckpointPayloadFactory().capture(format, + f.manager.getDbs(), 1); + new CommonCheckpointFile(checkpoint).publish(payload); + target = CommonCheckpointTarget.from(payload); + ChainbaseCheckpointMaterializer chain = new ChainbaseCheckpointMaterializer( + checkpoint.resolve("chain"), format, f.manager.getDbs(), baseline); + PathStateCheckpointMaterializer materializer = head.checkpointMaterializer(format, baseline); + chain.materialize(payload, target); + materializer.materialize(payload, target); + chain.publish(target); + materializer.publish(target); + head.prepareCommonCheckpointRebase(target).apply(); + new CommonCheckpointSnapshotRebaser().prepare(f.manager.getDbs(), target, 1).apply(); + assertEquals(13, Longs.fromByteArray(f.assets.getHead().getRoot().get(ASSET))); + } + try (PathStatePhysicalOverlayHead reopened = PathStatePhysicalOverlayHead.openCommonCheckpoint( + path, Engine.LEVELDB, new PathStateLayerLimits(16, 16L << 20), 1L << 20, 2, 2, + format, target.getLastBlock(), P66Phase.P66_ON)) { + assertArrayEquals(expected, reopened.getHead().getStateRoot()); + } + } + + @Test + public void checkpointCapturesBothStoresAndReplaysAfterPartialNativeWrite() throws Exception { + Path checkpoint = temporaryFolder.newFolder().toPath(); + CommonCheckpointPayload payload; + try (Fixture f = fixture()) { + f.manager.installArchiveCollector(new SnapshotOldValueCollector(), diff -> { }); + f.manager.attachPathStateRuntime(new PathStateRuntimeAttachment( + new PhysicalSnapshotPathStateCollector(), transition -> { }, (number, hash) -> { }, + null, (meta, transition) -> delta(meta, transition))); + try (ISession block = f.manager.buildSession()) { + f.accounts.put(ADDRESS, account(false, 9).toByteArray()); + block.commit(meta(1)); + } + payload = new CommonCheckpointPayloadFactory().capture(CommonCheckpointFormat.identity(true), + f.manager.getDbs(), 1); + assertTrue(payload.getChainbaseStores().stream() + .anyMatch(store -> store.getDbName().equals("account-asset"))); + CommonCheckpointFile wal = new CommonCheckpointFile(checkpoint); + wal.publish(payload); + // Account durable, AccountAsset still absent: emulate interruption between Store batches. + Map accountBatch = new LinkedHashMap<>(); + accountBatch.put(WrappedByteArray.of(ADDRESS), + WrappedByteArray.of(f.accounts.getUnchecked(ADDRESS))); + ((SnapshotRoot) f.accounts.getHead().getRoot()).applyCheckpointMutations(accountBatch); + assertNull(f.assets.getHead().getRoot().get(ASSET)); + f.manager.fastPop(); + ChainbaseCheckpointMaterializer materializer = new ChainbaseCheckpointMaterializer( + checkpoint.resolve("chainbase"), CommonCheckpointFormat.identity(true), + f.manager.getDbs()); + CommonCheckpointPayload loaded = wal.loadRequired(); + CommonCheckpointTarget target = CommonCheckpointTarget.from(loaded); + materializer.materialize(loaded, target); + materializer.publish(target); + assertEquals(9, Longs.fromByteArray(f.assets.getUnchecked(ASSET))); + materializer.materialize(loaded, target); + assertEquals(CommonCheckpointMaterializer.Status.PUBLISHED, materializer.inspect(target)); + assertThrows(java.io.IOException.class, () -> ChainbaseCheckpointMaterializer + .loadPublishedHead(checkpoint.resolve("chainbase"), CommonCheckpointFormat.identity())); + } + } + + private static PathStateSnapshotDelta delta(BlockSnapshotMeta meta, + PathStateBlockTransition transition) { + PathStateSnapshotDelta delta = mock(PathStateSnapshotDelta.class); + when(delta.getMeta()).thenReturn(meta); + when(delta.getParentStateRoot()).thenReturn(new byte[32]); + when(delta.getStateRoot()).thenReturn(addressHash(3)); + when(delta.getTransitionPayloadDigest()).thenReturn(transition.getPayloadDigest()); + when(delta.getStores()).thenReturn(Collections.emptyList()); + when(delta.getSuperNodeMutations()).thenReturn(Collections.emptyList()); + return delta; + } + + private Fixture fixture() throws Exception { + return new Fixture(temporaryFolder.newFolder().toPath()); + } + + private static void await(CountDownLatch latch) { + try { + assertTrue("both branches must run concurrently", latch.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } + } + + private static byte[] address(int seed) { + byte[] bytes = new byte[21]; + bytes[0] = 0x41; + bytes[20] = (byte) seed; + return bytes; + } + + private static byte[] addressHash(int seed) { + byte[] bytes = new byte[32]; + bytes[31] = (byte) seed; + return bytes; + } + + private static BlockSnapshotMeta meta(int number) { + return BlockSnapshotMeta.forBlock(number, addressHash(number), addressHash(number - 1), + number * 3000L); + } + + private static Account account(boolean optimized, long balance) { + return Account.newBuilder().setAddress(ByteString.copyFrom(ADDRESS)) + .setAssetOptimized(optimized).putAssetV2("1", balance).build(); + } + + private static final class Fixture implements AutoCloseable { + private final SnapshotManager manager = new SnapshotManager(""); + private final Chainbase accounts; + private final Chainbase assets; + private final Chainbase properties; + + private Fixture(Path path) { + accounts = database(path, "account"); + assets = database(path, "account-asset"); + properties = database(path, "properties"); + manager.add(accounts); + manager.add(properties); + manager.installP66SnapshotLane(assets); + properties.getHead().put(FLAG, Longs.toByteArray(1)); + manager.enable(); + } + + private P66CoupledMutationMaterializer.Statistics materialize() { + return new P66CoupledMutationMaterializer(accounts, assets, properties).materialize(); + } + + private static Chainbase database(Path path, String name) { + return new Chainbase(new SnapshotRoot(new LevelDB( + new LevelDbDataSourceImpl(path.toString(), name)))); + } + + @Override + public void close() { + manager.shutdown(); + accounts.close(); + assets.close(); + properties.close(); + } + } +} From fd69340ca33ceca74bb117f4da80f3c66dbc062e Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 16:46:18 +0800 Subject: [PATCH 136/161] fix(chainbase): preserve p66 recovery format --- .../core/db2/core/CommonCheckpointFormat.java | 6 --- .../tron/core/db2/core/SnapshotManager.java | 16 +++++- .../tron/core/store/AccountAssetStore.java | 15 +++++- .../main/java/org/tron/core/db/Manager.java | 12 ++--- .../db2/core/P66SnapshotPipelineTest.java | 10 ++-- .../db2/core/P66SnapshotRecoveryTest.java | 52 +++++++++++++++++++ ...athStateManagerStartupIntegrationTest.java | 5 +- 7 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/db2/core/P66SnapshotRecoveryTest.java diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java index efb180d94c3..32ad2c01789 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointFormat.java @@ -14,12 +14,6 @@ public final class CommonCheckpointFormat { private CommonCheckpointFormat() { } - public static byte[] identity(boolean physicalSnapshot) { - return physicalSnapshot ? Hashing.sha256() - .hashString(ID + "/p66-physical-snapshot-v1", StandardCharsets.UTF_8).asBytes() - : identity(); - } - public static byte[] identity() { return Arrays.copyOf(DIGEST, DIGEST.length); } diff --git a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java index 376f55c8988..47aff011851 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/SnapshotManager.java @@ -77,6 +77,10 @@ public class SnapshotManager implements RevokingDatabase { private ExecutorService artifactExecutor; public synchronized void installP66SnapshotLane(Chainbase assets) { + installP66SnapshotLane(assets, false); + } + + public synchronized void installP66SnapshotLane(Chainbase assets, boolean recovering) { if (size != 0 || activeSession != 0 || p66Materializer != null || dbs.stream().anyMatch(db -> "account-asset".equals(db.getDbName()))) { throw new IllegalStateException("P66 Snapshot lane must be installed once before sessions"); @@ -87,7 +91,9 @@ public synchronized void installP66SnapshotLane(Chainbase assets) { throw new IllegalStateException("P66 Snapshot installation requires root heads"); } add(assets); - ((SnapshotRoot) accounts.getHead()).useMaterializedCoupledMutations(); + if (!recovering) { + ((SnapshotRoot) accounts.getHead()).useMaterializedCoupledMutations(); + } p66Materializer = new P66CoupledMutationMaterializer(accounts, assets, properties); artifactExecutor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1), task -> { @@ -97,6 +103,14 @@ public synchronized void installP66SnapshotLane(Chainbase assets) { }); } + public synchronized void finishP66Recovery() { + if (size != 0 || activeSession != 0 || p66Materializer == null) { + throw new IllegalStateException("P66 recovery must finish before sessions"); + } + ((SnapshotRoot) requireDatabase("account").getHead().getRoot()) + .useMaterializedCoupledMutations(); + } + private Chainbase requireDatabase(String name) { return dbs.stream().filter(db -> name.equals(db.getDbName())).findFirst() .orElseThrow(() -> new IllegalStateException("Missing P66 participant: " + name)); diff --git a/chainbase/src/main/java/org/tron/core/store/AccountAssetStore.java b/chainbase/src/main/java/org/tron/core/store/AccountAssetStore.java index a08ec6b122d..1b8114b1121 100644 --- a/chainbase/src/main/java/org/tron/core/store/AccountAssetStore.java +++ b/chainbase/src/main/java/org/tron/core/store/AccountAssetStore.java @@ -25,9 +25,14 @@ public class AccountAssetStore extends TronDatabase { private volatile Chainbase snapshots; + private boolean recoveringSnapshots; /** Shares the existing native DB; registers before recovery and before any sessions exist. */ public synchronized void enableSnapshots(SnapshotManager manager) { + enableSnapshots(manager, false); + } + + public synchronized void enableSnapshots(SnapshotManager manager, boolean recovering) { if (snapshots != null) { throw new IllegalStateException("AccountAsset Snapshot lane already attached"); } @@ -44,10 +49,16 @@ public synchronized void enableSnapshots(SnapshotManager manager) { Chainbase lane = new Chainbase( new SnapshotRoot(engine)); lane.setRegistrationSource(AccountAssetStore.class.getName()); - manager.installP66SnapshotLane(lane); + manager.installP66SnapshotLane(lane, recovering); + recoveringSnapshots = recovering; snapshots = lane; } + public synchronized void finishSnapshotRecovery(SnapshotManager manager) { + manager.finishP66Recovery(); + recoveringSnapshots = false; + } + @Override public void close() { if (snapshots != null) { @@ -78,7 +89,7 @@ public void updateByBatch(Map rows) { @Override public void updateByBatchSynced(Map rows) { - if (snapshots != null) { + if (snapshots != null && !recoveringSnapshots) { throw new IllegalStateException("AccountAsset durability belongs to Common checkpoint"); } super.updateByBatchSynced(rows); diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 9c6ed4fcd62..cf890da8bf8 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -573,7 +573,8 @@ public void init() { if (!Args.getInstance().getStorage().isCommonCheckpointEnabled()) { throw new IllegalStateException("P66 Snapshot requires Common checkpoint"); } - chainBaseManager.getAccountAssetStore().enableSnapshots((SnapshotManager) revokingStore); + chainBaseManager.getAccountAssetStore() + .enableSnapshots((SnapshotManager) revokingStore, true); } revokingStore.check(); transactionCache.initCache(); @@ -790,7 +791,7 @@ private void initCommonCheckpoint() { storage.getStateArchiveDirectory()).normalize(); Path checkpointDirectory = Paths.get(Args.getInstance().getOutputDirectory(), storage.getCommonCheckpointDirectory()).normalize(); - byte[] formatIdentity = CommonCheckpointFormat.identity(storage.isP66SnapshotEnabled()); + byte[] formatIdentity = CommonCheckpointFormat.identity(); org.tron.core.config.args.StorageConfig.StateArchiveAppendFileConfig appendConfig = storage.getStateArchiveAppendFileSettings(); boolean appendEnabled = appendConfig != null && appendConfig.isEnabled(); @@ -812,10 +813,6 @@ private void initCommonCheckpoint() { boolean baselineExists = Files.isRegularFile( checkpointDirectory.resolve(CommonCheckpointBaselineFile.FILE_NAME), LinkOption.NOFOLLOW_LINKS); - if (storage.isP66SnapshotEnabled() && pathExisted && !baselineExists - && !baselineFile.hasBootstrapIntent(formatIdentity)) { - throw new IllegalStateException("P66 Snapshot mode requires a fresh Common baseline"); - } if (baselineExists && !Arrays.equals(baselineFile.load().getFormatIdentity(), formatIdentity)) { throw new IllegalStateException("Common checkpoint Snapshot semantics differ"); @@ -853,6 +850,9 @@ private void initCommonCheckpoint() { pathDirectory, pathEngine, servingIndexEngine, storage.getPathStateRootNodeCacheBytes(), formatIdentity, baselineFile, baselineExists, modeAdmitted, appendEnabled, appendDirectory, appendConfig); + if (storage.isP66SnapshotEnabled()) { + chainBaseManager.getAccountAssetStore().finishSnapshotRecovery(snapshots); + } BlockSnapshotMeta canonical = currentCanonicalBlockMeta(); P66Phase phase = currentPathStatePhase(); if (modeAdmitted && Files.isRegularFile( diff --git a/framework/src/test/java/org/tron/core/db2/core/P66SnapshotPipelineTest.java b/framework/src/test/java/org/tron/core/db2/core/P66SnapshotPipelineTest.java index ed4d17da668..cbfe5ddd452 100644 --- a/framework/src/test/java/org/tron/core/db2/core/P66SnapshotPipelineTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/P66SnapshotPipelineTest.java @@ -345,7 +345,7 @@ public void realPathStateForkRewindCheckpointAndNativeReopenMatchPhysicalOracle( Path checkpoint = temporaryFolder.newFolder().toPath(); PathStateParticipantScope scope = new PathStateCanonicalizer().participantScope(); PathStateRootMetadata baselineHead; - byte[] format = CommonCheckpointFormat.identity(true); + byte[] format = CommonCheckpointFormat.identity(); try (PathStatePhysicalStoreSet stores = PathStatePhysicalStoreSet.open(path, scope, Engine.LEVELDB)) { PathStateRoot root = stores.createRoot(); @@ -432,7 +432,7 @@ public void checkpointCapturesBothStoresAndReplaysAfterPartialNativeWrite() thro f.accounts.put(ADDRESS, account(false, 9).toByteArray()); block.commit(meta(1)); } - payload = new CommonCheckpointPayloadFactory().capture(CommonCheckpointFormat.identity(true), + payload = new CommonCheckpointPayloadFactory().capture(CommonCheckpointFormat.identity(), f.manager.getDbs(), 1); assertTrue(payload.getChainbaseStores().stream() .anyMatch(store -> store.getDbName().equals("account-asset"))); @@ -446,7 +446,7 @@ public void checkpointCapturesBothStoresAndReplaysAfterPartialNativeWrite() thro assertNull(f.assets.getHead().getRoot().get(ASSET)); f.manager.fastPop(); ChainbaseCheckpointMaterializer materializer = new ChainbaseCheckpointMaterializer( - checkpoint.resolve("chainbase"), CommonCheckpointFormat.identity(true), + checkpoint.resolve("chainbase"), CommonCheckpointFormat.identity(), f.manager.getDbs()); CommonCheckpointPayload loaded = wal.loadRequired(); CommonCheckpointTarget target = CommonCheckpointTarget.from(loaded); @@ -455,8 +455,10 @@ public void checkpointCapturesBothStoresAndReplaysAfterPartialNativeWrite() thro assertEquals(9, Longs.fromByteArray(f.assets.getUnchecked(ASSET))); materializer.materialize(loaded, target); assertEquals(CommonCheckpointMaterializer.Status.PUBLISHED, materializer.inspect(target)); + ChainbaseCheckpointMaterializer.loadPublishedHead( + checkpoint.resolve("chainbase"), CommonCheckpointFormat.identity()); assertThrows(java.io.IOException.class, () -> ChainbaseCheckpointMaterializer - .loadPublishedHead(checkpoint.resolve("chainbase"), CommonCheckpointFormat.identity())); + .loadPublishedHead(checkpoint.resolve("chainbase"), addressHash(7))); } } diff --git a/framework/src/test/java/org/tron/core/db2/core/P66SnapshotRecoveryTest.java b/framework/src/test/java/org/tron/core/db2/core/P66SnapshotRecoveryTest.java new file mode 100644 index 00000000000..7f9f9b488a1 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/core/P66SnapshotRecoveryTest.java @@ -0,0 +1,52 @@ +package org.tron.core.db2.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.common.primitives.Bytes; +import com.google.common.primitives.Longs; +import com.google.protobuf.ByteString; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import org.junit.Test; +import org.tron.common.BaseMethodTest; +import org.tron.core.db2.common.WrappedByteArray; +import org.tron.core.store.AccountAssetStore; +import org.tron.protos.Protocol.Account; + +public class P66SnapshotRecoveryTest extends BaseMethodTest { + + @Test + public void legacyAccountRedoCompletesBeforePhysicalSnapshotWritesTakeOver() throws Exception { + SnapshotManager manager = context.getBean(SnapshotManager.class); + AccountAssetStore assets = chainBaseManager.getAccountAssetStore(); + Chainbase accounts = manager.getDbs().stream() + .filter(db -> "account".equals(db.getDbName())).findFirst().get(); + Chainbase properties = manager.getDbs().stream() + .filter(db -> "properties".equals(db.getDbName())).findFirst().get(); + properties.getHead().getRoot().put("ALLOW_ASSET_OPTIMIZATION" + .getBytes(StandardCharsets.US_ASCII), Longs.toByteArray(1)); + assets.enableSnapshots(manager, true); + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = 99; + byte[] asset = Bytes.concat(address, new byte[]{'1'}); + Account oldWalAccount = Account.newBuilder().setAddress(ByteString.copyFrom(address)) + .putAssetV2("1", 23).build(); + SnapshotRoot root = (SnapshotRoot) accounts.getHead().getRoot(); + root.applyCheckpointMutations(Collections.singletonMap(WrappedByteArray.of(address), + WrappedByteArray.of(oldWalAccount.toByteArray()))); + Account persisted = Account.parseFrom(root.get(address)); + assertTrue(persisted.getAssetOptimized()); + assertEquals(0, persisted.getAssetV2Count()); + assertEquals(23, Longs.fromByteArray(assets.getFromRoot(asset))); + // Replaying the same pre-Snapshot WAL must remain idempotent. + root.applyCheckpointMutations(Collections.singletonMap(WrappedByteArray.of(address), + WrappedByteArray.of(oldWalAccount.toByteArray()))); + assertEquals(23, Longs.fromByteArray(assets.getFromRoot(asset))); + assets.finishSnapshotRecovery(manager); + assertThrows(IllegalStateException.class, + () -> assets.updateByBatchSynced(Collections.singletonMap(asset, Longs.toByteArray(24)))); + } +} diff --git a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java index 358a2827d59..0575cc16536 100644 --- a/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db2/stateroot/PathStateManagerStartupIntegrationTest.java @@ -393,7 +393,10 @@ public void commonCheckpointFreshFlushAndRestartUseOneDurableBoundary() throws E invoke(manager, "closeCommonCheckpoint"); invoke(manager, "closePathStateRoot"); - withCommonConfig(output, () -> invoke(manager, "initCommonCheckpoint")); + withCommonConfig(output, () -> { + CommonParameter.getInstance().getStorage().setP66SnapshotEnabled(true); + invoke(manager, "initCommonCheckpoint"); + }); assertEquals(102L, manager.getPathStateSnapshotHead().getHead().getBlockNumber()); assertArrayEquals(pendingId.getBytes(), manager.getPathStateSnapshotHead().getHead().getBlockHash()); From 5363f21ff9acfad7722858da73376a91b531d113 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 17:17:10 +0800 Subject: [PATCH 137/161] perf(chainbase): reduce archive bookkeeping --- .../StateArchiveFiveLaneSegmentWriterV3.java | 51 +++++++++++-------- ...ArchiveServingIndexBuildCoordinatorV3.java | 12 +++-- ...rchiveCatalogAndServingCornerCaseTest.java | 37 ++++++++++++++ 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java index ff8ddf788c1..eb726f1f6d3 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java @@ -346,7 +346,9 @@ public synchronized List readCommittedDiffs(long fromExclusive if (fromExclusive == through) { return Collections.emptyList(); } - Map> bundles = new TreeMap<>(); + int blockCount = Math.toIntExact(through - fromExclusive); + int[] laneIds = StateArchiveFileFormatV3.fiveLaneIds(); + byte[][][] bundles = new byte[laneIds.length][blockCount][]; if (servingSegments == null) { servingSegments = new HashMap<>(); for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { @@ -357,7 +359,8 @@ public synchronized List readCommittedDiffs(long fromExclusive } } long first = fromExclusive + 1; - for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { + for (int laneOrdinal = 0; laneOrdinal < laneIds.length; laneOrdinal++) { + int laneId = laneIds[laneOrdinal]; NavigableMap segments = servingSegments.get(laneId); Map.Entry selected = segments.floorEntry(first); if (selected == null) { @@ -369,7 +372,7 @@ public synchronized List readCommittedDiffs(long fromExclusive readIndexedRange(laneId, segment.getSegmentSeq(), segment.getFirstBlock(), Math.max(first, segment.getFirstBlock()), Math.min(through, segment.getLastBlock()), segment.getSegmentHeaderDigest(), - bundles); + first, bundles[laneOrdinal]); } selected = segments.higherEntry(selected.getKey()); } @@ -377,19 +380,19 @@ public synchronized List readCommittedDiffs(long fromExclusive if (current != null && current.firstBlock <= through && current.lastBlock >= first) { readIndexedRange(laneId, current.segmentSeq, current.firstBlock, Math.max(first, current.firstBlock), Math.min(through, current.lastBlock), - current.headerDigest, bundles); + current.headerDigest, first, bundles[laneOrdinal]); } } - List result = new ArrayList<>(); - for (long block = fromExclusive + 1; block <= through; block++) { - Map laneFrames = bundles.remove(block); - if (laneFrames == null - || laneFrames.size() != StateArchiveFileFormatV3.fiveLaneIds().length) { - throw new IOException("Incomplete State Archive serving source bundle"); - } - List ordered = new ArrayList<>(); - for (int laneId : StateArchiveFileFormatV3.fiveLaneIds()) { - ordered.add(laneFrames.get(laneId)); + List result = new ArrayList<>(blockCount); + for (int block = 0; block < blockCount; block++) { + List ordered = new ArrayList<>(laneIds.length); + for (int lane = 0; lane < laneIds.length; lane++) { + byte[] frame = bundles[lane][block]; + if (frame == null) { + throw new IOException("Incomplete State Archive serving source bundle"); + } + ordered.add(frame); + bundles[lane][block] = null; } result.add(codec.decode(ordered).getDiff()); } @@ -406,7 +409,7 @@ synchronized long getServingReadBytes() { private void readIndexedRange(int laneId, long sequence, long segmentFirst, long first, long last, byte[] headerDigest, - Map> bundles) throws IOException { + long rangeFirst, byte[][] laneFrames) throws IOException { try (FileChannel data = FileChannel.open(dataPath(laneId, sequence), StandardOpenOption.READ); FileChannel index = FileChannel.open(indexPath(laneId, sequence), @@ -443,10 +446,11 @@ private void readIndexedRange(int laneId, long sequence, long segmentFirst, != entry.getEncodedFrameDigestPrefix()) { throw new IOException("State Archive serving block index frame mismatch"); } - if (bundles.computeIfAbsent(block, ignored -> new HashMap<>()).put(laneId, frame) - != null) { + int slot = Math.toIntExact(block - rangeFirst); + if (laneFrames[slot] != null) { throw new IOException("Duplicate State Archive serving source lane frame"); } + laneFrames[slot] = frame; } } } @@ -1535,11 +1539,16 @@ private void validateCatalogSelection() throws IOException { List expectedCurrent = catalog.selected().getCurrent(); List actualCurrent = getCurrentSegments(); List expectedSealed = catalog.selected().getSealed(); + Map> actualSealed = new HashMap<>(); + for (SealedSegment segment : sealedSegments) { + if (actualSealed.computeIfAbsent(segment.getLaneId(), ignored -> new HashMap<>()) + .put(segment.getSegmentSeq(), segment) != null) { + throw new IOException("State Archive duplicate sealed segment identity"); + } + } for (SealedSegment expected : expectedSealed) { - SealedSegment actual = sealedSegments.stream().filter(candidate -> - candidate.getLaneId() == expected.getLaneId() - && candidate.getSegmentSeq() == expected.getSegmentSeq()) - .findFirst().orElse(null); + SealedSegment actual = actualSealed.getOrDefault(expected.getLaneId(), + Collections.emptyMap()).get(expected.getSegmentSeq()); if (actual == null || !Arrays.equals(StateArchiveSegmentFormatV3.encodeSealedMapRecord( expected), StateArchiveSegmentFormatV3.encodeSealedMapRecord(actual))) { throw new IOException("State Archive Catalog sealed segment identity mismatch"); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java index 0fea69f82ec..3de5768a507 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java @@ -304,19 +304,25 @@ public BuildProgress indexNow(List diffs, CommonCheckpointTarg throw new IllegalStateException("Serving live handle is stale"); } List admitted = new ArrayList<>(Objects.requireNonNull(diffs, "diffs")); + int published = 0; + boolean rangeAccepted = false; try { admit(admitted, target); + rangeAccepted = true; + pending.clear(); for (BlockReverseDiff diff : admitted) { - List remainder = new ArrayList<>(pending); - pending.clear(); pending.add(diff); flushPending(); - pending.addAll(remainder.subList(1, remainder.size())); + published++; } sequence = buildSequence; generation = catalog.getCurrentGenerationId(); return progress(); } catch (IOException | RuntimeException failure) { + if (rangeAccepted) { + pending.clear(); + pending.addAll(admitted.subList(published, admitted.size())); + } valid = false; mode = Mode.CATCH_UP_REQUIRED; throw failure; diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCatalogAndServingCornerCaseTest.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCatalogAndServingCornerCaseTest.java index 3fca8d3ed23..a6385236d0f 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCatalogAndServingCornerCaseTest.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveCatalogAndServingCornerCaseTest.java @@ -59,6 +59,43 @@ public void bulkBatchCutsProduceTheSameExactIndexIdentity() throws Exception { } } + @Test + public void liveRangePublishesEveryBlockWithTheBulkLogicalIdentity() throws Exception { + List all = diffs(1, 17, 0); + Path bulkRoot = temporaryFolder.newFolder("bulk-reference").toPath(); + Path liveRoot = temporaryFolder.newFolder("live-range").toPath(); + try (StateArchiveServingIndexBuildCoordinatorV3 bulk = + new StateArchiveServingIndexBuildCoordinatorV3(bulkRoot, Engine.LEVELDB, 17); + StateArchiveServingIndexBuildCoordinatorV3 liveOwner = + new StateArchiveServingIndexBuildCoordinatorV3(liveRoot, Engine.LEVELDB, 1)) { + bulk.offerCommittedRange(all, target(all, 1)); + List first = all.subList(0, 1); + liveOwner.offerCommittedRange(first, target(first, 2)); + LiveServingIndexer live = liveOwner.completeInitialSync(target(first, 2)); + List suffix = all.subList(1, all.size()); + live.indexNow(suffix, target(suffix, 3)); + assertEquals(17, liveOwner.status().getIndexedThrough()); + assertEquals(17, liveOwner.status().getBuildSequence()); + assertEquals(0, liveOwner.status().getPendingBlocks()); + } + try (PersistentServingKeyIndexCatalog bulk = PersistentServingKeyIndexCatalog.open( + bulkRoot.resolve(StateArchiveServingIndexBuildCoordinatorV3.DIRECTORY), + Engine.LEVELDB, stage -> { }); + PersistentServingKeyIndexCatalog live = PersistentServingKeyIndexCatalog.open( + liveRoot.resolve(StateArchiveServingIndexBuildCoordinatorV3.DIRECTORY), + Engine.LEVELDB, stage -> { }); + PersistentServingKeyIndexGeneration expected = bulk.pin(); + PersistentServingKeyIndexGeneration actual = live.pin()) { + assertArrayEquals(expected.getAuthoritativePrefixDigest(), + actual.getAuthoritativePrefixDigest()); + assertEquals(expected.getKeyChangeCount(), actual.getKeyChangeCount()); + for (int key = 0; key < 8; key++) { + assertEquals(expected.firstChangeAfter("code", new byte[]{(byte) key}, 0, 17), + actual.firstChangeAfter("code", new byte[]{(byte) key}, 0, 17)); + } + } + } + @Test public void gapInvalidatesLiveHandleWithoutAdvancingI() throws Exception { Path root = temporaryFolder.newFolder("live-gap").toPath(); From 8ac7a6afb1ca5d07413dd0d0bda1b13455e2e38a Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 17:48:11 +0800 Subject: [PATCH 138/161] feat(chainbase): isolate archive index worker --- ...ArchiveAppendCheckpointMaterializerV3.java | 86 ++---- .../StateArchiveFiveLaneSegmentWriterV3.java | 20 +- ...ArchiveServingIndexBuildCoordinatorV3.java | 7 +- .../archive/StateArchiveServingWorkerV3.java | 258 ++++++++++++++++++ .../core/CommonCheckpointMaterializer.java | 4 + .../core/CommonCheckpointRedoCoordinator.java | 18 ++ .../db2/core/CommonCheckpointRuntime.java | 8 + .../core/CommonCheckpointRuntimeOwner.java | 3 +- .../main/java/org/tron/core/db/Manager.java | 11 +- ...iveAppendCheckpointMaterializerV3Test.java | 51 +++- .../StateArchiveServingWorkerV3Test.java | 154 +++++++++++ .../CommonCheckpointRedoCoordinatorTest.java | 20 ++ 12 files changed, 573 insertions(+), 67 deletions(-) create mode 100644 chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingWorkerV3.java create mode 100644 framework/src/test/java/org/tron/core/db2/archive/StateArchiveServingWorkerV3Test.java diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java index da94d5bd383..f6623148215 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3.java @@ -6,7 +6,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -30,10 +29,7 @@ public final class StateArchiveAppendCheckpointMaterializerV3 private final StateArchiveFiveLaneBlockCodecV3 codec = new StateArchiveFiveLaneBlockCodecV3(); private final StateArchiveFiveLaneSegmentWriterV3 writer; - private final StateArchiveServingIndexBuildCoordinatorV3 servingCoordinator; - private StateArchiveServingIndexBuildCoordinatorV3.LiveServingIndexer liveServingIndexer; - private List stagedServingDiffs; - private CommonCheckpointTarget stagedServingTarget; + private final StateArchiveServingWorkerV3 servingWorker; private boolean closed; public StateArchiveAppendCheckpointMaterializerV3(Path directory, @@ -46,15 +42,22 @@ public StateArchiveAppendCheckpointMaterializerV3(Path directory, public StateArchiveAppendCheckpointMaterializerV3(Path directory, byte[] commonFormatIdentity, Engine bindingEngine, byte[] baselineHistoryDigest, short compressionId, long rotationTargetBytes) throws IOException { + this(directory, commonFormatIdentity, bindingEngine, baselineHistoryDigest, compressionId, + rotationTargetBytes, () -> { }); + } + + StateArchiveAppendCheckpointMaterializerV3(Path directory, + byte[] commonFormatIdentity, Engine bindingEngine, byte[] baselineHistoryDigest, + short compressionId, long rotationTargetBytes, Runnable beforeServingBuild) throws IOException { this.directory = Objects.requireNonNull(directory, "directory"); this.commonFormatIdentity = requireDigest(commonFormatIdentity, "Common format identity"); this.bindingEngine = Objects.requireNonNull(bindingEngine, "bindingEngine"); this.compressionId = compressionId; this.writer = new StateArchiveFiveLaneSegmentWriterV3(directory, baselineHistoryDigest, compressionId, rotationTargetBytes); - this.servingCoordinator = new StateArchiveServingIndexBuildCoordinatorV3(directory, - bindingEngine, 1_000); - recoverServingIndex(); + this.servingWorker = new StateArchiveServingWorkerV3( + () -> new StateArchiveServingIndexBuildCoordinatorV3(directory, bindingEngine, 1_000), + writer, beforeServingBuild); } @Override @@ -93,8 +96,6 @@ public synchronized CommonCheckpointTarget prepare(CommonCheckpointCapture captu return target; } List diffs = admittedDiffs(admitted.getArchiveDiffs()); - stagedServingDiffs = diffs; - stagedServingTarget = target; if (!admitted.getArchiveBinding().equals(planCheckpoint(diffs))) { throw new IOException("Append-file Archive checkpoint binding differs"); } @@ -187,26 +188,39 @@ public synchronized void publish(CommonCheckpointTarget target) throws IOExcepti CommonCheckpointTarget admitted = requireTarget(target); Status status = inspect(admitted); if (status == Status.PUBLISHED) { - dispatchServingIfStaged(admitted); return; } if (status != Status.MATERIALIZED) { throw new IOException("Append-file Archive target is not materialized"); } StateArchiveCheckpointMaterializer.publishReadableTarget(directory, admitted); - dispatchServingIfStaged(admitted); + } + + @Override + public void afterCommit(CommonCheckpointTarget target) { + try { + servingWorker.offer(target); + } catch (IOException | RuntimeException failure) { + org.slf4j.LoggerFactory.getLogger("DB").error( + "Archive serving degraded after Common commit at {}", + target.getLastBlock().getBlockNumber(), failure); + } } /** Explicit sync-lifecycle handoff; it never infers completion from peer/head timing. */ public synchronized void completeServingInitialSync(CommonCheckpointTarget boundary) throws IOException { requireOpen(); - liveServingIndexer = servingCoordinator.completeInitialSync(boundary); + servingWorker.completeInitialSync(boundary); } public synchronized StateArchiveServingIndexBuildCoordinatorV3.BuildProgress servingIndexStatus() { - return servingCoordinator.status(); + return servingWorker.status(); + } + + public IOException servingIndexFailure() { + return servingWorker.failure(); } @Override @@ -215,7 +229,7 @@ public synchronized void close() throws IOException { closed = true; IOException failure = null; try { - servingCoordinator.close(); + servingWorker.close(); } catch (IOException closeFailure) { failure = closeFailure; } @@ -313,48 +327,6 @@ private short writerCompressionId() { return compressionId; } - private void dispatchServingIfStaged(CommonCheckpointTarget target) throws IOException { - if (stagedServingTarget == null || !stagedServingTarget.equals(target) - || stagedServingDiffs == null) { - return; - } - if (liveServingIndexer == null) { - servingCoordinator.offerCommittedRange(stagedServingDiffs, target); - } else { - liveServingIndexer.indexNow(stagedServingDiffs, target); - } - stagedServingDiffs = null; - stagedServingTarget = null; - } - - private void recoverServingIndex() throws IOException { - Optional published = - StateArchiveCheckpointMaterializer.loadReadableTargetIfPresent(directory); - if (!published.isPresent()) { - return; - } - CommonCheckpointTarget boundary = published.get(); - long indexed = servingCoordinator.status().getIndexedThrough(); - long target = boundary.getLastBlock().getBlockNumber(); - if (indexed > target) { - throw new IOException("Append-file serving index is ahead of Common W"); - } - if (indexed == target) { - servingCoordinator.recoverCommittedRange(Collections.emptyList(), boundary, true); - return; - } - long cursor = indexed >= 0 ? indexed : writer.getHistoryStartBlock() - 1; - if (cursor < 0) { - throw new IOException("Append-file serving recovery source is missing"); - } - while (cursor < target) { - long batchEnd = Math.min(target, cursor + 1_000); - List batch = writer.readCommittedDiffs(cursor, batchEnd); - servingCoordinator.recoverCommittedRange(batch, boundary, batchEnd == target); - cursor = batchEnd; - } - } - private void requireOpen() throws IOException { if (closed) { throw new IOException("Append-file Archive materializer is closed"); diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java index eb726f1f6d3..418bdcf254a 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveFiveLaneSegmentWriterV3.java @@ -336,6 +336,11 @@ public synchronized List getSealedSegments() { /** Replays complete five-lane bundles from Catalog-selected authority for serving repair. */ public synchronized List readCommittedDiffs(long fromExclusive, long through) throws IOException { + return readCommittedDiffs(fromExclusive, through, Long.MAX_VALUE); + } + + synchronized List readCommittedDiffs(long fromExclusive, long through, + long maxEncodedBytes) throws IOException { requireUsable(); if (fromExclusive < 0 || through < fromExclusive || appendHead == null || through > appendHead.getBlockNumber()) { @@ -372,7 +377,7 @@ public synchronized List readCommittedDiffs(long fromExclusive readIndexedRange(laneId, segment.getSegmentSeq(), segment.getFirstBlock(), Math.max(first, segment.getFirstBlock()), Math.min(through, segment.getLastBlock()), segment.getSegmentHeaderDigest(), - first, bundles[laneOrdinal]); + first, bundles[laneOrdinal], maxEncodedBytes); } selected = segments.higherEntry(selected.getKey()); } @@ -380,7 +385,7 @@ public synchronized List readCommittedDiffs(long fromExclusive if (current != null && current.firstBlock <= through && current.lastBlock >= first) { readIndexedRange(laneId, current.segmentSeq, current.firstBlock, Math.max(first, current.firstBlock), Math.min(through, current.lastBlock), - current.headerDigest, first, bundles[laneOrdinal]); + current.headerDigest, first, bundles[laneOrdinal], maxEncodedBytes); } } List result = new ArrayList<>(blockCount); @@ -403,13 +408,19 @@ synchronized long getServingReadFrames() { return servingReadFrames; } + static final class ServingReadBudgetException extends IOException { + ServingReadBudgetException() { + super("Serving source encoded-byte budget exceeded"); + } + } + synchronized long getServingReadBytes() { return servingReadBytes; } private void readIndexedRange(int laneId, long sequence, long segmentFirst, long first, long last, byte[] headerDigest, - long rangeFirst, byte[][] laneFrames) throws IOException { + long rangeFirst, byte[][] laneFrames, long maxEncodedBytes) throws IOException { try (FileChannel data = FileChannel.open(dataPath(laneId, sequence), StandardOpenOption.READ); FileChannel index = FileChannel.open(indexPath(laneId, sequence), @@ -438,6 +449,9 @@ private void readIndexedRange(int laneId, long sequence, long segmentFirst, || entry.getFrameOffset() > dataBytes - entry.getFrameLength()) { throw new IOException("State Archive serving block index range mismatch"); } + if (entry.getFrameLength() > maxEncodedBytes - servingReadBytes) { + throw new ServingReadBudgetException(); + } byte[] frame = readExact(data, entry.getFrameOffset(), entry.getFrameLength()); servingReadFrames++; servingReadBytes += frame.length; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java index 3de5768a507..3848b8889bf 100644 --- a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingIndexBuildCoordinatorV3.java @@ -146,6 +146,11 @@ public synchronized BuildProgress status() { return progress(); } + synchronized void flushRecoveryBatch() throws IOException { + requireOpen(); + flushPending(); + } + @Override public synchronized void close() throws IOException { if (!closed) { @@ -266,7 +271,7 @@ public static final class BuildProgress { private final int pendingBlocks; private final long buildSequence; - private BuildProgress(Mode mode, long indexedThrough, long committedThrough, + BuildProgress(Mode mode, long indexedThrough, long committedThrough, int pendingBlocks, long buildSequence) { this.mode = mode; this.indexedThrough = indexedThrough; diff --git a/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingWorkerV3.java b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingWorkerV3.java new file mode 100644 index 00000000000..5a707501f34 --- /dev/null +++ b/chainbase/src/main/java/org/tron/core/db2/archive/StateArchiveServingWorkerV3.java @@ -0,0 +1,258 @@ +package org.tron.core.db2.archive; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import org.tron.core.db2.archive.StateArchiveServingIndexBuildCoordinatorV3.BuildProgress; +import org.tron.core.db2.core.CommonCheckpointTarget; + +/** Single owner with a constant-size committed watermark mailbox, never a queue of bodies. */ +final class StateArchiveServingWorkerV3 implements AutoCloseable { + static final long MAX_SOURCE_BYTES = 32L * 1024 * 1024; + private final CoordinatorFactory factory; + private final StateArchiveFiveLaneSegmentWriterV3 source; + private final Object dispatch = new Object(); + private final Thread thread; + private final Runnable beforeBuild; + private CommonCheckpointTarget requested; + private CommonCheckpointTarget completed; + private CommonCheckpointTarget handoff; + private boolean live; + private boolean closing; + private long pendingSince; + private volatile IOException failure; + private volatile BuildProgress progress; + + StateArchiveServingWorkerV3(CoordinatorFactory factory, + StateArchiveFiveLaneSegmentWriterV3 source, Runnable beforeBuild) { + this.factory = factory; + this.source = source; + this.beforeBuild = beforeBuild; + progress = new BuildProgress(StateArchiveServingIndexBuildCoordinatorV3.Mode.BULK_CATCH_UP, + -1, -1, 0, 0); + thread = new Thread(this::run, "state-archive-serving-v3"); + thread.setDaemon(true); + thread.start(); + } + + void offer(CommonCheckpointTarget target) throws IOException { + synchronized (dispatch) { + synchronized (this) { + requireHealthy(); + if (requested != null && !requested.equals(target) + && (target.getFirstBlock().getBlockNumber() + != requested.getLastBlock().getBlockNumber() + 1 + || !java.util.Arrays.equals(target.getFirstBlock().getParentHash(), + requested.getLastBlock().getBlockHash()))) { + IOException gap = new IOException("Serving notification is not a committed successor"); + fail(gap); + throw gap; + } + requested = target; + if (pendingSince == 0) { + pendingSince = System.nanoTime(); + } + notifyAll(); + if (live) { + await(target, false); + } + } + } + } + + void completeInitialSync(CommonCheckpointTarget target) throws IOException { + synchronized (dispatch) { + synchronized (this) { + requireHealthy(); + if (requested == null || !requested.equals(target) || live || handoff != null) { + throw new IOException("Serving handoff differs from committed mailbox boundary"); + } + handoff = target; + notifyAll(); + await(target, true); + } + } + } + + synchronized BuildProgress status() { + BuildProgress snapshot = progress; + long committed = requested == null ? snapshot.getCommittedThrough() + : requested.getLastBlock().getBlockNumber(); + return new BuildProgress(closing ? StateArchiveServingIndexBuildCoordinatorV3.Mode.CLOSED + : failure != null ? StateArchiveServingIndexBuildCoordinatorV3.Mode.CATCH_UP_REQUIRED + : handoff != null && !live + ? StateArchiveServingIndexBuildCoordinatorV3.Mode.HANDOFF_DRAINING : snapshot.getMode(), + snapshot.getIndexedThrough(), committed, snapshot.getPendingBlocks(), + snapshot.getBuildSequence()); + } + + IOException failure() { + return failure; + } + + private void await(CommonCheckpointTarget target, boolean requireLive) throws IOException { + while (!target.equals(completed) || requireLive && !live) { + requireHealthy(); + try { + wait(); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + IOException cancelled = new IOException("Interrupted waiting for serving publication", + interrupted); + fail(cancelled); + throw cancelled; + } + } + requireHealthy(); + } + + private void requireHealthy() throws IOException { + if (failure != null) { + throw new IOException("Serving index requires recovery", failure); + } + if (closing) { + throw new IOException("Serving worker is closed"); + } + } + + private void run() { + StateArchiveServingIndexBuildCoordinatorV3 coordinator = null; + StateArchiveServingIndexBuildCoordinatorV3.LiveServingIndexer handle = null; + try { + coordinator = factory.open(); + progress = coordinator.status(); + while (true) { + CommonCheckpointTarget target; + boolean enterLive; + synchronized (this) { + while (!closing && failure == null && (requested == null || requested.equals(completed)) + && (handoff == null || live)) { + wait(); + } + if (closing || failure != null) { + return; + } + // Prototype budget: at most one second before flushing an undersized tail. + if (!live && handoff == null + && requested.getLastBlock().getBlockNumber() - progress.getIndexedThrough() < 1_000) { + long remaining = 1_000_000_000L - (System.nanoTime() - pendingSince); + if (remaining > 0) { + wait(Math.max(1, remaining / 1_000_000L)); + continue; + } + } + target = requested; + enterLive = handoff != null && !live; + } + beforeBuild.run(); + if (handle == null) { + long cursor = progress.getIndexedThrough(); + if (cursor < 0) { + cursor = source.getHistoryStartBlock() - 1; + } + long end = target.getLastBlock().getBlockNumber(); + if (cursor > end || cursor < 0) { + throw new IOException("Serving durable boundary is outside committed history"); + } + if (cursor == end) { + coordinator.recoverCommittedRange(Collections.emptyList(), target, true); + } + while (cursor < end) { + synchronized (this) { + if (closing || failure != null) { + return; + } + } + long batchEnd = Math.min(end, cursor + 1_000); + List batch; + while (true) { + try { + batch = source.readCommittedDiffs(cursor, batchEnd, MAX_SOURCE_BYTES); + break; + } catch (StateArchiveFiveLaneSegmentWriterV3.ServingReadBudgetException tooLarge) { + if (batchEnd == cursor + 1) { + throw tooLarge; + } + batchEnd = cursor + (batchEnd - cursor) / 2; + } + } + coordinator.recoverCommittedRange(batch, target, batchEnd == end); + coordinator.flushRecoveryBatch(); + progress = coordinator.status(); + cursor = batchEnd; + } + if (enterLive) { + handle = coordinator.completeInitialSync(target); + } + } else if (!target.equals(completed)) { + List batch = source.readCommittedDiffs( + progress.getIndexedThrough(), target.getLastBlock().getBlockNumber(), + MAX_SOURCE_BYTES); + handle.indexNow(batch, target); + } + progress = coordinator.status(); + synchronized (this) { + completed = target; + if (target.equals(requested)) { + pendingSince = 0; + } + live = failure == null && handle != null; + notifyAll(); + } + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + fail(new IOException("Serving owner interrupted", interrupted)); + } catch (IOException | RuntimeException buildFailure) { + if (coordinator != null) { + progress = coordinator.status(); + } + fail(new IOException("Serving build failed", buildFailure)); + } finally { + try { + if (coordinator != null) { + coordinator.close(); + } + } catch (IOException closeFailure) { + fail(closeFailure); + } + synchronized (this) { + notifyAll(); + } + } + } + + private synchronized void fail(IOException cause) { + failure = cause; + live = false; + progress = new BuildProgress(StateArchiveServingIndexBuildCoordinatorV3.Mode.CATCH_UP_REQUIRED, + progress.getIndexedThrough(), requested == null ? -1 + : requested.getLastBlock().getBlockNumber(), 0, progress.getBuildSequence()); + org.slf4j.LoggerFactory.getLogger("DB").error("Archive serving owner degraded", cause); + notifyAll(); + } + + @Override + public void close() throws IOException { + synchronized (this) { + closing = true; + notifyAll(); + } + boolean interrupted = false; + while (thread.isAlive()) { + try { + thread.join(); + } catch (InterruptedException retry) { + interrupted = true; + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted closing serving owner; owner has terminated"); + } + } + + interface CoordinatorFactory { + StateArchiveServingIndexBuildCoordinatorV3 open() throws IOException; + } +} diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java index 0f24508fc74..b8e50031cad 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointMaterializer.java @@ -33,6 +33,10 @@ void materialize(CommonCheckpointPayload payload, CommonCheckpointTarget target) /** Idempotently publishes this authority's already-materialized exact target. */ void publish(CommonCheckpointTarget target) throws IOException; + /** Best-effort derived work, invoked only after durable redo and WAL retirement complete. */ + default void afterCommit(CommonCheckpointTarget target) { + } + enum Authority { CHAINBASE, PATH_STATE, diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java index 67e6c666156..fd9f0094f08 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinator.java @@ -53,6 +53,12 @@ public CommonCheckpointRedoCoordinator(CommonCheckpointFile checkpointFile, /** Durably publishes the redo payload before applying it to any authority. */ public synchronized RecoveryAction apply(CommonCheckpointPayload payload) throws IOException { + RecoveryAction action = applyDurable(payload); + notifyCommitted(CommonCheckpointTarget.from(payload)); + return action; + } + + synchronized RecoveryAction applyDurable(CommonCheckpointPayload payload) throws IOException { requireOpen(); CommonCheckpointPayload admitted = Objects.requireNonNull(payload, "payload"); Timing timing = new Timing("apply", CommonCheckpointTarget.from(admitted), @@ -82,6 +88,7 @@ public synchronized RecoveryAction recover() throws IOException { RecoveryAction action = redo(loaded.value, timing); timing.totalUs = elapsedUs(totalStart); emitTiming(timing); + notifyCommitted(CommonCheckpointTarget.from(loaded.value)); return action; } @@ -94,6 +101,17 @@ synchronized void requireMaterializer(Authority authority, } } + synchronized void notifyCommitted(CommonCheckpointTarget target) { + for (Authority authority : ORDER) { + try { + materializers.get(authority).afterCommit(target); + } catch (RuntimeException failure) { + logger.error("Derived post-commit work failed for {} at {}", authority, + target.getLastBlock().getBlockNumber(), failure); + } + } + } + /** Requires every authority to expose the same fully published startup target. */ synchronized void requirePublished(CommonCheckpointTarget target) throws IOException { requireOpen(); diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java index f4a0781e402..08b91ff3205 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntime.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Objects; import java.util.function.LongSupplier; +import org.tron.core.db2.archive.StateArchiveAppendCheckpointMaterializerV3; import org.tron.core.db2.archive.StateArchiveCheckpointMaterializer; import org.tron.core.db2.archive.StateArchiveCheckpointPlanner; import org.tron.core.db2.archive.StateArchiveCheckpointReadSnapshot; @@ -141,8 +142,15 @@ public synchronized CommonCheckpointRedoCoordinator.RecoveryAction recoverBefore publishedTarget = archivePlanner == null ? StateArchiveCheckpointMaterializer.loadPublishedTargetIfPresent( archiveDirectory, formatIdentity, engine, materializedStore).orElse(null) : null; + if (archivePlanner instanceof StateArchiveAppendCheckpointMaterializerV3) { + publishedTarget = ((StateArchiveAppendCheckpointMaterializerV3) archivePlanner) + .loadPublishedTargetIfPresent().orElse(null); + } if (publishedTarget != null) { owner.requirePublishedBeforeServing(publishedTarget); + if (archivePlanner != null) { + archivePlanner.afterCommit(publishedTarget); + } } return action; } catch (IOException | RuntimeException failure) { diff --git a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java index 2a17fc0f207..c65ff14fef7 100644 --- a/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java +++ b/chainbase/src/main/java/org/tron/core/db2/core/CommonCheckpointRuntimeOwner.java @@ -73,10 +73,11 @@ CommonCheckpointRedoCoordinator.RecoveryAction apply(CommonCheckpointPayload pay requireState(State.READY, "common checkpoint runtime is not ready to flush"); state = State.CHECKPOINTING; try { - CommonCheckpointRedoCoordinator.RecoveryAction action = coordinator.apply( + CommonCheckpointRedoCoordinator.RecoveryAction action = coordinator.applyDurable( Objects.requireNonNull(payload, "payload")); Objects.requireNonNull(completion, "completion").run(); state = State.READY; + coordinator.notifyCommitted(CommonCheckpointTarget.from(payload)); return action; } catch (IOException | RuntimeException failure) { state = State.FAILED; diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index cf890da8bf8..62b3564777b 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -3834,15 +3834,20 @@ private void updateStateArchiveServingMode(boolean syncSource) { if (materializer == null || syncSource || stateArchiveServingLive) { return; } + java.util.Optional published; + try { + published = materializer.loadPublishedTargetIfPresent(); + } catch (java.io.IOException failure) { + throw new IllegalStateException("State Archive authority verification failed", failure); + } try { - java.util.Optional published = - materializer.loadPublishedTargetIfPresent(); if (published.isPresent()) { materializer.completeServingInitialSync(published.get()); stateArchiveServingLive = true; } } catch (java.io.IOException failure) { - throw new IllegalStateException("State Archive serving-index handoff failed", failure); + logger.error("State Archive serving-index handoff failed; historical queries unavailable", + failure); } } diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java index 4d9e37ecce7..277ed875164 100644 --- a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveAppendCheckpointMaterializerV3Test.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -65,8 +66,10 @@ public void preparesBeforeWalPublishesAndReopensExactTarget() throws Exception { try (StateArchiveAppendCheckpointMaterializerV3 reopened = materializer( root, format, baseline, 10_000)) { assertEquals(Status.PUBLISHED, reopened.inspect(target)); + reopened.afterCommit(target); + reopened.completeServingInitialSync(target); assertEquals(1, reopened.servingIndexStatus().getIndexedThrough()); - assertEquals(StateArchiveServingIndexBuildCoordinatorV3.Mode.BULK_CATCH_UP, + assertEquals(StateArchiveServingIndexBuildCoordinatorV3.Mode.LIVE_IMMEDIATE, reopened.servingIndexStatus().getMode()); reopened.materialize(payload, target); reopened.publish(target); @@ -118,6 +121,49 @@ public void persistsRotationSpanningSap3DuringPrepare() throws Exception { } } + @Test(timeout = 15000) + public void commonRetiresWhileDerivedBuilderIsBlocked() throws Exception { + Path root = temporaryFolder.newFolder("isolated-serving").toPath(); + java.util.concurrent.CountDownLatch entered = new java.util.concurrent.CountDownLatch(1); + java.util.concurrent.CountDownLatch release = new java.util.concurrent.CountDownLatch(1); + StateArchiveAppendCheckpointMaterializerV3 archive = + new StateArchiveAppendCheckpointMaterializerV3(root.resolve("history"), hash(71), + Engine.LEVELDB, hash(81), StateArchiveFileFormatV3.COMPRESSION_NONE, 10000, () -> { + entered.countDown(); + try { + if (!release.await(5, java.util.concurrent.TimeUnit.SECONDS)) { + throw new IllegalStateException("test release timed out"); + } + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } + }); + CommonCheckpointFile wal = new CommonCheckpointFile(root.resolve("wal")); + CommonCheckpointRedoCoordinator coordinator = new CommonCheckpointRedoCoordinator(wal, + new FakeMaterializer(Authority.CHAINBASE), new FakeMaterializer(Authority.PATH_STATE), + archive); + try { + List diffs = Collections.singletonList(diff(1, 32)); + StateArchiveHotBatchDescriptor descriptor = archive.planCheckpoint(diffs); + CommonCheckpointPayload payload = payload(hash(71), diffs, descriptor); + CommonCheckpointTarget target = archive.prepare( + CommonCheckpointCapture.create(payload, diffs, descriptor)); + coordinator.apply(payload); + assertTrue(entered.await(5, java.util.concurrent.TimeUnit.SECONDS)); + assertEquals(CommonCheckpointRedoCoordinator.RecoveryAction.NO_CHECKPOINT, + coordinator.recover()); + assertEquals(Status.PUBLISHED, archive.inspect(target)); + assertEquals(-1, archive.servingIndexStatus().getIndexedThrough()); + release.countDown(); + archive.completeServingInitialSync(target); + assertEquals(1, archive.servingIndexStatus().getIndexedThrough()); + } finally { + release.countDown(); + coordinator.close(); + } + } + @Test public void advancesTwoPublishedTargetsAndPreservesFreshHotBindingBytes() throws Exception { Path root = temporaryFolder.newFolder("append-materializer-sequential").toPath(); @@ -139,7 +185,7 @@ public void advancesTwoPublishedTargetsAndPreservesFreshHotBindingBytes() throws assertEquals(Status.PUBLISHED, archive.inspect(firstTarget)); assertEquals(StateArchiveServingIndexBuildCoordinatorV3.Mode.BULK_CATCH_UP, archive.servingIndexStatus().getMode()); - assertEquals(1, archive.servingIndexStatus().getPendingBlocks()); + archive.afterCommit(firstTarget); archive.completeServingInitialSync(firstTarget); assertEquals(StateArchiveServingIndexBuildCoordinatorV3.Mode.LIVE_IMMEDIATE, archive.servingIndexStatus().getMode()); @@ -154,6 +200,7 @@ public void advancesTwoPublishedTargetsAndPreservesFreshHotBindingBytes() throws secondPayload, Collections.singletonList(second), secondDescriptor)); assertEquals(Status.MATERIALIZED, archive.inspect(secondTarget)); archive.publish(secondTarget); + archive.afterCommit(secondTarget); assertEquals(Status.PUBLISHED, archive.inspect(secondTarget)); assertEquals(2, archive.servingIndexStatus().getIndexedThrough()); assertEquals(0, archive.servingIndexStatus().getPendingBlocks()); diff --git a/framework/src/test/java/org/tron/core/db2/archive/StateArchiveServingWorkerV3Test.java b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveServingWorkerV3Test.java new file mode 100644 index 00000000000..bc7332b42d9 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db2/archive/StateArchiveServingWorkerV3Test.java @@ -0,0 +1,154 @@ +package org.tron.core.db2.archive; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.core.db2.archive.StateArchiveServingIndexBuildCoordinatorV3.Mode; +import org.tron.core.db2.core.CommonCheckpointTarget; +import org.tron.core.db2.stateroot.PathStateStoreManifest.Engine; + +public class StateArchiveServingWorkerV3Test { + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test(timeout = 15000) + public void blockedBuilderAllowsCoalescedOffersAndOrderedHandoffThenLive() throws Exception { + Path root = temporaryFolder.newFolder().toPath(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + try (StateArchiveFiveLaneSegmentWriterV3 source = source(root)) { + append(source, 1); + StateArchiveServingWorkerV3 worker = worker(root, source, () -> { + entered.countDown(); + try { + if (!release.await(5, TimeUnit.SECONDS)) { + throw new IllegalStateException("test release timed out"); + } + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } + }); + try { + worker.offer(target(1, 1)); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + append(source, 2); + worker.offer(target(2, 2)); + assertEquals(2, worker.status().getCommittedThrough()); + assertEquals(-1, worker.status().getIndexedThrough()); + release.countDown(); + worker.completeInitialSync(target(2, 2)); + assertEquals(2, worker.status().getIndexedThrough()); + assertEquals(Mode.LIVE_IMMEDIATE, worker.status().getMode()); + append(source, 3); + worker.offer(target(3, 3)); + assertEquals(3, worker.status().getIndexedThrough()); + long sequence = worker.status().getBuildSequence(); + worker.offer(target(3, 3)); + assertEquals(sequence, worker.status().getBuildSequence()); + assertThrows(IOException.class, () -> worker.offer(target(5, 5))); + } finally { + release.countDown(); + worker.close(); + } + } + } + + @Test(timeout = 15000) + public void failedOwnerIsObservableAndRestartReplaysLostNotification() throws Exception { + Path root = temporaryFolder.newFolder().toPath(); + try (StateArchiveFiveLaneSegmentWriterV3 source = source(root)) { + append(source, 1); + try (StateArchiveServingWorkerV3 failed = worker(root, source, () -> { + throw new IllegalStateException("injected builder failure"); + })) { + failed.offer(target(1, 1)); + assertThrows(IOException.class, () -> failed.completeInitialSync(target(1, 1))); + assertEquals(Mode.CATCH_UP_REQUIRED, failed.status().getMode()); + assertNotNull(failed.failure()); + assertEquals(-1, failed.status().getIndexedThrough()); + } + // No volatile mailbox survives. The recovered Common target is sufficient. + try (StateArchiveServingWorkerV3 recovered = worker(root, source, () -> { })) { + recovered.offer(target(1, 1)); + recovered.completeInitialSync(target(1, 1)); + assertEquals(1, recovered.status().getIndexedThrough()); + } + try (StateArchiveServingWorkerV3 again = worker(root, source, () -> { })) { + again.offer(target(1, 1)); + again.completeInitialSync(target(1, 1)); + assertEquals(0, again.status().getBuildSequence()); + } + } + } + + @Test(timeout = 15000) + public void indexOpenFailureDoesNotThrowOnConstruction() throws Exception { + Path root = temporaryFolder.newFolder().toPath(); + try (StateArchiveFiveLaneSegmentWriterV3 source = source(root); + StateArchiveServingWorkerV3 worker = new StateArchiveServingWorkerV3(() -> { + throw new IOException("injected index open failure"); + }, source, () -> { })) { + assertThrows(IOException.class, () -> { + worker.offer(target(1, 1)); + worker.completeInitialSync(target(1, 1)); + }); + assertEquals(Mode.CATCH_UP_REQUIRED, worker.status().getMode()); + } + } + + @Test + public void readBudgetRejectsBeforeAllocatingAnOversizedFrame() throws Exception { + Path root = temporaryFolder.newFolder().toPath(); + try (StateArchiveFiveLaneSegmentWriterV3 source = source(root)) { + append(source, 1); + assertThrows(StateArchiveFiveLaneSegmentWriterV3.ServingReadBudgetException.class, + () -> source.readCommittedDiffs(0, 1, 1)); + assertEquals(0, source.getServingReadBytes()); + assertEquals(1, source.readCommittedDiffs(0, 1).size()); + } + } + + private StateArchiveServingWorkerV3 worker(Path root, + StateArchiveFiveLaneSegmentWriterV3 source, Runnable hook) { + return new StateArchiveServingWorkerV3( + () -> new StateArchiveServingIndexBuildCoordinatorV3(root, Engine.LEVELDB, 1000), + source, hook); + } + + private StateArchiveFiveLaneSegmentWriterV3 source(Path root) throws IOException { + return new StateArchiveFiveLaneSegmentWriterV3(root, hash(90), + StateArchiveFileFormatV3.COMPRESSION_NONE, 1500); + } + + private void append(StateArchiveFiveLaneSegmentWriterV3 source, int block) throws IOException { + source.append(new StateArchiveFiveLaneBlockCodecV3().encode(diff(block), + source.getResultHistoryDigest(), StateArchiveFileFormatV3.COMPRESSION_NONE)); + } + + private static BlockReverseDiff diff(int block) { + return new BlockReverseDiff(BlockSnapshotMeta.forBlock(block, hash(block), hash(block - 1), + block * 3000L), Collections.emptyList()); + } + + private static CommonCheckpointTarget target(int first, int last) { + return CommonCheckpointTarget.restore(hash(70), hash(last + 80), diff(first).getMeta(), + diff(last).getMeta(), hash(first + 30), hash(last + 31)); + } + + private static byte[] hash(int value) { + byte[] hash = new byte[32]; + hash[31] = (byte) value; + return hash; + } +} diff --git a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java index 9c3aaf0f147..0654c736089 100644 --- a/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java +++ b/framework/src/test/java/org/tron/core/db2/core/CommonCheckpointRedoCoordinatorTest.java @@ -82,6 +82,20 @@ public void recordsDeterministicRedoPhaseTimingsWithoutChangingBarrierCalls() assertTrue(timing.getTotalUs() > 0); } + @Test + public void derivedFailureOccursAfterRetirementAndCannotFailCommon() throws Exception { + Fixture fixture = fixture("derived-failure", null); + FakeMaterializer archive = fixture.materializers.get(2); + archive.afterCommitHook = () -> { + assertFalse(Files.exists(fixture.file.getCheckpointPath())); + assertFalse(archive.scopeOpen); + throw new IllegalStateException("injected derived failure"); + }; + assertEquals(RecoveryAction.COMPLETED_REDO, fixture.coordinator.apply(fixture.payload)); + assertEquals(RecoveryAction.NO_CHECKPOINT, fixture.coordinator.recover()); + assertEquals(Status.PUBLISHED, archive.status); + } + @Test public void ignoresTimingSinkFailureAfterDurableCheckpointCompletes() throws Exception { Fixture fixture = fixture("timing-sink-failure", null); @@ -403,6 +417,12 @@ private static final class FakeMaterializer implements CommonCheckpointMateriali private int closed; private boolean closeFailure; private boolean scopeOpen; + private Runnable afterCommitHook = () -> { }; + + @Override + public void afterCommit(CommonCheckpointTarget expected) { + afterCommitHook.run(); + } private FakeMaterializer(Authority authority, List actions) { this.authority = authority; From abd9b1bbfb07f9519bf572b580aef812427c964f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 19:48:06 +0800 Subject: [PATCH 139/161] build(chainbase): support high rocksdb x86 candidate --- build.gradle | 8 ++++-- .../org/tron/common/math/MathWrapper.java | 24 ++++++++++++++++ .../MarketOrderPriceComparatorForRocksDB.java | 28 +++++++++++++++++++ .../MarketOrderPriceComparatorForRocksDB.java | 2 -- 4 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 platform/src/main/java/x86-high/org/tron/common/math/MathWrapper.java create mode 100644 platform/src/main/java/x86-high/org/tron/common/utils/MarketOrderPriceComparatorForRocksDB.java diff --git a/build.gradle b/build.gradle index e143ab3a947..02890cc1e9a 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,8 @@ allprojects { def arch = System.getProperty("os.arch").toLowerCase(Locale.ROOT) def javaVersion = JavaVersion.current() def isArm64 = Architectures.AARCH64.isAlias(arch) -def archSource = isArm64 ? "arm" : "x86" +def requestedRocksdbVersion = findProperty('rocksdbVersion') +def archSource = isArm64 ? "arm" : (requestedRocksdbVersion == '9.7.4' ? "x86-high" : "x86") def isMac = OperatingSystem.current().isMacOsX() ext.archInfo = [ @@ -37,7 +38,10 @@ ext.archInfo = [ ], requires: [ JavaVersion: isArm64 ? JavaVersion.VERSION_17 : JavaVersion.VERSION_1_8, - RocksdbVersion: isArm64 ? '9.7.4' : '5.15.10', + // Allow an explicitly pinned validation candidate to use the same JNI + // generation across architectures; the default remains the legacy x86 + // baseline until the candidate passes the AMD compatibility gate. + RocksdbVersion: requestedRocksdbVersion ?: (isArm64 ? '9.7.4' : '5.15.10'), // https://github.com/grpc/grpc-java/issues/7690 // https://github.com/grpc/grpc-java/pull/12319, Add support for macOS aarch64 with universal binary // https://github.com/grpc/grpc-java/pull/11371 , 1.64.x is not supported CentOS 7. diff --git a/platform/src/main/java/x86-high/org/tron/common/math/MathWrapper.java b/platform/src/main/java/x86-high/org/tron/common/math/MathWrapper.java new file mode 100644 index 00000000000..7bfb87ae1da --- /dev/null +++ b/platform/src/main/java/x86-high/org/tron/common/math/MathWrapper.java @@ -0,0 +1,24 @@ +package org.tron.common.math; + +/** Java 8 math compatibility implementation for the high-version x86 build. */ +@Deprecated +public class MathWrapper { + + public static double pow(double a, double b) { return Math.pow(a, b); } + public static long addExact(long x, long y) { return Math.addExact(x, y); } + public static int addExact(int x, int y) { return Math.addExact(x, y); } + public static long floorDiv(long x, long y) { return Math.floorDiv(x, y); } + public static int multiplyExact(int x, int y) { return Math.multiplyExact(x, y); } + public static long multiplyExact(long x, long y) { return Math.multiplyExact(x, y); } + public static long subtractExact(long x, long y) { return Math.subtractExact(x, y); } + public static int min(int a, int b) { return Math.min(a, b); } + public static long min(long a, long b) { return Math.min(a, b); } + public static int max(int a, int b) { return Math.max(a, b); } + public static long max(long a, long b) { return Math.max(a, b); } + public static int round(float a) { return Math.round(a); } + public static long round(double a) { return Math.round(a); } + public static double ceil(double a) { return Math.ceil(a); } + public static double signum(double a) { return Math.signum(a); } + public static double random() { return Math.random(); } + public static long abs(long a) { return Math.abs(a); } +} diff --git a/platform/src/main/java/x86-high/org/tron/common/utils/MarketOrderPriceComparatorForRocksDB.java b/platform/src/main/java/x86-high/org/tron/common/utils/MarketOrderPriceComparatorForRocksDB.java new file mode 100644 index 00000000000..6f558594ebc --- /dev/null +++ b/platform/src/main/java/x86-high/org/tron/common/utils/MarketOrderPriceComparatorForRocksDB.java @@ -0,0 +1,28 @@ +package org.tron.common.utils; + +import java.nio.ByteBuffer; +import org.rocksdb.AbstractComparator; +import org.rocksdb.ComparatorOptions; + +public class MarketOrderPriceComparatorForRocksDB extends AbstractComparator { + + public MarketOrderPriceComparatorForRocksDB(final ComparatorOptions copt) { + super(copt); + } + + @Override + public String name() { + return "MarketOrderPriceComparator"; + } + + @Override + public int compare(final ByteBuffer a, final ByteBuffer b) { + return MarketComparator.comparePriceKey(convertDataToBytes(a), convertDataToBytes(b)); + } + + public byte[] convertDataToBytes(ByteBuffer buf) { + byte[] bytes = new byte[buf.remaining()]; + buf.get(bytes); + return bytes; + } +} diff --git a/platform/src/main/java/x86/org/tron/common/utils/MarketOrderPriceComparatorForRocksDB.java b/platform/src/main/java/x86/org/tron/common/utils/MarketOrderPriceComparatorForRocksDB.java index be406ff658d..ffb39c5f638 100644 --- a/platform/src/main/java/x86/org/tron/common/utils/MarketOrderPriceComparatorForRocksDB.java +++ b/platform/src/main/java/x86/org/tron/common/utils/MarketOrderPriceComparatorForRocksDB.java @@ -26,11 +26,9 @@ public int compare(final DirectSlice a, final DirectSlice b) { public byte[] convertDataToBytes(DirectSlice directSlice) { int capacity = directSlice.data().capacity(); byte[] bytes = new byte[capacity]; - for (int i = 0; i < capacity; i++) { bytes[i] = directSlice.get(i); } - return bytes; } From 48712b3f09be44bfe801cb147b86cbaa6bb2d1fa Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 21:33:55 +0800 Subject: [PATCH 140/161] build(chainbase): allow explicit amd jdk17 candidate --- build.gradle | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 02890cc1e9a..92fb56849d9 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ def arch = System.getProperty("os.arch").toLowerCase(Locale.ROOT) def javaVersion = JavaVersion.current() def isArm64 = Architectures.AARCH64.isAlias(arch) def requestedRocksdbVersion = findProperty('rocksdbVersion') +def java17Candidate = findProperty('java17Candidate') == 'true' def archSource = isArm64 ? "arm" : (requestedRocksdbVersion == '9.7.4' ? "x86-high" : "x86") def isMac = OperatingSystem.current().isMacOsX() @@ -37,7 +38,8 @@ ext.archInfo = [ ] ], requires: [ - JavaVersion: isArm64 ? JavaVersion.VERSION_17 : JavaVersion.VERSION_1_8, + JavaVersion: (isArm64 || java17Candidate) + ? JavaVersion.VERSION_17 : JavaVersion.VERSION_1_8, // Allow an explicitly pinned validation candidate to use the same JNI // generation across architectures; the default remains the legacy x86 // baseline until the candidate passes the AMD compatibility gate. @@ -47,7 +49,9 @@ ext.archInfo = [ // https://github.com/grpc/grpc-java/pull/11371 , 1.64.x is not supported CentOS 7. ProtocGenVersion: isArm64 || isMac ? '1.81.0' : '1.60.0' ], - VMOptions: isArm64 ? "${rootDir}/gradle/jdk17/java-tron.vmoptions" : "${rootDir}/gradle/java-tron.vmoptions" + VMOptions: (isArm64 || java17Candidate) + ? "${rootDir}/gradle/jdk17/java-tron.vmoptions" + : "${rootDir}/gradle/java-tron.vmoptions" ] if (!archInfo.java.is(archInfo.requires.JavaVersion)) { From 9ea27f27972b549f4665ccdf81c776355a2f9faf Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 21:45:44 +0800 Subject: [PATCH 141/161] feat(platform): allow opt-in x86 java17 candidate --- .../src/main/java/common/org/tron/common/arch/Arch.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/platform/src/main/java/common/org/tron/common/arch/Arch.java b/platform/src/main/java/common/org/tron/common/arch/Arch.java index 999bb631bea..1409d99fea2 100644 --- a/platform/src/main/java/common/org/tron/common/arch/Arch.java +++ b/platform/src/main/java/common/org/tron/common/arch/Arch.java @@ -6,6 +6,9 @@ @Slf4j(topic = "arch") public final class Arch { + /** Explicit opt-in for the x86 Java 17 validation candidate. */ + public static final String JAVA17_X86_CANDIDATE_PROPERTY = "tron.java17.x86.candidate"; + private Arch() { } @@ -74,8 +77,12 @@ public static boolean isJava17() { return javaSpecificationVersion().equals("17"); } + public static boolean isJava17X86Candidate() { + return Boolean.parseBoolean(System.getProperty(JAVA17_X86_CANDIDATE_PROPERTY, "false")); + } + public static void throwIfUnsupportedJavaVersion() { - if ((isX86() && !isJava8()) || (isArm64() && !isJava17())) { + if ((isX86() && !isJava8() && !isJava17X86Candidate()) || (isArm64() && !isJava17())) { logger.info(withAll()); throw new UnsupportedOperationException(String.format( "Java %s is required for %s architecture. Detected version %s", isX86() ? "1.8" : "17", From 9d6d040988de7c5efc66a02afd2036c886791979 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 21:49:26 +0800 Subject: [PATCH 142/161] docs(archive): record amd jdk17 candidate gate --- .../execution/records/ITER-20260910-397.md | 18 ++++++++ .../ai-archive/execution/tasks/TASK-023.md | 42 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260910-397.md create mode 100644 .helper/ai-archive/execution/tasks/TASK-023.md diff --git a/.helper/ai-archive/execution/records/ITER-20260910-397.md b/.helper/ai-archive/execution/records/ITER-20260910-397.md new file mode 100644 index 00000000000..8e2b3ef61e1 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260910-397.md @@ -0,0 +1,18 @@ +# ITER-20260910-397 + +## Slice + +HLT-002 AMD-002 JDK17/RocksDB 9.7.4 candidate gate and baseline restoration. + +## Observed + +- AMD-002 installed OpenJDK `17.0.20` at `/usr/lib/jvm/java-17-openjdk-amd64/bin/java`; installation changed the system `java` alternative, so the live control was relaunched with the explicit JDK8 binary `/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java`. +- Initial JDK17 smoke failed closed before database open with `Java 1.8 is required for amd64 architecture. Detected version 17`. This was an application architecture gate, not a RocksDB JNI failure. +- Commit `9ea27f2797` adds the opt-in system property `tron.java17.x86.candidate`; default x86 behavior remains Java8-only. The commit is signed and pushed to `origin/feature/archive_block2`. +- Rebuilt JDK17 + RocksDB 9.7.4 candidate on AMD-002; build succeeded and produced SHA-256 `91a0560fc9a4581d9f5643e0233c60772be00cfb8945154760942f83af7ef76e`. +- With `-Dtron.java17.x86.candidate=true` and P2P disabled, candidate startup succeeded. Existing 27 PathState stores, Chainbase LevelDB stores, Common checkpoint, and State Archive stores opened; no JNI/linkage exception observed. Candidate unit was stopped after the startup gate, so no performance claim is made. +- AMD-002 control was restored using explicit JDK8, unit `amd002-state-archive-current-8ac7a6afb1-live.service`; follow-up verification showed active PID and ports 8090/18888/9527 listening. + +## Conclusion + +JDK17 application startup compatibility is now proven for the existing AMD snapshot when explicitly opted in, while the production/default x86 gate remains unchanged. HLT-002 still lacks an attributable P2P performance window and long-term compaction evidence; keep the 9.7.4/JDK17 runtime as a candidate only. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md new file mode 100644 index 00000000000..253f0d3d893 --- /dev/null +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -0,0 +1,42 @@ +--- +id: TASK-023 +type: task +title: HLT-002 AMD JDK17 RocksDB long-sync performance +status: in_progress +priority: high +parent: HLT-002 +owner: AI-archive +created: 2026-09-10 +updated: 2026-09-10 +active_worktree: . +active_branch: feature/archive_block2 +--- + +# HLT-002 AMD/JDK17/RocksDB长期同步性能 + +## 目标 + +在 AMD-002 上建立与 ARM-001 可比的 JDK17 + RocksDB 高版本长期同步基线,逐项验证 JVM、JNI、native options、cgroup/cache 与磁盘基础设施对 PushBlock、PathState、Common checkpoint、State Archive 和 compaction 的影响。 + +## 当前基线 + +- AMD-002 live control:JDK8、Chainbase LevelDB、PathState/Hot/serving RocksDB 5.15.10,Xmx18G/direct1G,MemoryHigh=29G、MemoryMax=30G、swap=0。 +- `abd9b1bbfb`:RocksDB JNI 9.7.4 在当前完整 runtime reflink 上启动兼容通过;P2P 窗口 60s=8 blocks、180s=20 blocks(约0.11–0.13 block/s),随后回滚;候选 runtime/evidence 保留。 +- AMD-002 已安装 OpenJDK 17.0.20,路径 `/usr/lib/jvm/java-17-openjdk-amd64/bin/java`;系统默认 `java` 已切换为 JDK17,因此 control 必须使用显式 JDK8 路径 `/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java`。 +- `9ea27f2797` 增加 `-Dtron.java17.x86.candidate=true` 的显式 x86 JDK17 候选门禁;JDK17+RocksDB9.7.4 候选在现有快照上 P2P-disabled 启动成功,27 个 PathState store、Common checkpoint 与 State Archive 数据库均打开。 + +## 实验顺序 + +1. 固定 JDK8/5.15.10 control 的 config、runtime、head/hash、JVM/cgroup/cache 与 30m 观测合同。 +2. 只切换 JDK17,保持 5.15.10、配置和运行目录语义不变,完成 P2P-disabled 启动与短窗。 +3. 在 JDK17 control 上只切换 RocksDB JNI 9.7.4,完成兼容/恢复门。 +4. 最后单变量调 native options(block cache、write buffer、max open files、compaction),每次保留失败副本。 +5. 通过固定输入后再进行 30m/6h 长期窗口;性能、恢复、容量和生产切换分别判门。 + +## 禁止 + +不得原地转换 LevelDB;不得把 JDK 安装、JNI 升级和 options 调优合并成不可归因的一轮;不得删除 control 或失败 runtime;不得以短窗提升关闭长期性能 Gate。 + +## 下一动作 + +在显式 JDK8 control 下固定一次同步窗口;随后只启用 `-Dtron.java17.x86.candidate=true` 做 JDK17+RocksDB9.7.4 P2P 短窗,再决定是否进入 native options 与长期窗口。候选启动失败或性能不足时保留 runtime/evidence 并恢复 control。 From b4c38bfcee11124fc11adecbf71b3873aade01b1 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 22:11:57 +0800 Subject: [PATCH 143/161] docs(archive): record amd jdk17 p2p window --- .../execution/records/ITER-20260910-398.md | 17 +++++++++++++++++ .helper/ai-archive/execution/tasks/TASK-023.md | 1 + 2 files changed, 18 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260910-398.md diff --git a/.helper/ai-archive/execution/records/ITER-20260910-398.md b/.helper/ai-archive/execution/records/ITER-20260910-398.md new file mode 100644 index 00000000000..50c96c20007 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260910-398.md @@ -0,0 +1,17 @@ +# ITER-20260910-398 + +## Slice + +HLT-002 AMD-002 JDK17 + RocksDB 9.7.4 P2P short window. + +## Observed + +- Candidate unit `amd002-hlt002-jdk17-rocks97-9ea27f2797` started with `/usr/lib/jvm/java-17-openjdk-amd64/bin/java`, `-Dtron.java17.x86.candidate=true`, existing snapshot runtime, and P2P enabled. +- Window start via `/wallet/getnowblock`: block `85150075`. +- After approximately 120 seconds, head was `85150103`: +28 blocks, approximately `0.23 block/s`. +- Candidate logs showed peer block reception and no `ERROR`, `Exception`, or `Caused by` entries in the sampled tail. +- Candidate was stopped; AMD-002 control was restored with explicit JDK8 binary `/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java`. Unit active with `NRestarts=0`; ports 8090/18888/9527 listening. + +## Conclusion + +The JDK17/RocksDB9.7.4 candidate passes a short P2P liveness window on the existing snapshot. Because the candidate started from an older snapshot than the live control, the +28-block rate is mechanism evidence only, not a strict A/B performance promotion. A synchronized control/candidate pair and longer window remain required before native-options tuning or 30m/6h gates. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index 253f0d3d893..c046c29d650 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -24,6 +24,7 @@ active_branch: feature/archive_block2 - `abd9b1bbfb`:RocksDB JNI 9.7.4 在当前完整 runtime reflink 上启动兼容通过;P2P 窗口 60s=8 blocks、180s=20 blocks(约0.11–0.13 block/s),随后回滚;候选 runtime/evidence 保留。 - AMD-002 已安装 OpenJDK 17.0.20,路径 `/usr/lib/jvm/java-17-openjdk-amd64/bin/java`;系统默认 `java` 已切换为 JDK17,因此 control 必须使用显式 JDK8 路径 `/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java`。 - `9ea27f2797` 增加 `-Dtron.java17.x86.candidate=true` 的显式 x86 JDK17 候选门禁;JDK17+RocksDB9.7.4 候选在现有快照上 P2P-disabled 启动成功,27 个 PathState store、Common checkpoint 与 State Archive 数据库均打开。 +- 同一候选 P2P 窗口约 120 秒从区块 85150075 到 85150103(+28,约 0.23 block/s),未见 ERROR/Exception;窗口结束后已恢复 JDK8 control。该样本起点为候选旧快照,不能与实时 control 直接作严格 A/B 晋级结论。 ## 实验顺序 From 55463c9ee98656aee7e391e8fafc27265fc4e60f Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 22:37:17 +0800 Subject: [PATCH 144/161] docs(archive): record amd same snapshot ab --- .../execution/records/ITER-20260910-399.md | 16 ++++++++++++++++ .helper/ai-archive/execution/tasks/TASK-023.md | 1 + 2 files changed, 17 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260910-399.md diff --git a/.helper/ai-archive/execution/records/ITER-20260910-399.md b/.helper/ai-archive/execution/records/ITER-20260910-399.md new file mode 100644 index 00000000000..78f23269ad9 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260910-399.md @@ -0,0 +1,16 @@ +# ITER-20260910-399 + +## Slice + +HLT-002 AMD-002 same-snapshot sequential A/B short window. + +## Observed + +- While control was stopped, two reflink runtimes were created from the same `runtime-current-8ac7a6afb1` snapshot; existing runtimes and failed candidates were retained. +- JDK8 control (`amd002-hlt002-ab-control-9ea27f2797`): measured 120 seconds from block `85150420` to `85150443`, +23 blocks, approximately `0.192 block/s`. +- JDK17 + RocksDB 9.7.4 candidate (`amd002-hlt002-ab-jdk17-9ea27f2797`): measured 120 seconds from block `85150409` to `85150435`, +26 blocks, approximately `0.217 block/s`. +- Candidate produced no sampled `ERROR`, `Exception`, or `Caused by` lines. Both units were stopped cleanly. AMD-002 original live was restored with explicit JDK8 and remains the active control. + +## Conclusion + +The synchronized snapshot experiment provides directional evidence of approximately 13% higher short-window throughput for the JDK17/RocksDB9.7.4 candidate. It is not a promotion result: runs were sequential, network conditions were not randomized, and each window is only 120 seconds. Repeat interleaved windows and collect checkpoint/compaction/resource metrics before native-options tuning or long-term gates. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index c046c29d650..41b9ce9bed7 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -25,6 +25,7 @@ active_branch: feature/archive_block2 - AMD-002 已安装 OpenJDK 17.0.20,路径 `/usr/lib/jvm/java-17-openjdk-amd64/bin/java`;系统默认 `java` 已切换为 JDK17,因此 control 必须使用显式 JDK8 路径 `/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java`。 - `9ea27f2797` 增加 `-Dtron.java17.x86.candidate=true` 的显式 x86 JDK17 候选门禁;JDK17+RocksDB9.7.4 候选在现有快照上 P2P-disabled 启动成功,27 个 PathState store、Common checkpoint 与 State Archive 数据库均打开。 - 同一候选 P2P 窗口约 120 秒从区块 85150075 到 85150103(+28,约 0.23 block/s),未见 ERROR/Exception;窗口结束后已恢复 JDK8 control。该样本起点为候选旧快照,不能与实时 control 直接作严格 A/B 晋级结论。 +- 同快照顺序 A/B:JDK8 control 120 秒 `85150420→85150443`(+23,约 0.192 block/s);JDK17+RocksDB9.7.4 120 秒 `85150409→85150435`(+26,约 0.217 block/s)。候选约高 13%,但受顺序网络窗口与起点微差影响,仅作方向性证据。 ## 实验顺序 From 3a454e8bfca8df4144682e03ba2597bda7f43b77 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 22:46:53 +0800 Subject: [PATCH 145/161] docs(archive): record snapshot asset gate failure --- .../execution/records/ITER-20260910-400.md | 15 +++++++++++++++ .helper/ai-archive/execution/tasks/TASK-023.md | 1 + 2 files changed, 16 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260910-400.md diff --git a/.helper/ai-archive/execution/records/ITER-20260910-400.md b/.helper/ai-archive/execution/records/ITER-20260910-400.md new file mode 100644 index 00000000000..a0d44357265 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260910-400.md @@ -0,0 +1,15 @@ +# ITER-20260910-400 + +## Slice + +HLT-002 second same-snapshot A/B preparation and candidate startup failure. + +## Observed + +- AMD-002 live was stopped and two fresh reflink runtimes were created from `runtime-current-8ac7a6afb1`; existing runtimes were retained. +- The JDK17 candidate unit reached `ActiveState=active` at the systemd level but did not expose API port 8090. Startup log reported `BeanCreationException` caused by `RuntimeException: Asset num is wrong!` during manager initialization. +- No performance window was counted. The candidate was stopped and AMD-002 control was restored with explicit JDK8; PID and ports 8090/18888/9527 are healthy. + +## Conclusion + +Do not treat this as a JDK17/RocksDB incompatibility: the earlier clean snapshot candidate passed startup and P2P liveness. The new failure is a snapshot-consistency/Archive asset identity gate when cloning the latest live runtime. Before another A/B attempt, compare decoded archive asset identity, CURRENT/marker state, and PathState/Common checkpoint alignment; preserve this failed runtime as evidence. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index 41b9ce9bed7..79778e73fa3 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -26,6 +26,7 @@ active_branch: feature/archive_block2 - `9ea27f2797` 增加 `-Dtron.java17.x86.candidate=true` 的显式 x86 JDK17 候选门禁;JDK17+RocksDB9.7.4 候选在现有快照上 P2P-disabled 启动成功,27 个 PathState store、Common checkpoint 与 State Archive 数据库均打开。 - 同一候选 P2P 窗口约 120 秒从区块 85150075 到 85150103(+28,约 0.23 block/s),未见 ERROR/Exception;窗口结束后已恢复 JDK8 control。该样本起点为候选旧快照,不能与实时 control 直接作严格 A/B 晋级结论。 - 同快照顺序 A/B:JDK8 control 120 秒 `85150420→85150443`(+23,约 0.192 block/s);JDK17+RocksDB9.7.4 120 秒 `85150409→85150435`(+26,约 0.217 block/s)。候选约高 13%,但受顺序网络窗口与起点微差影响,仅作方向性证据。 +- 第二组候选从最新 live 停机快照启动时触发 `Asset num is wrong!`,未进入 API/性能窗口;已停止并恢复 control。该失败说明“当前 live 直接 reflink”仍需先验证 Archive/PathState 共享身份与 marker 完整性,不能继续盲目重复启动。 ## 实验顺序 From b73b0b68d580ad3226107e10d8a1ec4dcec669f4 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 22:52:12 +0800 Subject: [PATCH 146/161] docs(archive): detail incomplete amd snapshot root cause --- .helper/ai-archive/execution/records/ITER-20260910-400.md | 3 ++- .helper/ai-archive/execution/tasks/TASK-023.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.helper/ai-archive/execution/records/ITER-20260910-400.md b/.helper/ai-archive/execution/records/ITER-20260910-400.md index a0d44357265..5ed846ea392 100644 --- a/.helper/ai-archive/execution/records/ITER-20260910-400.md +++ b/.helper/ai-archive/execution/records/ITER-20260910-400.md @@ -9,7 +9,8 @@ HLT-002 second same-snapshot A/B preparation and candidate startup failure. - AMD-002 live was stopped and two fresh reflink runtimes were created from `runtime-current-8ac7a6afb1`; existing runtimes were retained. - The JDK17 candidate unit reached `ActiveState=active` at the systemd level but did not expose API port 8090. Startup log reported `BeanCreationException` caused by `RuntimeException: Asset num is wrong!` during manager initialization. - No performance window was counted. The candidate was stopped and AMD-002 control was restored with explicit JDK8; PID and ports 8090/18888/9527 are healthy. +- Post-failure inspection showed the failed clone's `database/block` had `CURRENT=MANIFEST-000002` and only about 16 KB, while the source live database had `CURRENT=MANIFEST-001533` and about 4.7 GB. The copy was incomplete, explaining `latestBlockHeaderNumber=0` and `Asset num is wrong!`. ## Conclusion -Do not treat this as a JDK17/RocksDB incompatibility: the earlier clean snapshot candidate passed startup and P2P liveness. The new failure is a snapshot-consistency/Archive asset identity gate when cloning the latest live runtime. Before another A/B attempt, compare decoded archive asset identity, CURRENT/marker state, and PathState/Common checkpoint alignment; preserve this failed runtime as evidence. +Do not treat this as a JDK17/RocksDB incompatibility: the earlier clean snapshot candidate passed startup and P2P liveness. Before another A/B attempt, complete the copy under a long-running job and gate startup on regular-file totals, database CURRENT/MANIFEST presence and size, decoded archive asset identity, and PathState/Common checkpoint alignment; preserve this failed runtime as evidence. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index 79778e73fa3..9b4893df083 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -26,7 +26,7 @@ active_branch: feature/archive_block2 - `9ea27f2797` 增加 `-Dtron.java17.x86.candidate=true` 的显式 x86 JDK17 候选门禁;JDK17+RocksDB9.7.4 候选在现有快照上 P2P-disabled 启动成功,27 个 PathState store、Common checkpoint 与 State Archive 数据库均打开。 - 同一候选 P2P 窗口约 120 秒从区块 85150075 到 85150103(+28,约 0.23 block/s),未见 ERROR/Exception;窗口结束后已恢复 JDK8 control。该样本起点为候选旧快照,不能与实时 control 直接作严格 A/B 晋级结论。 - 同快照顺序 A/B:JDK8 control 120 秒 `85150420→85150443`(+23,约 0.192 block/s);JDK17+RocksDB9.7.4 120 秒 `85150409→85150435`(+26,约 0.217 block/s)。候选约高 13%,但受顺序网络窗口与起点微差影响,仅作方向性证据。 -- 第二组候选从最新 live 停机快照启动时触发 `Asset num is wrong!`,未进入 API/性能窗口;已停止并恢复 control。该失败说明“当前 live 直接 reflink”仍需先验证 Archive/PathState 共享身份与 marker 完整性,不能继续盲目重复启动。 +- 第二组候选从最新 live 停机快照启动时触发 `Asset num is wrong!`,未进入 API/性能窗口;失败 runtime 的 `database/block` 仅约 16 KB、`CURRENT=MANIFEST-000002`,而 live 为约 4.7 GB、`MANIFEST-001533`,确认是 reflink 复制未完成/残缺导致的空 block DB。已停止并恢复 control;后续必须先做 regular-file/manifest/marker 完整性校验。 ## 实验顺序 From cd77400aec307482ce471360ff2fe5ffb82937d3 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 23:04:28 +0800 Subject: [PATCH 147/161] docs(archive): confirm incomplete block clone evidence --- .helper/ai-archive/execution/records/ITER-20260910-400.md | 1 + .helper/ai-archive/execution/tasks/TASK-023.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.helper/ai-archive/execution/records/ITER-20260910-400.md b/.helper/ai-archive/execution/records/ITER-20260910-400.md index 5ed846ea392..57dcff266a8 100644 --- a/.helper/ai-archive/execution/records/ITER-20260910-400.md +++ b/.helper/ai-archive/execution/records/ITER-20260910-400.md @@ -10,6 +10,7 @@ HLT-002 second same-snapshot A/B preparation and candidate startup failure. - The JDK17 candidate unit reached `ActiveState=active` at the systemd level but did not expose API port 8090. Startup log reported `BeanCreationException` caused by `RuntimeException: Asset num is wrong!` during manager initialization. - No performance window was counted. The candidate was stopped and AMD-002 control was restored with explicit JDK8; PID and ports 8090/18888/9527 are healthy. - Post-failure inspection showed the failed clone's `database/block` had `CURRENT=MANIFEST-000002` and only about 16 KB, while the source live database had `CURRENT=MANIFEST-001533` and about 4.7 GB. The copy was incomplete, explaining `latestBlockHeaderNumber=0` and `Asset num is wrong!`. +- AssetUpdateHelper logged `Total block:1`, confirming the cloned block store exposed latest height zero rather than the source live height. ## Conclusion diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index 9b4893df083..12dfbacfac9 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -27,6 +27,7 @@ active_branch: feature/archive_block2 - 同一候选 P2P 窗口约 120 秒从区块 85150075 到 85150103(+28,约 0.23 block/s),未见 ERROR/Exception;窗口结束后已恢复 JDK8 control。该样本起点为候选旧快照,不能与实时 control 直接作严格 A/B 晋级结论。 - 同快照顺序 A/B:JDK8 control 120 秒 `85150420→85150443`(+23,约 0.192 block/s);JDK17+RocksDB9.7.4 120 秒 `85150409→85150435`(+26,约 0.217 block/s)。候选约高 13%,但受顺序网络窗口与起点微差影响,仅作方向性证据。 - 第二组候选从最新 live 停机快照启动时触发 `Asset num is wrong!`,未进入 API/性能窗口;失败 runtime 的 `database/block` 仅约 16 KB、`CURRENT=MANIFEST-000002`,而 live 为约 4.7 GB、`MANIFEST-001533`,确认是 reflink 复制未完成/残缺导致的空 block DB。已停止并恢复 control;后续必须先做 regular-file/manifest/marker 完整性校验。 +- 进一步日志确认失败 clone 的 `latestBlockHeaderNumber=0`(`Total block:1`),与残缺 block DB 一致;现有失败目录可作为复制完整性负例。 ## 实验顺序 From fee99c808b54f552cc416995a0495241e3a68ecc Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 23:16:57 +0800 Subject: [PATCH 148/161] docs(archive): add snapshot copy integrity gate --- .../execution/records/ITER-20260910-401.md | 16 ++++++++++++++++ .helper/ai-archive/execution/tasks/TASK-023.md | 1 + 2 files changed, 17 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260910-401.md diff --git a/.helper/ai-archive/execution/records/ITER-20260910-401.md b/.helper/ai-archive/execution/records/ITER-20260910-401.md new file mode 100644 index 00000000000..9f316860747 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260910-401.md @@ -0,0 +1,16 @@ +# ITER-20260910-401 + +## Slice + +HLT-002 verified long-running reflink copy and startup-gate check. + +## Observed + +- A dedicated systemd copy unit completed successfully after the live node was stopped. Source `runtime-current-8ac7a6afb1` and destination `runtime-hlt002-safe-jdk17` were compared before any candidate use. +- `database/block`: 769 regular files and 5,050,533,467 apparent bytes on both sides; `CURRENT` and `MANIFEST-001533` SHA-256 matched. +- `path-state-root`: 3,272 regular files on both sides; `CURRENT` SHA-256 matched. `state-archive/hot`: 149 regular files and 2,476,596,233 apparent bytes on both sides; `CURRENT` SHA-256 matched. +- A subsequent candidate smoke was accidentally started while the control had already been restored, so Prometheus port binding conflicted. Its API output came from the live control and is discarded; the candidate was stopped immediately. AMD-002 control remains active with JDK8 and ports 8090/18888/9527. + +## Conclusion + +The copy-integrity gate now distinguishes the prior 16-KB incomplete clone from a complete snapshot. The next candidate run must stop control for the entire smoke/performance window and verify ownership of 8090/18888/9527 before counting any API or throughput result. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index 12dfbacfac9..b71cdc02386 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -28,6 +28,7 @@ active_branch: feature/archive_block2 - 同快照顺序 A/B:JDK8 control 120 秒 `85150420→85150443`(+23,约 0.192 block/s);JDK17+RocksDB9.7.4 120 秒 `85150409→85150435`(+26,约 0.217 block/s)。候选约高 13%,但受顺序网络窗口与起点微差影响,仅作方向性证据。 - 第二组候选从最新 live 停机快照启动时触发 `Asset num is wrong!`,未进入 API/性能窗口;失败 runtime 的 `database/block` 仅约 16 KB、`CURRENT=MANIFEST-000002`,而 live 为约 4.7 GB、`MANIFEST-001533`,确认是 reflink 复制未完成/残缺导致的空 block DB。已停止并恢复 control;后续必须先做 regular-file/manifest/marker 完整性校验。 - 进一步日志确认失败 clone 的 `latestBlockHeaderNumber=0`(`Total block:1`),与残缺 block DB 一致;现有失败目录可作为复制完整性负例。 +- 使用独立长时 copy unit 重新复制后,block DB 校验为 769 文件/5,050,533,467 字节,PathState 3272 文件,Archive hot 149 文件,关键 CURRENT/MANIFEST hash 与源一致。后续 smoke 因 control 已恢复、Prometheus 端口冲突而不计入性能/启动结论。 ## 实验顺序 From e31bcf861808fbcbe97bacc50d657ccb3f881d15 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 23:31:27 +0800 Subject: [PATCH 149/161] docs(archive): record verified jdk17 startup gate --- .../execution/records/ITER-20260910-402.md | 15 +++++++++++++++ .helper/ai-archive/execution/tasks/TASK-023.md | 1 + 2 files changed, 16 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260910-402.md diff --git a/.helper/ai-archive/execution/records/ITER-20260910-402.md b/.helper/ai-archive/execution/records/ITER-20260910-402.md new file mode 100644 index 00000000000..951d7d193d2 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260910-402.md @@ -0,0 +1,15 @@ +# ITER-20260910-402 + +## Slice + +HLT-002 complete-copy candidate startup gate. + +## Observed + +- A fresh snapshot was copied by the long-running `amd002-hlt002-safe-copy2` unit while control was stopped. `database/block` matched source at 771 files and 5,062,113,945 bytes; `CURRENT` SHA-256 matched. +- With control kept stopped for the entire smoke, JDK17 + RocksDB9.7.4 candidate unit `amd002-hlt002-safe2-jdk17` started successfully using the explicit opt-in property. API port 8090 and PBFT port 9527 were owned by the candidate; `/wallet/getnowblock` returned head `85150943`. Latest logs showed all API services started and JVM `17.0.20`. +- Candidate was stopped and AMD-002 control was restored with explicit JDK8. Existing incomplete/failed runtimes remain retained. + +## Conclusion + +The complete-copy and exclusive-port gates now produce a valid JDK17 startup result from the latest snapshot. This closes the prior snapshot/port false negatives; a P2P performance window can now be run from this validated destination, with control kept stopped until candidate teardown. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index b71cdc02386..f2d7faa5b07 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -29,6 +29,7 @@ active_branch: feature/archive_block2 - 第二组候选从最新 live 停机快照启动时触发 `Asset num is wrong!`,未进入 API/性能窗口;失败 runtime 的 `database/block` 仅约 16 KB、`CURRENT=MANIFEST-000002`,而 live 为约 4.7 GB、`MANIFEST-001533`,确认是 reflink 复制未完成/残缺导致的空 block DB。已停止并恢复 control;后续必须先做 regular-file/manifest/marker 完整性校验。 - 进一步日志确认失败 clone 的 `latestBlockHeaderNumber=0`(`Total block:1`),与残缺 block DB 一致;现有失败目录可作为复制完整性负例。 - 使用独立长时 copy unit 重新复制后,block DB 校验为 769 文件/5,050,533,467 字节,PathState 3272 文件,Archive hot 149 文件,关键 CURRENT/MANIFEST hash 与源一致。后续 smoke 因 control 已恢复、Prometheus 端口冲突而不计入性能/启动结论。 +- 第二份完整快照在 control 保持停止期间启动 JDK17 smoke 成功,HTTP/PBFT API 全部启动并返回快照 head;候选随后停止并恢复 JDK8 control。 ## 实验顺序 From 00389e0cbc24e8f73338da9b0b2034a393bfb2ca Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 23:43:23 +0800 Subject: [PATCH 150/161] docs(archive): record verified amd p2p window --- .../execution/records/ITER-20260910-403.md | 16 ++++++++++++++++ .helper/ai-archive/execution/tasks/TASK-023.md | 1 + 2 files changed, 17 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260910-403.md diff --git a/.helper/ai-archive/execution/records/ITER-20260910-403.md b/.helper/ai-archive/execution/records/ITER-20260910-403.md new file mode 100644 index 00000000000..81a7d8d7475 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260910-403.md @@ -0,0 +1,16 @@ +# ITER-20260910-403 + +## Slice + +HLT-002 verified-snapshot JDK17/RocksDB9.7.4 P2P window. + +## Observed + +- A fresh candidate runtime was copied by long-running unit `amd002-hlt002-p2p-copy`; block DB validation matched source at 772 files and 5,069,146,804 bytes, with identical `CURRENT`. +- Control remained stopped while candidate unit `amd002-hlt002-p2p-jdk17` owned ports 8090/18888/9527. +- 120-second P2P window: head `85151013→85151045`, +32 blocks, approximately `0.267 block/s`. At sampling, process CPU was about 471% and RSS about 9.8 GB. +- Candidate was stopped and AMD-002 control restored with explicit JDK8. Historical error lines in the reused candidate log were from prior failed experiments; no new failure occurred in this window. + +## Conclusion + +The verified-copy and exclusive-port gates now support a valid JDK17 P2P sample. The observed rate is directionally above the earlier JDK8 same-snapshot sample (`0.192 block/s`), but remains a short, non-interleaved measurement; repeat control/candidate windows with synchronized resource and checkpoint telemetry before promotion. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index f2d7faa5b07..912048a4e34 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -30,6 +30,7 @@ active_branch: feature/archive_block2 - 进一步日志确认失败 clone 的 `latestBlockHeaderNumber=0`(`Total block:1`),与残缺 block DB 一致;现有失败目录可作为复制完整性负例。 - 使用独立长时 copy unit 重新复制后,block DB 校验为 769 文件/5,050,533,467 字节,PathState 3272 文件,Archive hot 149 文件,关键 CURRENT/MANIFEST hash 与源一致。后续 smoke 因 control 已恢复、Prometheus 端口冲突而不计入性能/启动结论。 - 第二份完整快照在 control 保持停止期间启动 JDK17 smoke 成功,HTTP/PBFT API 全部启动并返回快照 head;候选随后停止并恢复 JDK8 control。 +- 在完整性校验通过且 control 全程停止的前提下,JDK17+RocksDB9.7.4 P2P 窗口从 `85151013` 到 `85151045`(120 秒 +32,约 `0.267 block/s`),采样 CPU 约 471%、RSS 约 9.8 GB;窗口后已恢复 control。该值仍需与同快照 JDK8 交错复测。 ## 实验顺序 From 7129f806b26cda4cabba9b599cc6413defff71d5 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Thu, 10 Sep 2026 23:46:46 +0800 Subject: [PATCH 151/161] docs(archive): attribute amd sync host bottleneck --- .../execution/records/ITER-20260910-404.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260910-404.md diff --git a/.helper/ai-archive/execution/records/ITER-20260910-404.md b/.helper/ai-archive/execution/records/ITER-20260910-404.md new file mode 100644 index 00000000000..a6537df0fa7 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260910-404.md @@ -0,0 +1,16 @@ +# ITER-20260910-404 + +## Slice + +HLT-002 AMD-002 host exclusivity and slow-sync attribution. + +## Observed + +- AMD-002 reports 16 vCPUs on an AMD EPYC 7R13 host. At observation time load average was `4.81/5.09/4.86`; the FullNode was the only material user process at about 467% CPU. Alloy and system agents were each near 1% or below. +- The live unit has no CPU quota (`CPUQuotaPerSecUSec=infinity`), MemoryHigh 29 GiB, MemoryMax 30 GiB, swap disabled, and MemoryCurrent about 17.0 GB. `vmstat` showed 85–94% idle CPU and no swap activity. +- I/O PSI was non-zero (`some/full avg10 about 5.3%`), while the node had 525 listed peers. This points away from host CPU contention and toward storage wait plus per-block database/write/commit work; peer count alone does not establish useful sync bandwidth. +- The environment is not proven bare-metal exclusive: it is a 16-vCPU host/VM with background system services. No competing heavy workload was observed during the sample. + +## Conclusion + +The observed ~0.2–0.3 block/s is not explained by another process monopolizing AMD-002 CPU or by the cgroup CPU limit. Next HLT-002 windows must collect interval disk latency/utilization, write amplification, checkpoint/compaction timing, and per-block stage timings; only then tune RocksDB options or claim a storage fix. From 5bbd1b80d961d27c3fa696c9630e367a12f508bc Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 11 Sep 2026 00:01:08 +0800 Subject: [PATCH 152/161] docs(archive): correct arm amd performance comparison --- .../execution/records/ITER-20260910-405.md | 16 ++++++++++++++++ .helper/ai-archive/execution/tasks/TASK-023.md | 1 + 2 files changed, 17 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260910-405.md diff --git a/.helper/ai-archive/execution/records/ITER-20260910-405.md b/.helper/ai-archive/execution/records/ITER-20260910-405.md new file mode 100644 index 00000000000..26800094ed4 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260910-405.md @@ -0,0 +1,16 @@ +# ITER-20260910-405 + +## Slice + +HLT-002 ARM-001 versus AMD-002 live configuration attribution. + +## Observed + +- The actual ARM-001 host is `10.255.10.101` (the previously checked `10.255.10.84` currently runs a Geth archive process). ARM-001 FullNode PID `1665805` runs JDK8 with `/data/blade/state-archive-4f143bd6b8/config-p66-live.conf`. +- ARM-001 live config reports `db.engine=ROCKSDB`, State Archive enabled, and `pathStateRoot.mode=shadow`; its service has no cgroup MemoryHigh/MemoryMax and no CPU quota. Host load was `7.06` on 16 vCPUs, FullNode about 454% CPU, IO PSI avg10 about 2.55%. +- AMD-002 live runs JDK8 with Chainbase LevelDB, synchronous PathState/Archive semantics, MemoryHigh 29 GiB and MemoryMax 30 GiB, no CPU quota. Host load was about `4.56`, FullNode about 410% CPU, IO PSI avg10 about 5.38%, cgroup MemoryCurrent about 30.1 GB. +- ARM-001 did not expose the expected 8090/18888 API ports in this observation; only 9527 was listening. Therefore no current same-window block-rate comparison was performed. + +## Conclusion + +AMD's lower observed rate is plausibly explained by the heavier synchronous persistence path, Chainbase LevelDB, and tighter memory/cgroup/IO conditions rather than lack of host CPU. ARM historical rates are not an apples-to-apples control until ARM is actually running the documented synchronous configuration with matching JDK, engines, cgroup, snapshot head, and transaction density. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index 912048a4e34..e9ffb66781d 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -31,6 +31,7 @@ active_branch: feature/archive_block2 - 使用独立长时 copy unit 重新复制后,block DB 校验为 769 文件/5,050,533,467 字节,PathState 3272 文件,Archive hot 149 文件,关键 CURRENT/MANIFEST hash 与源一致。后续 smoke 因 control 已恢复、Prometheus 端口冲突而不计入性能/启动结论。 - 第二份完整快照在 control 保持停止期间启动 JDK17 smoke 成功,HTTP/PBFT API 全部启动并返回快照 head;候选随后停止并恢复 JDK8 control。 - 在完整性校验通过且 control 全程停止的前提下,JDK17+RocksDB9.7.4 P2P 窗口从 `85151013` 到 `85151045`(120 秒 +32,约 `0.267 block/s`),采样 CPU 约 471%、RSS 约 9.8 GB;窗口后已恢复 control。该值仍需与同快照 JDK8 交错复测。 +- 对比校准:真正 ARM-001 为 `10.255.10.101`,当前 live 实际是 JDK8、Chainbase RocksDB、`pathStateRoot.mode=shadow`,且 unit 无 MemoryHigh/Max;AMD-002 是 JDK8、Chainbase LevelDB、同步 PathState、30 GiB cgroup。不能把 ARM 历史 shadow/无硬内存上限窗口与 AMD 同步/30 GiB窗口直接比较。 ## 实验顺序 From bdc5a089e221a8ed7604c75221e9134728733ebb Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 11 Sep 2026 00:10:27 +0800 Subject: [PATCH 153/161] docs(archive): align live arm amd harness facts --- .../current/current-validation-status.md | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 .helper/ai-archive/evidence/current/current-validation-status.md diff --git a/.helper/ai-archive/evidence/current/current-validation-status.md b/.helper/ai-archive/evidence/current/current-validation-status.md new file mode 100644 index 00000000000..ccb445eb92c --- /dev/null +++ b/.helper/ai-archive/evidence/current/current-validation-status.md @@ -0,0 +1,240 @@ +# State Archive Harness 当前现实使用状态 + +- 状态日期:2026-09-10 +- 权威范围:当前验证用途与最近已记录现场 +- 重要:这是文档快照,不是live monitor;任何远端操作前必须重做只读preflight。 + +## 总览 + +| 节点 | 当前角色 | 最近现实使用 | 当前证据边界 | +|---|---|---|---| +| `tron-apse1-amd-002` | 最终性能验证 | Verified `53702079ee`隔离canary正常轮转PASS但WAL恢复FAIL,已回滚到`ddb0d6ef33` | rollback run active且越过candidate高度;crash/recovery与production Gate保持OPEN | +| `tron-apse1-arm-001` | 主要开发辅助 | fd69340ca3 P66同Snapshot/并行产物,原runtime升级并两次clean reopen,联网继续 | 83,836,637旧基线与83,836,754新写入重开authority一致;100样本0.729 block/s非A/B,crash/production Gate仍OPEN | +| `tron-apse1-arm-002` | 次要辅助/备用 | PathState保持disabled;100k Archive窗口继续同步,并提供52,246块/23,229,129交易增长统计 | auxiliary capacity evidence;不提供PathState/common-checkpoint或最终性能验收 | +| `tron-apse1-amd-001` | Geth核心外部架构对照观察组 | Geth v1.17.4 path archive Phase 1执行到约25.50M;本机与集中监控可用 | external control evidence;当前历史RPC不可用,不替代AMD java-tron Gate | + +## Geth核心外部架构对照组 + +- 2026-09-09只读现场:Geth commit `36a7dc72`,unit/PID为`geth-world-state-archive.service/182838`,参数 + `--state.scheme path --history.state 0 --history.trienode -1 --syncmode full --cache 6144`,Pebble加ancient state + freezer;Geth/Lighthouse unit均active; +- 同墙钟近似窗口:Geth 457块/167,971交易/99秒,即4.616 block/s、1,696.68 tx/s;AMD最新Hot/Common为 + 40块/10,562交易/98秒,即0.408 block/s、107.78 tx/s。链与持久语义不同,仅用于架构数量级对照; +- 三个历史balance探针均返回`historical state ... is not available`,当前数字不是完整可查询archive吞吐; +- Geth 6060 metrics返回200,Alloy运行;Thanos中`instance="tron-apse1-amd-001"`的`geth-archive`和 + `node-exporter-full`均`up=1`;详细见 + [`Geth对照观察结果`](../results/geth-path-archive-control-observation-20260909.md)。 +- 03:59 UTC曾把Geth从20G短暂调为29G/30G,随后按人工修正回调为`MemoryHigh=infinity / MemoryMax=22G`; + Lighthouse保持8 GiB,两service硬上限合计30 GiB。回调后同一PID、0新增restart/OOM/kill且短窗继续推进208块; + Geth距自身硬上限仅约0.38 GiB,主机无swap,仍须观察host available、memory PSI及kernel/cgroup OOM。 + +## AMD-002:最终性能节点的当前基线 + +### 2026-09-06 memory-rebase修复run(历史基线,已被后续部署取代) + +- 历史候选:GitHub Verified `37723917cbddd98df067a7c9576300ebd9f14725`;AMD/JDK8 JAR SHA-256 + `a50dec3f42443e8b7d29d330e9a6d84a4f39a7f3998f87bd915b66bf032a0e96`; +- unit:`amd002-archive-block2-sync-mf10-37723917cb.service`;monitor: + `amd002-archive-block2-monitor-mf10-37723917cb.service`;当时active,后由`ddb0d6ef33`部署正常接替; +- 继续使用`/data/blade/node_mainnet/runtime-a2fc535a5a/output-directory`和既有maxFlushCount=10配置;没有 + exact-27 scan、ingest、rebuild、repair或format migration; +- 启动日志明确`Common checkpoint authority established, skip legacy checkpoint recovery`,随后三组件以 + format-v1共同head 85,136,463附着; +- 精确窗口`(85,136,463, 85,136,963]`为500块、212,456 tx、691.105秒,wall为0.7235 block/s; + checkpoint、ProcessBlock、PathState和Archive分别为472.25、347.66、282.09和276.56 ms/block; +- JVM仍为Xms8G/Xmx18G/direct1G,cgroup仍为29G/30G。后续采样old-gen 52.41%、Full GC 0; + `memory.high`已出现但max/OOM/kill/restart/error为0。cgroup约14.70 GiB anon和13.73 GiB file, + 不能把high直接写成PathState retained-heap复发; +- evidence:`/data/blade/node_mainnet/archive-block2-37723917cb/evidence/sync-20260906T160557Z/`。 +- 30分钟夜间watch `amd002-archive-block2-nightwatch-37723917cb.service`当时active。它持续记录window throughput、 + JVM GC和OS/cgroup证据;连续90分钟不推进时先取证后正常restart且6小时冷却,disk<=80 GiB或 + memory.max/oom_kill时保护性停止。不会自动调参、删库、修复、迁移、重建或替换artifact。 +- 后段同量级交易密度精确500块窗口`(85,151,099, 85,151,599]`为0.6086 block/s;PathState仍稳定在 + 277.56 ms/block,checkpoint增至582.52 ms/block。30分钟watch最低窗为0.2932 block/s,RSS约21.4 GiB, + high超过58万但Full GC、max/OOM/kill/restart仍为0;唯一peer ERROR没有引起runtime失败。 +- 首个完整transaction-aware v2窗口为1,154块/439,177 tx/1800秒,即0.6411 block/s、243.99 tx/s; + PushBlock 1519.75、checkpoint 530.82 ms/block。窗口内10 peers、0 error/restart/OOM/kill/FGC; + old-gen端点86.86%但随后Young GC回落到78.86%。该run当时继续观察,下一candidate只增加checkpoint phase timer。 +- 00:51 UTC前连续值守无自动动作/incident,RSS约22.5M KiB稳定超过3小时、Full GC持续为0;最近四窗 + 0.448--0.486 block/s。v2从01:01 UTC起增加transaction、tx/s、PushBlock和checkpoint字段,首行仅建立日志游标。 +- 01:31--03:31 UTC五个完整v2窗口:block/s 0.641--1.008、tx/s 205.42--243.99、tx/block由380.57降至 + 212.75;checkpoint由530.82降至372.25 ms/block。RSS约22.6M KiB稳定,old-gen可回落且Full GC仍为0; + memory.high持续增长但max/OOM/kill为0,未触发自动动作。 +- 04:51--05:00 UTC rollout只读preflight再次确认FullNode、monitor、night-watch均active、0 restart;三authority + 在在线时点精确一致于durable head 85,162,803,checksum有效且连续5次无共同WAL。最后完整watch为1,246块、 + 405,964 tx、0.691838 block/s、checkpoint 519.12 ms/block,0 error/OOM/kill/FGC; +- 当前是lite FullNode,四个固定高度wallet端点均关闭,JSON-RPC也disabled。因此固定高度API Gate仍OPEN;为保持配置 + 不变,section 3.2仅提出“停机前H/hash + 新日志在H的blockID”continuity替代oracle,需与AMD rollout一并人工确认; +- 新candidate root/unit、pre-rollout snapshot和quarantine runtime identity均已列明并确认未占用。当前没有build、copy、 + stop/start或部署;详见[AMD只读preflight](../results/amd002-instrumentation-rollout-preflight-20260906.md)及 + [rollout plan section 3.2](../validation-operations/common-checkpoint-instrumentation-rollout-plan.md#32-待人工确认的精确amd-side-by-side-payload)。 + +### 2026-09-06至2026-09-07当前instrumentation run + +- 人工随后明确确认section 3.2及continuity替代oracle;`ddb0d6ef33` AMD/JDK8 clean JAR SHA-256为 + `de1fc9601c684a27b3827bd2b17d3a7664a5d61dd70e9440dfc638e8d8d19879`,embedded commit正确、dirty=false; +- 旧run正常停止并在共同D=85,164,093冻结三authority;266,924 files的runtime已建立强制reflink回退基线。新run从D + 附着,并在停机前H=85,164,102输出相同blockID,continuity PASS; +- instrumentation精确500块为0.7782 block/s、280.03 tx/s;首个完整30分钟窗为1,334块、489,385 tx、0.7407 + block/s。PathState rebase prepare约216.6 ms/block,是最大稳定叶子阶段;Archive end/close约97.0 ms/block但 + P95达6.49秒/batch,是尾部第一调查对象; +- 06:22 UTC FullNode及两个observer均active、0 restart,三authority在线一致于85,166,403,0 WAL/error/OOM/kill/FGC; + rollout与首窗Gate关闭,长期/strict A/B/production Gate仍OPEN。详见 + [AMD instrumentation结果](../results/amd002-common-checkpoint-instrumentation-rollout-20260906.md)。 +- 第二个完整30分钟窗为1,236块、491,445 tx、0.6863 block/s;checkpoint为569.98 ms/block,PathState rebase + prepare为229.14 ms/block,Archive end为106.49 ms/block且P95 6.55秒/batch。与首窗tx/block增加8.38%时,两个 + phase形态均复现;源码确认Archive end关闭target级writer并在最后引用释放时调用LevelDB `DB.close()`; +- 2026-09-07 02:59 UTC只读复核:最近实际部署仍为`ddb0d6ef33`,FullNode/monitor/night-watch均active、0 restart。 + 从05:29:37到次日02:30:52 UTC约21小时推进46,752块/75,675秒,即0.6178 block/s、1.619 s/block、约主网产块速度 + 1.85倍;最近12/6/3小时分别为0.6232/0.5438/0.5787 block/s,最新30分钟为0.7285 block/s。42个窗口范围 + 0.4528--1.0827 block/s,个别窗口超过1 block/s但长期目标未通过; +- 同一时点RSS约20.1 GiB、cgroup current约29.0 GiB,`memory.high=1,907,783`,但max/OOM/kill、Full GC和restart均为0。 + high只证明reclaim/throttling压力,不单独证明泄漏或解释全部同步差距; +- 2026-09-07 03:07 UTC同一runtime在线存储快照:`state-archive/`相对部署前reflink基线增加5,207,171,072 allocated B, + 对应约48,229--48,599 blocks和107,145.64--107,967.64 B/block;85M block外推Archive为8.283--8.347 TiB, + 加一次当前PathState代理值为8.578--8.641 TiB。该值是online allocated方向证据,不是clean-stop整节点容量Gate; + 详见[AMD存储外推](../results/amd002-online-storage-extrapolation-20260907.md); +- 06:30 UTC三个unit仍active、0 restart,0 error/WAL/OOM/kill/FGC,`/data`可用约270.3 GiB。已形成唯一待选候选 + [`CCI-O01`](../../design/ai/commit-recovery/common-checkpoint-serving-index-handle-reuse-candidate.md):只改变writer + 物理handle生命周期并补齐runtime关闭所有权;尚未实现或证明收益。 + +以下`e92aae0c89`材料保留为修复前历史基线,不代表当前运行identity。 + +- 机型:AWS `c6a.4xlarge`,AMD EPYC 7R13,16 vCPU,约30 GiB RAM; +- 数据盘:1 TiB EBS gp3/XFS,历史确认7,500 IOPS / 250 MiB/s; +- 历史候选:`feature/archive_block2@e92aae0c8922726f4b255764a7dd1981fe8a9ebb`,GitHub Verified; +- AMD/JDK8 JAR SHA-256:`440ef66647dbeb74c4c33c914339f266bc5a185248af3f93f60c80acac2ae939`; +- 一致输入是已完成exact-27、super和format-v1 PathState的冻结LevelDB head `85,130,544`: + `/data/blade/node_mainnet/archive-block2-bef5e53c-ready-85130544-20260904T2048Z/output-directory`; +- 当前独立runtime为`/data/blade/node_mainnet/runtime-a2fc535a5a/output-directory`,unit为 + `amd002-archive-block2-sync-mf10-e92aae0c89.service`;`formatVersion=1`、P2P已开启; +- 兼容接管只把已验证legacy `CURRENT`转换为共同baseline,没有运行exact-27 ingest/rebuild;固定高度正常关闭再启动 + 后再次在85,130,544附着,启动约1秒打开全部PathState store,无scan/redo/error; +- 2026-09-05 09:49 UTC抽取二进制marker验证:Chainbase `CHAINBASE_CURRENT`与PathState `CURRENT` + 除各自magic外identity完全相同;Archive `READABLE`的format identity、payload digest、last epoch/block/hash和 + state root逐字段一致,三文件checksum均正确;该证据才是设计中的三authority共同checkpoint; +- 2026-09-05 09:53 UTC同步head至少85,130,824,8个active peer,unit无重启、无OOM;cgroup在线由 + `25G/26G`调为`MemoryHigh=29G/MemoryMax=30G`,JVM仍为`Xmx=18G`。调整后`memory.high`事件停止增长, + 但逐块checkpoint仍约1.5--2.6秒,不能把同步慢单独归因于cgroup; +- 人工决定后已把`storage.snapshot.maxFlushCount`由1改为10,新配置SHA-256为 + `1ee65e2fb5a084dead602734c6c335201fcc77c767b58773078ef2069ec3e6c8`。旧run正常关闭到`close end`; + 非整批停止前durable marker为85,130,951、API head为85,130,979,shutdown把8块尾批共同发布到 + 85,130,959,三authority保持一致;重启准确从该head附着并恢复同步; +- 10-block batch首窗flush为4.1--6.7秒/批,约0.41--0.67秒/块;09:56:48--09:58:06推进 + 74块,约0.95 block/s,高于主网约0.333 block/s并具备净追赶能力。重启后flush短窗约1.38--3.40秒/批; +- 2026-09-05 10:28 UTC只读复核monitor:10:02:02--10:28:06连续1,564秒推进1,298块,墙钟约 + 0.830 block/s;8 connections、0 error、0 restart、0 OOM。RSS由8,742,536 KiB升至14,133,892 KiB, + cgroup peak 28,923,559,936 B;同窗`/data`可用字节减少31,243,923,456 B,主要受reflink CoW、SST生命周期 + 和compaction影响,不能当作逻辑Archive净增长,但必须作为现实停止条件继续观察; +- `e92aae0c89`保持相同runtime/config/JVM/cgroup完成精确500块对照:521.691秒、176,691交易、 + 0.9584 block/s;50个checkpoint对应50次serving-index open,checkpoint为229.06 ms/block。相对旧窗 + blocks/s提升28.9%、checkpoint摊销下降47.2%,但新窗tx/block高21.9%,证据是online attributed而非严格A/B; +- 对照后正常关闭到`close end`,共同durable head为85,133,781;三authority文件hash在重启前后不变,重启从 + 该head附着并跳过legacy recovery。没有运行ingest/rebuild,也没有修复或删除数据库; +- 持续证据位于`/data/blade/node_mainnet/archive-block2-e92aae0c89/evidence/maxflush10-20260905/`, + monitor unit为`amd002-archive-block2-monitor-mf10-e92aae0c89.service`; +- 2026-09-05 13:01 UTC长窗复核:有效monitor样本在6,727秒推进2,636块,仅0.3919 block/s; + `memory.high`首次非零后的样本段约0.1822 block/s,最近80块按PushBlock cost约0.0391 block/s。 + high累计206,806但max/OOM/OOM-kill/restart为0;checkpoint通常约3--4秒/10块,约25秒停顿主要落在 + prepare/trie/nodePlan。15秒device样本未饱和。该证据标记为退化相关性,尚未证明内存限流是唯一根因; +- 2026-09-05 13:10 UTC JVM只读诊断把直接机制收敛为Full GC thrashing:18 GiB old gen使用99.99%, + `Allocation Failure`触发约25秒Full GC,累计109次/2,638.593秒;VM Thread约占一核而host约93.5% idle, + CPU无cgroup throttle且I/O pressure低。PSS约26 GiB、几乎全为anonymous。对象保留源仍未知,未运行 + histogram/dump,也未停启或调参; +- 13:13--13:15 UTC续观测:unit仍active且0 restart,但两次API均3秒超时;连续三块各耗时约51.5--52.1秒, + 表明节点仍间歇推进而非死锁。cgroup约31.14 GB,`memory.high`在20秒增加139次至211,835,max/OOM仍为0; + `jstat`也无法attach。唯一ERROR为peer非法同步范围,不是数据库或三authority错误;本轮未执行侵入诊断或停启; +- 人工授权后于13:20 UTC冻结pre-stop evidence并正常停止,未发送kill。systemd约44秒后返回,shutdown执行一次 + 2,002 ms共同checkpoint并记录`close end`;主unit/monitor均inactive且无目标FullNode进程。Chainbase、PathState、 + Archive都在85,136,463,format/payload/block hash/state root一致、marker checksum有效、共同WAL为0;没有重启、 + 删除、修复或重建。最后完成块85,136,482之上的19块属于正常丢弃的可逆内存窗口; +- 2026-09-05 13:20 UTC源码审计确认PathState最新head的28个Trie snapshot各自通过`parent`保留全部历史 + snapshot和旧node graph;外层history限长无效,normal common checkpoint没有PathState内存rebase callback。 + 该机制与运行增长一致但无histogram字节归属。建议正常停止当前run并不重启,等待人工授权; +- 停止的`d0a7802dc2` format-v2 partial在Account 116,000,000行、PathState约18G且无`CURRENT`,继续保留但不再是候选; +- `bef5e53c`曾完成27/27和legacy顶层`CURRENT`,但后续同步在block `85,133,105`发生VM receipt + `SUCCESS -> OUT_OF_TIME`不一致;正常重启又暴露Chainbase/PathState恢复head不一致。该旧unit已停止, + 运行库和故障副本原位保留,没有回退、修复或删除; +- 日志`checkpoint v2 recover success`仅表示legacy Chainbase checkpoint v2,不得记为三authority共同checkpoint成功。 + +详细时间线和不断变化的unit/head/磁盘数据见 +[AMD-002准备与同步记录](../../../../.dev_ops/depoly_node/records/2026-09-04-amd-002-archive-sync-prep.md)。 + +## ARM-001:主要开发辅助 + +### 当前同步持久化对照校准(2026-09-10) + +- ARM-001 文档曾记录 r15 的同步 durable 配置,但 2026-09-11 现场 live PID `1665805` 实际使用 + `config-p66-live.conf`:JDK17、Chainbase/PathState/Hot/serving 均 RocksDB,`pathStateRoot.mode=shadow`;因此 + r15 记录不是当前 live 配置,不能用于当前性能对照。 +- AMD-002 当前 live PID `2725661` 实际使用显式 JDK8、Chainbase LevelDB、同步 PathState/Archive 语义,MemoryHigh/Max + 为 29/30 GiB;JDK17+RocksDB9.7.4 仅在隔离候选窗口验证,未切换 live。 +- 因此此前“ARM 异步/AMD 同步”的速度归因已撤销。后续比较必须绑定 DB engine/JNI、JDK、JVM/cgroup、config hash、 + 起始 head/hash、tx/block 与窗口;不能把 ARM 的历史 async 窗口与 AMD 的 synchronous 窗口直接比较。 + +- `ddb0d6ef33` ARM/JDK17 clean JAR SHA-256为 + `7921af4c80d7e46b303c33c4c8b25f407057ad90607ffc9c3cfa82c6a3cd4203`,embedded commit正确、 + `git.dirty=false`; +- head-38 MR-06 base的两个独立reflink副本完成13次apply关联和12,594-byte共同WAL SIGKILL;首次恢复唯一 + `mode=recover`到head 39,第二次为zero-action,恢复后四authority hash逐项不变; +- apply阶段中Archive materialize均值23.28 ms且12/13次为列出的最大子阶段;Chainbase materialize均值21.50 ms, + redo/runtime total均值74.24/78.77 ms。由于fresh私链无真实交易且r5并发运行,只用于决定AMD观测字段; +- 最终candidate进程、open fd和WAL均为0;r5仍为PID 747996、active、0 restart。证据根为 + `/data/blade/common-checkpoint-instrumentation-ddb0d6ef33`; +- `37723917cb` MR-06根:`/data/blade/common-checkpoint-memory-rebase-37723917cb`;ARM JDK17 JAR SHA-256 + `89420091236810c13ec030b06b78fdd9b41a148079de78ea181845f006078788`; +- 为避免隐式格式迁移,旧format-v2隔离副本只记录fail-closed,不被打开或重建;验证改用fresh format-v1私链; +- 命中11,141-byte共同WAL后精确SIGKILL;首次恢复退役WAL,第二次恢复仍在head 38附着,四个authority marker + checksum逐项稳定;r5的PID、restart count和数据路径未变化; +- 四个新增memory-rebase测试4/4以及framework main/test Checkstyle通过。完整ARM集合的两个失败分别来自 + aarch64 LevelDB JNI不可用和RocksDB 9.7 OPTIONS文本差异,不能将其写成完整ARM suite全绿; +- fresh私链接近实时出块导致effective flush近似逐块,不能当作maxFlushCount=10性能样本。header diagnostic + MISMATCH仍是独立OPEN Gate。 + +以下为更早的ARM开发证据。 + +- 本轮候选:base `bef5e53c`之上的36个未提交文件;规范化内容manifest + `211c6c89fa0d0b35e0e66eba41b8a80d43b0fa139efdfb9a568565219a435b22`; +- ARM JAR SHA-256 `ee676ad34406ce4f630f53d76ff073f19fa952303f6481e4f0eaf2d9b17c3acd`,配置SHA-256 + `9a99a4d5750bdc36ca85add5388b929cf838b20940194f4bbd890657450b754a`; +- 私链block-final实测PathState `durableWrites=0, journal=0`;SIGKILL保留11,227-byte共同WAL,恢复后WAL退役并在 + head 24附着,第二次启动head与三authority marker hash不变; +- 证据根:`/data/blade/common-checkpoint-dev-20260905-ec90022d`;原始失败副本和两个后续故障副本全部保留; +- 2026-09-05 03:32 UTC可用661,668,003,840 bytes,所有本轮FullNode unit已停止; +- 保留唯一full-valid base和r5历史证据,不用next-format代码原地打开r5目录; +- 每次运行前重查process、mount/device/filesystem、runtime/CURRENT、base identity、evidence和cgroup; +- 私链持续出现`Path-state header diagnostic: result=MISMATCH`,target-specific marker随checkpoint增长;二者和 + fresh mainnet容量均未关闭。本机结论仅为`development evidence`,不能替代AMD-002最终性能验收。 + +详细现场见[ARM-001当前部署](../../../../.dev_ops/current_depoly/tron-apse1-arm-001.md)。 + +## ARM-002:次要辅助/备用 + +- 当前保持`storage.pathStateRoot.enabled=false`,不复制、创建或启动PathState runtime; +- 当前允许作为Archive增长预估样本,与ARM-001 current stateRoot固定footprint分列后组合;不得称为同机A/B; +- 可用于Historical query固定语料、replay oracle、查询脚本和不需要PathState的辅助回归; +- 只在ARM-001不适合承载某个辅助任务时作为备用,不自动接管最终性能验证; +- 证据一律标记`auxiliary`。 + +详细现场见[ARM-002当前部署](../../../../.dev_ops/current_depoly/tron-apse1-arm-002.md)。 + +当前容量结果见[ARM-002 Archive增长与ARM-001 stateRoot容量预估](../results/storage-growth-arm002-state-root-arm001-20260905.md)。 +当前最高优先级问题的背景、指标和关闭条件见 +[`HT-001同步速度与可持续性`](../../problem/focus/state-root-sync-speed.md)。 + +## 当前下一验证链 + +1. 已完成:AMD-002以冻结`a2fc535a5a`、JAR、config和format-v1 ready输入完成无重建兼容接管; +2. 已完成:固定高度正常关闭/reopen并验证三authority共同checkpoint与zero-scan; +3. 已完成:部署`e92aae0c89`消除同一redo内重复open,取得固定500块对照并通过正常停机/恢复Gate; +4. 已完成:ARM-001对`37723917cb`执行真实WAL crash、恢复、第二次恢复和短窗资源MR-06; +5. 已完成:AMD-002以`37723917cb`冻结首个500块statistics;PathState没有明显回退,checkpoint为第一大项; +6. 进行中:保持run不变取得30分钟以上长窗,持续观察FGC、old-gen、cgroup anon/file、吞吐和磁盘; +7. 已完成:`ddb0d6ef33`在ARM-001通过13次phase关联、真实WAL crash和second recovery;Archive materialize为当前 + development样本均值最大子阶段; +8. 已完成:AMD `ddb0d6ef33` clean build、normal stop、reflink基线、continuity、3-checkpoint smoke及observer接管; +9. 已完成:首个精确500块和完整30分钟phase窗口;PathState rebase prepare为均值主项,Archive close为尾部主项; +10. 已完成:第二个完整窗口复现两项phase形态,源码关闭路径已定位,并形成单变量`CCI-O01`及A/B、风险和磁盘控制表; +11. 下一步等待人工选择是否准备`CCI-O01`本地实现与测试;未选择前保持live run及全部远端现场不变; +12. 同步完成后按[`AI-MANAGEMENT.md`](../../execution/AI-MANAGEMENT.md)暂停修改并执行项目控制恢复; +13. header diagnostic、target-marker retention、reorg/fault/power-loss与production acceptance仍是独立开放Gate。 From 63d9c7ccdcc8b839423013209ae65a66bd82e2c6 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 11 Sep 2026 00:19:05 +0800 Subject: [PATCH 154/161] docs(archive): record arm amd runtime alignment --- .../execution/records/ITER-20260911-406.md | 16 ++++++++++++++++ .helper/ai-archive/execution/tasks/TASK-023.md | 1 + 2 files changed, 17 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260911-406.md diff --git a/.helper/ai-archive/execution/records/ITER-20260911-406.md b/.helper/ai-archive/execution/records/ITER-20260911-406.md new file mode 100644 index 00000000000..5c3a2e7d4c8 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260911-406.md @@ -0,0 +1,16 @@ +# ITER-20260911-406 + +## Slice + +HLT-002 ARM/AMD runtime alignment requested by operator. + +## Observed + +- ARM-001 (`10.255.10.101`) live JVM is OpenJDK `17.0.20` and its current unit already has `MemoryHigh=MemoryMax=31138512896` (30 GiB), with swap disabled. The effective config has `pathStateRoot.mode=shadow`, `volatileSnapshotBenchmark=false`, and `asyncPrepareBenchmark=false`. +- `StorageConfig.PathStateRootConfig.postProcess()` rejects every mode other than `shadow`; writing literal `mode=sync` would fail closed at startup. The effective synchronous behavior is represented by the two benchmark flags remaining false. +- AMD-002 was switched from explicit JDK8 to the signed JDK17+RocksDB9.7.4 candidate JAR, with `-Dtron.java17.x86.candidate=true`, MemoryHigh 29 GiB, MemoryMax 30 GiB, and swap disabled. Unit `amd002-state-archive-current-9ea27f2797-live` is active with zero restarts, ports 8090/18888/9527 listening. +- After startup, AMD head advanced `85151840→85151866` in about 60 seconds; this is a live liveness sample, not a long-term performance claim. + +## Conclusion + +The requested JDK17 AMD live switch is complete. ARM already matches JDK17 and 30-GiB cgroup limits, but cannot be changed to a literal `sync` mode without a separate format/config contract change; current flags already disable volatile and async benchmark paths. Fair performance comparison remains blocked on aligning ARM's actual runtime mode/engine semantics with AMD and collecting matched windows. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index e9ffb66781d..b32ba3974c9 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -32,6 +32,7 @@ active_branch: feature/archive_block2 - 第二份完整快照在 control 保持停止期间启动 JDK17 smoke 成功,HTTP/PBFT API 全部启动并返回快照 head;候选随后停止并恢复 JDK8 control。 - 在完整性校验通过且 control 全程停止的前提下,JDK17+RocksDB9.7.4 P2P 窗口从 `85151013` 到 `85151045`(120 秒 +32,约 `0.267 block/s`),采样 CPU 约 471%、RSS 约 9.8 GB;窗口后已恢复 control。该值仍需与同快照 JDK8 交错复测。 - 对比校准:真正 ARM-001 为 `10.255.10.101`,当前 live 实际是 JDK8、Chainbase RocksDB、`pathStateRoot.mode=shadow`,且 unit 无 MemoryHigh/Max;AMD-002 是 JDK8、Chainbase LevelDB、同步 PathState、30 GiB cgroup。不能把 ARM 历史 shadow/无硬内存上限窗口与 AMD 同步/30 GiB窗口直接比较。 +- 2026-09-11 对齐动作:ARM-001 现场确认已是 JDK17、RocksDB、shadow 模式且 `volatileSnapshotBenchmark=false`/`asyncPrepareBenchmark=false`,unit 已是 MemoryHigh/Max=30 GiB;代码不接受字面 `mode=sync`。AMD-002 已切换 JDK17+RocksDB9.7.4 live,启动后 60 秒 `85151840→85151866`、0 restart。 ## 实验顺序 From 0e51f17674932418cfbae1c569a2a3551f453575 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 11 Sep 2026 00:35:48 +0800 Subject: [PATCH 155/161] docs(archive): record aligned live model deployment --- .../current/current-validation-status.md | 10 +++++----- .../execution/records/ITER-20260911-407.md | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 .helper/ai-archive/execution/records/ITER-20260911-407.md diff --git a/.helper/ai-archive/evidence/current/current-validation-status.md b/.helper/ai-archive/evidence/current/current-validation-status.md index ccb445eb92c..1180a6a675e 100644 --- a/.helper/ai-archive/evidence/current/current-validation-status.md +++ b/.helper/ai-archive/evidence/current/current-validation-status.md @@ -164,11 +164,11 @@ ### 当前同步持久化对照校准(2026-09-10) -- ARM-001 文档曾记录 r15 的同步 durable 配置,但 2026-09-11 现场 live PID `1665805` 实际使用 - `config-p66-live.conf`:JDK17、Chainbase/PathState/Hot/serving 均 RocksDB,`pathStateRoot.mode=shadow`;因此 - r15 记录不是当前 live 配置,不能用于当前性能对照。 -- AMD-002 当前 live PID `2725661` 实际使用显式 JDK8、Chainbase LevelDB、同步 PathState/Archive 语义,MemoryHigh/Max - 为 29/30 GiB;JDK17+RocksDB9.7.4 仅在隔离候选窗口验证,未切换 live。 +- ARM-001 文档曾记录 r15 的同步 durable 配置;2026-09-11 对齐后 live 使用 + `config-p66-live.conf`:JDK17、Chainbase/PathState/Hot/serving 均 RocksDB,`pathStateRoot.mode=shadow`、 + `volatileSnapshotBenchmark=false`、`asyncPrepareBenchmark=false`,unit MemoryHigh/Max=29/30 GiB。 +- AMD-002 2026-09-11 已切换 live PID `2726724`:显式 JDK17 + RocksDB9.7.4,Chainbase LevelDB、同步 + PathState/Archive 语义,MemoryHigh/Max=29/30 GiB;JDK17 x86 通过显式候选属性放行。 - 因此此前“ARM 异步/AMD 同步”的速度归因已撤销。后续比较必须绑定 DB engine/JNI、JDK、JVM/cgroup、config hash、 起始 head/hash、tx/block 与窗口;不能把 ARM 的历史 async 窗口与 AMD 的 synchronous 窗口直接比较。 diff --git a/.helper/ai-archive/execution/records/ITER-20260911-407.md b/.helper/ai-archive/execution/records/ITER-20260911-407.md new file mode 100644 index 00000000000..bbfcc51a562 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260911-407.md @@ -0,0 +1,16 @@ +# ITER-20260911-407 + +## Slice + +HLT-002 ARM/AMD aligned live model deployment. + +## Observed + +- ARM-001 was restarted with explicit `/usr/lib/jvm/java-17-openjdk-arm64/bin/java`, Xmx18G/direct1G, MemoryHigh=29 GiB, MemoryMax=30 GiB, MemorySwapMax=0. Its effective `pathStateRoot.mode=shadow` remains required by `StorageConfig`; both volatile and async benchmark flags are false, which is the supported synchronous path. +- The first restart attempt left the transient unit in a stopped state; after reset-failed and a clean re-create, `state-archive-8ac7-live.service` became active with PID `1811527`, zero restarts, and the 30-GiB cgroup settings. +- AMD-002 was switched live to explicit JDK17 + RocksDB9.7.4 candidate JAR with `tron.java17.x86.candidate=true`, MemoryHigh=29 GiB, MemoryMax=30 GiB, swap disabled. Unit `amd002-state-archive-current-9ea27f2797-live` is active with PID `2726724`, zero restarts, ports 8090/18888/9527 listening, and head advanced `85151840→85151866` in approximately 60 seconds. +- ARM's literal `mode=sync` was not written because the current code rejects it at config post-processing; changing that contract is a separate implementation task. + +## Conclusion + +Both live nodes now use JDK17 and the same 29-GiB high/30-GiB max cgroup envelope. Their storage engines and supported PathState mode still differ (ARM Chainbase RocksDB; AMD Chainbase LevelDB), so future speed comparisons must retain that distinction. From ee77389a923a32ddee76d76bcf0c3186b7ddc036 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 11 Sep 2026 00:54:43 +0800 Subject: [PATCH 156/161] docs(archive): record aligned chainbase baseline rates --- .../execution/records/ITER-20260911-408.md | 16 ++++++++++++++++ .helper/ai-archive/execution/tasks/TASK-023.md | 1 + 2 files changed, 17 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260911-408.md diff --git a/.helper/ai-archive/execution/records/ITER-20260911-408.md b/.helper/ai-archive/execution/records/ITER-20260911-408.md new file mode 100644 index 00000000000..0bbc38b1adc --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260911-408.md @@ -0,0 +1,16 @@ +# ITER-20260911-408 + +## Slice + +HLT-002 aligned live baseline rate observation. + +## Observed + +- ARM-001 and AMD-002 are both live on JDK17 with Xmx18G/direct1G, MemoryHigh 29 GiB, MemoryMax 30 GiB, swap disabled, Common checkpoint enabled, PathState/Archive RocksDB, `mode=shadow`, and volatile/async benchmark flags false. +- The remaining intentional engine difference is Chainbase: ARM RocksDB versus AMD LevelDB. +- Concurrent metrics sampling used `tron:block_process_latency_seconds_count{sync="true"}`. ARM increased `530→565` over about 73 seconds (`~0.48 block/s`); AMD increased `777→804` over about 76 seconds (`~0.36 block/s`). Header gauges at the later sample were ARM `83,851,674` and AMD `85,152,623`. +- This was online natural traffic, not locked tx/block, peers, network, or interleaved A/B. No restart or error gate was observed during sampling. + +## Conclusion + +The aligned baseline currently shows ARM processing faster than AMD by roughly 35% in this short observation, consistent with Chainbase engine as a candidate factor. It is not causal proof; use matched snapshot/traffic windows and collect per-block stage, I/O, and checkpoint/compaction metrics before attributing the gap to LevelDB versus RocksDB. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index b32ba3974c9..d524b84b56f 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -33,6 +33,7 @@ active_branch: feature/archive_block2 - 在完整性校验通过且 control 全程停止的前提下,JDK17+RocksDB9.7.4 P2P 窗口从 `85151013` 到 `85151045`(120 秒 +32,约 `0.267 block/s`),采样 CPU 约 471%、RSS 约 9.8 GB;窗口后已恢复 control。该值仍需与同快照 JDK8 交错复测。 - 对比校准:真正 ARM-001 为 `10.255.10.101`,当前 live 实际是 JDK8、Chainbase RocksDB、`pathStateRoot.mode=shadow`,且 unit 无 MemoryHigh/Max;AMD-002 是 JDK8、Chainbase LevelDB、同步 PathState、30 GiB cgroup。不能把 ARM 历史 shadow/无硬内存上限窗口与 AMD 同步/30 GiB窗口直接比较。 - 2026-09-11 对齐动作:ARM-001 现场确认已是 JDK17、RocksDB、shadow 模式且 `volatileSnapshotBenchmark=false`/`asyncPrepareBenchmark=false`,unit 已是 MemoryHigh/Max=30 GiB;代码不接受字面 `mode=sync`。AMD-002 已切换 JDK17+RocksDB9.7.4 live,启动后 60 秒 `85151840→85151866`、0 restart。 +- 对齐后在线观察(metrics):ARM block-process 计数 `530→565`(约 73 秒,`0.48 block/s`),AMD `777→804`(约 76 秒,`0.36 block/s`)。两端仅 Chainbase engine 仍不同,但该自然流量窗口未锁定 tx/block、peer/network 和时间交错,暂作基线不作因果结论。 ## 实验顺序 From b98e8862d788e5cecb19f6ac15eb349e7e603776 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 11 Sep 2026 00:57:55 +0800 Subject: [PATCH 157/161] docs(archive): arm aligned one k bottleneck observation --- .../execution/records/ITER-20260911-409.md | 16 ++++++++++++++++ .helper/ai-archive/execution/tasks/TASK-023.md | 1 + 2 files changed, 17 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260911-409.md diff --git a/.helper/ai-archive/execution/records/ITER-20260911-409.md b/.helper/ai-archive/execution/records/ITER-20260911-409.md new file mode 100644 index 00000000000..7e494f4abdb --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260911-409.md @@ -0,0 +1,16 @@ +# ITER-20260911-409 + +## Slice + +HLT-002 aligned live 1k-block observation setup. + +## Observed + +- ARM-001 current header gauge at setup: `83,851,674`; 1k target: `83,852,674`. +- AMD-002 current header gauge at setup: `85,152,623`; 1k target: `85,153,623`. +- Both nodes remain JDK17 with Xmx18G/direct1G, MemoryHigh 29 GiB, MemoryMax 30 GiB, swap disabled, Common checkpoint enabled, PathState/Archive RocksDB, `mode=shadow`, and volatile/async benchmark flags false. Chainbase remains the sole intentional engine difference (ARM RocksDB, AMD LevelDB). +- The observation contract is to retain complete 1k ranges and collect header/block-process counters, PushBlock and checkpoint stage timings, tx count/tx per block, CPU, I/O PSI, RSS/cgroup memory, GC and error/restart counters. No cross-height rate merging is allowed. + +## Conclusion + +The 1k-block quantitative bottleneck observation is armed from explicit per-node anchors. Final attribution is deferred until each target is reached and the complete windows are exported; expected candidates are Chainbase engine write/compaction cost, checkpoint amortization, and I/O wait. diff --git a/.helper/ai-archive/execution/tasks/TASK-023.md b/.helper/ai-archive/execution/tasks/TASK-023.md index d524b84b56f..a842dc19eb9 100644 --- a/.helper/ai-archive/execution/tasks/TASK-023.md +++ b/.helper/ai-archive/execution/tasks/TASK-023.md @@ -34,6 +34,7 @@ active_branch: feature/archive_block2 - 对比校准:真正 ARM-001 为 `10.255.10.101`,当前 live 实际是 JDK8、Chainbase RocksDB、`pathStateRoot.mode=shadow`,且 unit 无 MemoryHigh/Max;AMD-002 是 JDK8、Chainbase LevelDB、同步 PathState、30 GiB cgroup。不能把 ARM 历史 shadow/无硬内存上限窗口与 AMD 同步/30 GiB窗口直接比较。 - 2026-09-11 对齐动作:ARM-001 现场确认已是 JDK17、RocksDB、shadow 模式且 `volatileSnapshotBenchmark=false`/`asyncPrepareBenchmark=false`,unit 已是 MemoryHigh/Max=30 GiB;代码不接受字面 `mode=sync`。AMD-002 已切换 JDK17+RocksDB9.7.4 live,启动后 60 秒 `85151840→85151866`、0 restart。 - 对齐后在线观察(metrics):ARM block-process 计数 `530→565`(约 73 秒,`0.48 block/s`),AMD `777→804`(约 76 秒,`0.36 block/s`)。两端仅 Chainbase engine 仍不同,但该自然流量窗口未锁定 tx/block、peer/network 和时间交错,暂作基线不作因果结论。 +- 1k 观察锚点:ARM header `83,851,674`,目标 `83,852,674`;AMD header `85,152,623`,目标 `85,153,623`。两端继续保持 JDK17、29/30 GiB cgroup、同 PathState/Archive/Common 语义;达到目标后按完整 1k 窗口汇总 PushBlock、checkpoint、tx/block、CPU、I/O PSI、RSS/cgroup 与 GC。 ## 实验顺序 From 1ab34783c97efe4c22dfa932ed0819a86205050c Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 11 Sep 2026 01:05:31 +0800 Subject: [PATCH 158/161] feat(archive): add hlt002 one k watch --- .../execution/scripts/hlt002-1k-watch.sh | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100755 .helper/ai-archive/execution/scripts/hlt002-1k-watch.sh diff --git a/.helper/ai-archive/execution/scripts/hlt002-1k-watch.sh b/.helper/ai-archive/execution/scripts/hlt002-1k-watch.sh new file mode 100755 index 00000000000..ae0dad57929 --- /dev/null +++ b/.helper/ai-archive/execution/scripts/hlt002-1k-watch.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -u + +if [[ $# -ne 3 ]]; then + echo "usage: $0 TARGET_HEAD UNIT OUTPUT_TSV" >&2 + exit 2 +fi + +target=$1 +unit=$2 +output=$3 +mkdir -p "$(dirname "$output")" +if [[ ! -s "$output" ]]; then + printf 'epoch\thead\tprocess_count\tpush_count\tpid\trestarts\tmemory_current\tmemory_high\tmemory_max\tpsi_io_some_avg60\tpsi_io_full_avg60\tcpu_pct\trss_kib\n' > "$output" +fi + +while :; do + epoch=$(date +%s) + metrics=$(curl -fsS --max-time 10 http://127.0.0.1:9527/metrics 2>/dev/null || true) + head=$(printf '%s\n' "$metrics" | awk '/^tron:header_height / {printf "%.0f", $2; exit}') + process_count=$(printf '%s\n' "$metrics" | awk '/^tron:block_process_latency_seconds_count\{sync="true"/ {print $2; exit}') + push_count=$(printf '%s\n' "$metrics" | awk '/^tron:block_push_latency_seconds_count / {print $2; exit}') + pid=$(systemctl show "$unit" -p MainPID --value 2>/dev/null || printf 0) + restarts=$(systemctl show "$unit" -p NRestarts --value 2>/dev/null || printf 0) + cg=/sys/fs/cgroup/system.slice/${unit} + current=$(cat "$cg/memory.current" 2>/dev/null || printf 0) + high=$(cat "$cg/memory.high" 2>/dev/null || printf 0) + max=$(cat "$cg/memory.max" 2>/dev/null || printf 0) + psi_some=$(awk '/^some / {for (i=1;i<=NF;i++) if ($i ~ /^avg60=/) {sub("avg60=", "", $i); print $i}}' /proc/pressure/io) + psi_full=$(awk '/^full / {for (i=1;i<=NF;i++) if ($i ~ /^avg60=/) {sub("avg60=", "", $i); print $i}}' /proc/pressure/io) + cpu=0; rss=0 + if [[ "$pid" =~ ^[1-9][0-9]*$ ]]; then + read -r cpu rss < <(ps -p "$pid" -o %cpu=,rss= 2>/dev/null || echo 0 0) + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$epoch" "$head" "$process_count" "$push_count" "$pid" "$restarts" \ + "$current" "$high" "$max" "$psi_some" "$psi_full" "$cpu" "$rss" >> "$output" + if [[ "$head" =~ ^[0-9]+$ ]] && (( head >= target )); then + break + fi + sleep 60 +done From eb0a2baeedd7cf35533d884566708e86dc34a1be Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 11 Sep 2026 01:07:01 +0800 Subject: [PATCH 159/161] fix(archive): resolve cgroup path in one k watch --- .helper/ai-archive/execution/scripts/hlt002-1k-watch.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.helper/ai-archive/execution/scripts/hlt002-1k-watch.sh b/.helper/ai-archive/execution/scripts/hlt002-1k-watch.sh index ae0dad57929..5b352c78e93 100755 --- a/.helper/ai-archive/execution/scripts/hlt002-1k-watch.sh +++ b/.helper/ai-archive/execution/scripts/hlt002-1k-watch.sh @@ -9,6 +9,7 @@ fi target=$1 unit=$2 output=$3 +unit_cgroup=${unit%.service}.service mkdir -p "$(dirname "$output")" if [[ ! -s "$output" ]]; then printf 'epoch\thead\tprocess_count\tpush_count\tpid\trestarts\tmemory_current\tmemory_high\tmemory_max\tpsi_io_some_avg60\tpsi_io_full_avg60\tcpu_pct\trss_kib\n' > "$output" @@ -22,7 +23,7 @@ while :; do push_count=$(printf '%s\n' "$metrics" | awk '/^tron:block_push_latency_seconds_count / {print $2; exit}') pid=$(systemctl show "$unit" -p MainPID --value 2>/dev/null || printf 0) restarts=$(systemctl show "$unit" -p NRestarts --value 2>/dev/null || printf 0) - cg=/sys/fs/cgroup/system.slice/${unit} + cg=/sys/fs/cgroup/system.slice/${unit_cgroup} current=$(cat "$cg/memory.current" 2>/dev/null || printf 0) high=$(cat "$cg/memory.high" 2>/dev/null || printf 0) max=$(cat "$cg/memory.max" 2>/dev/null || printf 0) From 4d47089d741b27ed427ae9b3d36e748883edbdc6 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 11 Sep 2026 01:07:42 +0800 Subject: [PATCH 160/161] docs(archive): record one k night watch --- .../execution/records/ITER-20260911-410.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260911-410.md diff --git a/.helper/ai-archive/execution/records/ITER-20260911-410.md b/.helper/ai-archive/execution/records/ITER-20260911-410.md new file mode 100644 index 00000000000..a622d192a2d --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260911-410.md @@ -0,0 +1,16 @@ +# ITER-20260911-410 + +## Slice + +HLT-002 1k-block night-watch deployment. + +## Observed + +- Watcher script `hlt002-1k-watch.sh` was committed as `1ab34783c9` and cgroup-path fix as `eb0a2baeed`, then installed on ARM-001 and AMD-002. +- ARM watcher unit `arm001-hlt002-1k-watch` is active; first corrected sample head `83,851,985`, process count `882`, cgroup current `16,640,135,168` bytes, CPU `464%`, RSS `12,186,192 KiB`. +- AMD watcher unit `amd002-hlt002-1k-watch` is active; first corrected sample head `85,152,911`, process count `1,096`, cgroup current `30,057,975,808` bytes, CPU `367%`, RSS `21,535,316 KiB`, IO PSI avg60 about `19.16%`. +- Targets remain ARM `83,852,959` and AMD `85,153,885`; watchers stop themselves at target and preserve TSV evidence. + +## Conclusion + +Night watch is active on both live nodes. The first corrected sample already shows materially higher AMD memory pressure and IO PSI; final bottleneck attribution waits for complete 1k ranges and stage counters. From 49503efcc964094005d48fa45143b22c77550e17 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 11 Sep 2026 10:10:50 +0800 Subject: [PATCH 161/161] docs(archive): analyze aligned one k bottleneck --- .../execution/records/ITER-20260911-411.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .helper/ai-archive/execution/records/ITER-20260911-411.md diff --git a/.helper/ai-archive/execution/records/ITER-20260911-411.md b/.helper/ai-archive/execution/records/ITER-20260911-411.md new file mode 100644 index 00000000000..e6aeb23a947 --- /dev/null +++ b/.helper/ai-archive/execution/records/ITER-20260911-411.md @@ -0,0 +1,21 @@ +# ITER-20260911-411 + +## Slice + +HLT-002 completed aligned 1k-block observation and bottleneck attribution. + +## Observed + +- ARM-001 completed `83,851,963→83,852,970`: +1007 blocks over 2034 s, `0.495084 block/s`; process/push counters both +1007. Average CPU 472.2%, RSS 16.71 GiB, cgroup current 21.64 GiB (max observed 24.16 GiB), IO PSI avg60 sample mean 6.55%. +- AMD-002 completed `85,152,892→85,153,903`: +1011 blocks over 2816 s, `0.359020 block/s`; process/push counters both +1012. Average CPU 354.8%, RSS 20.67 GiB, cgroup current 28.05 GiB (max observed 29.00 GiB), IO PSI avg60 sample mean 15.16%. +- Relative wall-clock result: AMD was about 27.5% slower (`0.359` vs `0.495 block/s`). Both nodes stayed active with zero restarts; watchers stopped automatically at target. +- ARM's `tron.log` had rotated/stopped before this window and systemd journal did not retain matching PushBlock lines, so complete ARM PushBlock/checkpoint stage P50/P95 cannot be reconstructed from this run. Current histogram totals are cumulative and not valid as a 1k delta without a preserved baseline. + +## Quantitative attribution + +- The strongest measured differentiator is storage pressure: AMD mean IO PSI was about 2.3x ARM and mean cgroup current was about 6.4 GiB higher, while AMD CPU utilization was lower. This is consistent with LevelDB Chainbase write/compaction and the 30-GiB memory envelope causing more reclaim/IO stalls. +- Chainbase engine remains the only intentional software engine difference, but this run is not a causal engine benchmark: tx/block, peer/network timing, and per-stage checkpoint logs were not frozen. Follow-up should preserve PushBlock/checkpoint logs or scrape stage metrics at the start/end of the 1k window. + +## Conclusion + +The complete 1k observation confirms a material AMD throughput gap and quantitatively points to memory/IO pressure rather than CPU starvation. It does not yet prove LevelDB as the sole cause; a matched fixed-input or retained-log 1k A/B is still required for final engine attribution.