Skip to content

Commit 7ab8945

Browse files
authored
fix(db): re-verify all transaction signatures on fork switch (#6864)
The switchFork new-branch apply loop only re-validated each block's witness signature; the transactions inside were applied through the isVerified cache. That flag caches a signature-verification result computed against a specific account-permission state, but a fork switch re-applies blocks on a rewound, diverged chain state where those permissions may have changed. Trusting the stale flag lets a transaction skip verification and be accepted with a signature no longer valid under the fork-chain state. - Manager.switchFork: clear isVerified on every transaction of each block on the branch being switched to, before applyBlock, forcing full signature re-validation against the fork-chain state. The switch-back path (original main branch) is left untouched: it reproduces the exact original state, so its cached verifications stay valid and resetting them would only cost perf. - ManagerTest: add switchForkShouldResetTransactionSignVerifiedOnNewBranch, which drives a real reorg and asserts the new branch's transaction gets its cached verification flag cleared (fails without the fix).
1 parent b562449 commit 7ab8945

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,13 @@ private void switchFork(BlockCapsule newHead)
11461146
throw new ValidateSignatureException(
11471147
"switch fork: block " + item.getBlk().getNum() + " signature invalid");
11481148
}
1149+
// The new branch is applied on a rewound, diverged state where account permissions
1150+
// may have changed, so a cached signature-verification result is no longer
1151+
// trustworthy. Clear it to force every transaction to re-validate its signature
1152+
// against the fork-chain state.
1153+
for (TransactionCapsule tx : item.getBlk().getTransactions()) {
1154+
tx.setVerified(false);
1155+
}
11491156
applyBlock(item.getBlk().setSwitch(true));
11501157
tmpSession.commit();
11511158
} catch (AccountResourceInsufficientException

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import static org.junit.Assert.assertThrows;
44
import static org.mockito.ArgumentMatchers.any;
55
import static org.mockito.ArgumentMatchers.anyBoolean;
6+
import static org.mockito.Mockito.atLeastOnce;
67
import static org.mockito.Mockito.doThrow;
78
import static org.mockito.Mockito.mock;
89
import static org.mockito.Mockito.spy;
10+
import static org.mockito.Mockito.verify;
911
import static org.mockito.Mockito.when;
1012
import static org.tron.common.utils.Commons.adjustAssetBalanceV2;
1113
import static org.tron.common.utils.Commons.adjustTotalShieldedPoolValue;
@@ -1771,6 +1773,77 @@ public void switchForkShouldPostFullNodeFilterForNewBranch() throws Exception {
17711773
hasBlockFilterCapsule(queue, b2));
17721774
}
17731775

1776+
/**
1777+
* A fork switch re-applies the new branch on a rewound, diverged state, so any signature
1778+
* verification cached on those transactions (isVerified) must be cleared to force
1779+
* re-validation against the fork-chain state. Drives a real reorg and asserts that switchFork
1780+
* resets isVerified on the transactions of the branch it switches to.
1781+
*/
1782+
@Test
1783+
public void switchForkShouldResetTransactionSignVerifiedOnNewBranch() throws Exception {
1784+
// bootstrap a head with a known witness
1785+
String key = PublicMethod.getRandomPrivateKey();
1786+
byte[] privateKey = ByteArray.fromHexString(key);
1787+
final ECKey ecKey = ECKey.fromPrivate(privateKey);
1788+
byte[] address = ecKey.getAddress();
1789+
ByteString addressByte = ByteString.copyFrom(address);
1790+
chainManager.getAccountStore().put(addressByte.toByteArray(),
1791+
new AccountCapsule(Protocol.Account.newBuilder().setAddress(addressByte).build()));
1792+
WitnessCapsule witnessCapsule = new WitnessCapsule(addressByte);
1793+
chainManager.getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>());
1794+
chainManager.addWitness(addressByte);
1795+
chainManager.getWitnessStore().put(address, witnessCapsule);
1796+
Block block = blockGenerate.getSignedBlock(
1797+
witnessCapsule.getAddress(), 1533529947843L, privateKey);
1798+
dbManager.pushBlock(new BlockCapsule(block));
1799+
1800+
Map<ByteString, String> keys = addTestWitnessAndAccount();
1801+
keys.put(addressByte, key);
1802+
1803+
// fund an owner; transfers go owner -> witness 'address' (an existing account)
1804+
ECKey ownerKey = new ECKey(Utils.getRandom());
1805+
byte[] owner = ownerKey.getAddress();
1806+
AccountCapsule ownerAccount = new AccountCapsule(
1807+
Protocol.Account.newBuilder().setAddress(ByteString.copyFrom(owner)).build());
1808+
ownerAccount.setBalance(1_000_000_000L);
1809+
chainManager.getAccountStore().put(owner, ownerAccount);
1810+
1811+
long t = 1533529947843L;
1812+
long base = chainManager.getDynamicPropertiesStore().getLatestBlockHeaderNumber();
1813+
long expiration = t + 1_000_000L;
1814+
1815+
// common ancestor P (empty) — fork point and tapos reference
1816+
BlockCapsule p = createTestBlockCapsule(t + 3000, base + 1,
1817+
chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash().getByteString(), keys);
1818+
dbManager.pushBlock(p);
1819+
1820+
// old branch: A extends P via the normal path and becomes head
1821+
BlockCapsule a = blockWithTransfer(t + 6000, base + 2, p.getBlockId().getByteString(), keys,
1822+
transfer(owner, address, 1L, p, expiration));
1823+
dbManager.pushBlock(a);
1824+
Assert.assertEquals("control: head should be A after normal extend",
1825+
a.getBlockId(), chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash());
1826+
1827+
// heavier competing branch P -> B1 -> B2 forces switchFork; spy the tx on the branch we
1828+
// switch to and pre-mark it verified to mimic a stale cache computed on a different state
1829+
BlockCapsule b1 = blockWithTransfer(t + 6001, base + 2, p.getBlockId().getByteString(), keys,
1830+
transfer(owner, address, 2L, p, expiration));
1831+
dbManager.pushBlock(b1); // num <= head -> kept in khaosDb, no switch yet
1832+
1833+
TransactionCapsule forkTx = transfer(owner, address, 3L, p, expiration);
1834+
forkTx.setVerified(true);
1835+
TransactionCapsule spyTx = spy(forkTx);
1836+
BlockCapsule b2 = blockWithTransfer(t + 9000, base + 3, b1.getBlockId().getByteString(), keys,
1837+
spyTx);
1838+
dbManager.pushBlock(b2); // num > head & parent != head -> triggers switchFork
1839+
1840+
Assert.assertEquals("reorg must switch the canonical head to the competing branch (B2)",
1841+
b2.getBlockId(), chainManager.getDynamicPropertiesStore().getLatestBlockHeaderHash());
1842+
// switchFork must clear the cached verification flag on the new branch's transaction so it
1843+
// re-validates against the fork-chain state
1844+
verify(spyTx, atLeastOnce()).setVerified(false);
1845+
}
1846+
17741847
private TransactionCapsule transfer(byte[] owner, byte[] to, long amount,
17751848
BlockCapsule refBlock, long expiration) {
17761849
TransferContract contract = TransferContract.newBuilder()

0 commit comments

Comments
 (0)