-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(db): re-verify block signature during fork replay #6777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,12 +5,14 @@ | |
| import static org.junit.Assert.assertTrue; | ||
| import static org.mockito.ArgumentMatchers.any; | ||
| import static org.mockito.ArgumentMatchers.anyString; | ||
| import static org.mockito.Mockito.atLeastOnce; | ||
| import static org.mockito.Mockito.doNothing; | ||
| import static org.mockito.Mockito.doThrow; | ||
| import static org.mockito.Mockito.mock; | ||
| import static org.mockito.Mockito.mockConstruction; | ||
| import static org.mockito.Mockito.mockStatic; | ||
| import static org.mockito.Mockito.spy; | ||
| import static org.mockito.Mockito.verify; | ||
| import static org.mockito.Mockito.when; | ||
|
|
||
| import com.google.protobuf.Any; | ||
|
|
@@ -22,6 +24,7 @@ | |
| import java.nio.charset.StandardCharsets; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.LinkedList; | ||
| import java.util.List; | ||
|
|
||
| import lombok.SneakyThrows; | ||
|
|
@@ -42,13 +45,15 @@ | |
| import org.tron.common.parameter.CommonParameter; | ||
| import org.tron.common.runtime.ProgramResult; | ||
| import org.tron.common.runtime.vm.LogInfo; | ||
| import org.tron.common.utils.Pair; | ||
| import org.tron.common.utils.Sha256Hash; | ||
| import org.tron.core.ChainBaseManager; | ||
| import org.tron.core.capsule.BlockCapsule; | ||
| import org.tron.core.capsule.TransactionCapsule; | ||
| import org.tron.core.capsule.TransactionInfoCapsule; | ||
| import org.tron.core.capsule.utils.TransactionUtil; | ||
| import org.tron.core.config.args.Args; | ||
| import org.tron.core.db2.ISession; | ||
| import org.tron.core.exception.ContractSizeNotEqualToOneException; | ||
| import org.tron.core.exception.DupTransactionException; | ||
| import org.tron.core.exception.ItemNotFoundException; | ||
|
|
@@ -564,4 +569,157 @@ public void testPostContractTriggerSwallowsThrowable() throws Exception { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Covers the fork-replay signature recheck added in this PR: | ||
| * when a block being re-applied during switchFork fails witness signature | ||
| * validation, the new `if (!validateSignature) throw` block must fire, | ||
| * surfacing ValidateSignatureException through the existing catch list. | ||
| * | ||
| * <p>Strategy: spy(Manager), inject mocked khaosDb/revokingStore/chainBaseManager | ||
| * so switchFork enters the first apply loop with a single mock block whose | ||
| * validateSignature returns false. The throw is exercised; downstream | ||
| * switchback/finally exceptions from partially-mocked applyBlock are tolerated | ||
| * since the throw line is already executed before they run. | ||
| */ | ||
| @SneakyThrows | ||
| @Test | ||
| public void testSwitchForkRejectsBlockWithInvalidSignature() { | ||
| Manager dbManager = spy(new Manager()); | ||
|
|
||
| // chainBaseManager + stores so getDynamicPropertiesStore() / getAccountStore() resolve. | ||
| ChainBaseManager cbm = mock(ChainBaseManager.class); | ||
| DynamicPropertiesStore dps = mock(DynamicPropertiesStore.class); | ||
| AccountStore accountStore = mock(AccountStore.class); | ||
| Sha256Hash sharedHash = Sha256Hash.ZERO_HASH; | ||
| when(cbm.getDynamicPropertiesStore()).thenReturn(dps); | ||
| when(cbm.getAccountStore()).thenReturn(accountStore); | ||
| when(dps.getLatestBlockHeaderHash()).thenReturn(sharedHash); | ||
| setField(dbManager, "chainBaseManager", cbm); | ||
|
|
||
| // revokingStore.buildSession() returns a no-op ISession. | ||
| RevokingDatabase revokingStore = mock(RevokingDatabase.class); | ||
| ISession session = mock(ISession.class); | ||
| when(revokingStore.buildSession()).thenReturn(session); | ||
| setField(dbManager, "revokingStore", revokingStore); | ||
|
|
||
| // khaosDb.getBranch returns (first=[badBlock], value=[oldBlock]). | ||
| // The bad block goes into the apply loop; the old block lets the while | ||
| // loops in the rollback/switchback paths exit immediately by matching | ||
| // parent hash to the current head hash. | ||
| KhaosDatabase khaosDb = mock(KhaosDatabase.class); | ||
| setField(dbManager, "khaosDb", khaosDb); | ||
|
|
||
| BlockCapsule badBlock = mock(BlockCapsule.class); | ||
| BlockCapsule.BlockId badBlockId = mock(BlockCapsule.BlockId.class); | ||
| when(badBlock.getBlockId()).thenReturn(badBlockId); | ||
| when(badBlock.getNum()).thenReturn(100L); | ||
| when(badBlock.validateSignature(any(DynamicPropertiesStore.class), | ||
| any(AccountStore.class))).thenReturn(false); | ||
|
|
||
| BlockCapsule oldBlock = mock(BlockCapsule.class); | ||
| BlockCapsule.BlockId oldBlockId = mock(BlockCapsule.BlockId.class); | ||
| when(oldBlock.getBlockId()).thenReturn(oldBlockId); | ||
| when(oldBlock.getParentHash()).thenReturn(sharedHash); | ||
|
|
||
| LinkedList<KhaosDatabase.KhaosBlock> first = new LinkedList<>(); | ||
| first.add(new KhaosDatabase.KhaosBlock(badBlock)); | ||
| LinkedList<KhaosDatabase.KhaosBlock> value = new LinkedList<>(); | ||
| value.add(new KhaosDatabase.KhaosBlock(oldBlock)); | ||
| when(khaosDb.getBranch(any(BlockCapsule.BlockId.class), any(Sha256Hash.class))) | ||
| .thenReturn(new Pair<>(first, value)); | ||
|
|
||
| Method switchFork = Manager.class.getDeclaredMethod("switchFork", BlockCapsule.class); | ||
| switchFork.setAccessible(true); | ||
|
|
||
| // The throw fires before the finally's switchback runs. Switchback's applyBlock | ||
| // may surface another exception due to partial mocks; we tolerate any throwable | ||
| // here because the new code's throw has already been executed (line covered). | ||
| try { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MUST] This test still passes if production code calls
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the heads-up. This was considered — the boolean return contract of |
||
| switchFork.invoke(dbManager, badBlock); | ||
| } catch (Throwable ignored) { | ||
| // expected: switchback path partially mocked | ||
| } | ||
|
|
||
| // The fix's contract: validateSignature was invoked on the replayed block. | ||
| verify(badBlock, atLeastOnce()).validateSignature( | ||
| any(DynamicPropertiesStore.class), any(AccountStore.class)); | ||
| } | ||
|
|
||
| /** | ||
| * Symmetric "happy path" coverage: when validateSignature returns true, the | ||
| * throw is skipped and execution continues to applyBlock. Pins that the | ||
| * new check correctly inverts the boolean (no off-by-one in the `!`). | ||
| */ | ||
| @SneakyThrows | ||
| @Test | ||
| public void testSwitchForkPassesValidSignatureBlockToApply() { | ||
| Manager dbManager = spy(new Manager()); | ||
|
|
||
| ChainBaseManager cbm = mock(ChainBaseManager.class); | ||
| DynamicPropertiesStore dps = mock(DynamicPropertiesStore.class); | ||
| AccountStore accountStore = mock(AccountStore.class); | ||
| Sha256Hash sharedHash = Sha256Hash.ZERO_HASH; | ||
| when(cbm.getDynamicPropertiesStore()).thenReturn(dps); | ||
| when(cbm.getAccountStore()).thenReturn(accountStore); | ||
| when(dps.getLatestBlockHeaderHash()).thenReturn(sharedHash); | ||
| setField(dbManager, "chainBaseManager", cbm); | ||
|
|
||
| RevokingDatabase revokingStore = mock(RevokingDatabase.class); | ||
| ISession session = mock(ISession.class); | ||
| when(revokingStore.buildSession()).thenReturn(session); | ||
| setField(dbManager, "revokingStore", revokingStore); | ||
|
|
||
| KhaosDatabase khaosDb = mock(KhaosDatabase.class); | ||
| setField(dbManager, "khaosDb", khaosDb); | ||
|
|
||
| BlockCapsule goodBlock = mock(BlockCapsule.class); | ||
| BlockCapsule.BlockId goodBlockId = mock(BlockCapsule.BlockId.class); | ||
| when(goodBlock.getBlockId()).thenReturn(goodBlockId); | ||
| when(goodBlock.getNum()).thenReturn(100L); | ||
| when(goodBlock.validateSignature(any(DynamicPropertiesStore.class), | ||
| any(AccountStore.class))).thenReturn(true); | ||
| // setSwitch returns self for chained call from applyBlock argument expression. | ||
| when(goodBlock.setSwitch(true)).thenReturn(goodBlock); | ||
|
|
||
| LinkedList<KhaosDatabase.KhaosBlock> first = new LinkedList<>(); | ||
| first.add(new KhaosDatabase.KhaosBlock(goodBlock)); | ||
| LinkedList<KhaosDatabase.KhaosBlock> value = new LinkedList<>(); | ||
| when(khaosDb.getBranch(any(BlockCapsule.BlockId.class), any(Sha256Hash.class))) | ||
| .thenReturn(new Pair<>(first, value)); | ||
|
|
||
| Method switchFork = Manager.class.getDeclaredMethod("switchFork", BlockCapsule.class); | ||
| switchFork.setAccessible(true); | ||
| try { | ||
| switchFork.invoke(dbManager, goodBlock); | ||
| } catch (Throwable ignored) { | ||
| // applyBlock against a mocked BlockCapsule will NPE somewhere; tolerated. | ||
| } | ||
|
|
||
| // Validation ran AND setSwitch was reached — proves the `if` did not short-circuit | ||
| // on the false branch when validateSignature returned true. | ||
| verify(goodBlock, atLeastOnce()).validateSignature( | ||
| any(DynamicPropertiesStore.class), any(AccountStore.class)); | ||
| verify(goodBlock, atLeastOnce()).setSwitch(true); | ||
| } | ||
|
|
||
| private static void setField(Object target, String name, Object value) throws Exception { | ||
| Field f = target.getClass().getSuperclass() != null | ||
| ? findField(target.getClass(), name) | ||
| : target.getClass().getDeclaredField(name); | ||
| f.setAccessible(true); | ||
| f.set(target, value); | ||
| } | ||
|
|
||
| private static Field findField(Class<?> cls, String name) throws NoSuchFieldException { | ||
| Class<?> c = cls; | ||
| while (c != null) { | ||
| try { | ||
| return c.getDeclaredField(name); | ||
| } catch (NoSuchFieldException e) { | ||
| c = c.getSuperclass(); | ||
| } | ||
| } | ||
| throw new NoSuchFieldException(name); | ||
| } | ||
|
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[MUST] In normal chain state a witness account should exist, but fork replay re-validates the block under a different state from the one used when the block was first accepted into KhaosDB. If the witness account/permission only existed on the old branch and is gone after rollback to the common ancestor,
accountStore.get(witnessAccountAddress)can return null andvalidateSignature()throws NPE. This unchecked exception skips the existing switchback path. Please convert this case tofalse/ValidateSignatureExceptionor otherwise route it through the same rollback handling.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TronNetDelegate.validBlock— which already invokesblock.validateSignature(dynamicPropertiesStore, accountStore)once.accountStore.get(witnessAccountAddress)had returned null at that point, thatvalidateSignaturecall would return false due to address mismatch before any NullPointerException could fire, andvalidBlockwould raise it asBLOCK_SIGN_INVALIDand reject the block — so it could never have entered KhaosDB, and would never reachswitchFork.