Skip to content

Commit b5b8ee5

Browse files
committed
fix(vm): write TIP-2935 parent hash before tx loop in generateBlock
processBlock writes the parent block hash before iterating transactions; generateBlock did not, so the producer's simulation loop saw a stale/empty slot at HISTORY_STORAGE_ADDRESS while validators saw the live value. Mirror processBlock's call order.
1 parent cbec094 commit b5b8ee5

2 files changed

Lines changed: 65 additions & 0 deletions

File tree

framework/src/main/java/org/tron/core/db/Manager.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,6 +1629,7 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) {
16291629
session.reset();
16301630
session.setValue(revokingStore.buildSession());
16311631

1632+
HistoryBlockHashUtil.write(this, blockCapsule);
16321633
accountStateCallBack.preExecute(blockCapsule);
16331634

16341635
if (getDynamicPropertiesStore().getAllowMultiSign() == 1) {

framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,24 @@
88
import static org.junit.Assert.assertTrue;
99

1010
import com.google.protobuf.ByteString;
11+
import java.lang.reflect.Field;
1112
import java.util.Arrays;
13+
import java.util.concurrent.atomic.AtomicReference;
1214
import org.junit.Before;
1315
import org.junit.Test;
16+
import org.mockito.Mockito;
1417
import org.tron.common.BaseTest;
1518
import org.tron.common.TestConstants;
19+
import org.tron.common.crypto.ECKey;
1620
import org.tron.common.runtime.vm.DataWord;
1721
import org.tron.common.utils.Sha256Hash;
22+
import org.tron.consensus.base.Param;
1823
import org.tron.core.capsule.AccountCapsule;
1924
import org.tron.core.capsule.BlockCapsule;
2025
import org.tron.core.capsule.CodeCapsule;
2126
import org.tron.core.capsule.ContractCapsule;
2227
import org.tron.core.config.args.Args;
28+
import org.tron.core.db.accountstate.callback.AccountStateCallBack;
2329
import org.tron.core.store.DynamicPropertiesStore;
2430
import org.tron.core.store.StoreFactory;
2531
import org.tron.core.vm.program.Storage;
@@ -245,6 +251,64 @@ public void deploySkipsWhenForeignContractPresent() {
245251
* upgrade the type to {@code Contract} in place — preserving balance —
246252
* rather than failing or zeroing the account.
247253
*/
254+
/**
255+
* SR / validator parity: the producer's {@code generateBlock} simulation
256+
* loop and the validator's {@code processBlock} apply loop must see the
257+
* same storage state when transactions hit {@code HISTORY_STORAGE_ADDRESS}.
258+
* That requires {@link HistoryBlockHashUtil#write} to run before the tx
259+
* loop on both paths. {@code processBlock} writes at line 1858; this test
260+
* pins the matching write inside {@code generateBlock}.
261+
*
262+
* <p>Spy {@code accountStateCallBack.preExecute} — called between the
263+
* write and the tx loop on both paths — and snapshot the slot from inside
264+
* the revoking session. Pre-fix the slot is empty (write never ran);
265+
* post-fix it holds the parent block hash.
266+
*/
267+
@Test
268+
public void generateBlockWritesParentHashBeforeTxLoop() throws Exception {
269+
chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(1L);
270+
HistoryBlockHashUtil.deploy(dbManager);
271+
272+
byte[] expectedParentHash = chainBaseManager.getHeadBlockId().getBytes();
273+
long nextBlockNum = chainBaseManager.getHeadBlockNum() + 1;
274+
long expectedSlot =
275+
(nextBlockNum - 1) % HistoryBlockHashUtil.HISTORY_SERVE_WINDOW;
276+
277+
Field cbField = Manager.class.getDeclaredField("accountStateCallBack");
278+
cbField.setAccessible(true);
279+
AccountStateCallBack realCb = (AccountStateCallBack) cbField.get(dbManager);
280+
AccountStateCallBack spy = Mockito.spy(realCb);
281+
AtomicReference<DataWord> captured = new AtomicReference<>();
282+
Mockito.doAnswer(inv -> {
283+
Storage st = new Storage(
284+
HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS,
285+
chainBaseManager.getStorageRowStore());
286+
captured.set(st.getValue(new DataWord(expectedSlot)));
287+
return inv.callRealMethod();
288+
}).when(spy).preExecute(Mockito.any(BlockCapsule.class));
289+
cbField.set(dbManager, spy);
290+
291+
try {
292+
ECKey ecKey = new ECKey();
293+
ByteString witness = ByteString.copyFrom(ecKey.getAddress());
294+
Param.Miner miner =
295+
Param.getInstance().new Miner(ecKey.getPrivKeyBytes(), witness, witness);
296+
long blockTime = System.currentTimeMillis() / 3000 * 3000;
297+
BlockCapsule generated = dbManager.generateBlock(
298+
miner, blockTime, System.currentTimeMillis() + 1000);
299+
assertNotNull("generateBlock returned null", generated);
300+
} finally {
301+
cbField.set(dbManager, realCb);
302+
}
303+
304+
assertNotNull(
305+
"preExecute fired with an empty slot — write() must run before preExecute",
306+
captured.get());
307+
assertArrayEquals(
308+
"slot must hold the parent block hash before the tx loop runs",
309+
expectedParentHash, captured.get().getData());
310+
}
311+
248312
@Test
249313
public void deployUpgradesPreExistingNormalAccountPreservingBalance() {
250314
byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS;

0 commit comments

Comments
 (0)