fix(db): re-verify block signature during fork replay#6777
Conversation
- Manager.switchFork now re-validates each replayed block's witness signature before applying. The witness account's permission can change between forks (via permission-update transactions), so a signature that was valid on the original chain may no longer be valid on the replay path. - Use `if (!validateSignature(...)) throw new ValidateSignatureException` rather than discarding the boolean return: validateSignature only throws on malformed signature bytes; an attacker-supplied valid-format signature with a wrong-signer address returns false. Discarding the return would let that attack through. - The existing switchFork catch list already includes ValidateSignatureException, so the new throw is wired into the existing switchback path with no additional handling. - Add three BlockCapsule.validateSignature contract tests pinning the two failure modes the fix relies on: signer-mismatch returns false, signer-match returns true, and a 65-byte malformed signature throws ValidateSignatureException.
2d44164 to
70e5b2e
Compare
| Exception exception = null; | ||
| // todo process the exception carefully later | ||
| try (ISession tmpSession = revokingStore.buildSession()) { | ||
| if (!item.getBlk().validateSignature( |
There was a problem hiding this comment.
[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 and validateSignature() throws NPE. This unchecked exception skips the existing switchback path. Please convert this case to false / ValidateSignatureException or otherwise route it through the same rollback handling.
There was a problem hiding this comment.
- When a block arrives from the network, it first goes through
TronNetDelegate.validBlock— which already invokesblock.validateSignature(dynamicPropertiesStore, accountStore)once. - If
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.
| // 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 { |
There was a problem hiding this comment.
[MUST] This test still passes if production code calls validateSignature() but ignores the false return value, because it catches any Throwable and only verifies the method was invoked. Please assert the reflected invocation fails specifically with ValidateSignatureException, and verify setSwitch(true) / apply path is not reached for the invalid block.
There was a problem hiding this comment.
Thanks for the heads-up. This was considered — the boolean return contract of validateSignature is already pinned independently in BlockCapsuleTest by three contract tests; this ManagerMockTest test is here to pin that the switchFork replay path actually reaches validateSignature. The two layers together already cover the regression. Adding a cause assertion plus verify(never()).setSwitch(true) here would couple the test to the implementation detail that applyBlock calls setSwitch via its argument expression — a future refactor would break this test for the wrong reason. Prefer to keep the current form.
What does this PR do?
if (!validateSignature(...)) throw new ValidateSignatureExceptionrather than discarding the boolean return: validateSignature only throws on malformed signature bytes; an attacker-supplied valid-format signature with a wrong-signer address returns false. Discarding the return would let that attack through.Why are these changes required?
This PR has been tested by:
Follow up
Extra details