Skip to content

chore: guard non-validator from signing singnatures#1699

Merged
MicBun merged 3 commits into
mainfrom
guardSign
Apr 27, 2026
Merged

chore: guard non-validator from signing singnatures#1699
MicBun merged 3 commits into
mainfrom
guardSign

Conversation

@MicBun
Copy link
Copy Markdown
Collaborator

@MicBun MicBun commented Apr 27, 2026

resolves: https://github.com/truflation/website/issues/3752

Summary by CodeRabbit

  • Security
    • Validator-membership gate added to voting so only active validators with positive voting power can submit epoch votes; non-validator nodes no longer run validator signer/broadcast routines.
  • Tests
    • New test suite validating validator membership checks and signer behavior across active, non-active, zero-power, and absent-validator scenarios.

@MicBun MicBun requested a review from pr-time-tracker April 27, 2026 01:25
@MicBun MicBun self-assigned this Apr 27, 2026
@MicBun MicBun added the chore label Apr 27, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Warning

Rate limit exceeded

@MicBun has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 31 minutes and 17 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4bdb03bc-e048-4603-855a-bb4972c0d992

📥 Commits

Reviewing files that changed from the base of the PR and between 825a9af and 970cdbb.

📒 Files selected for processing (2)
  • node/exts/erc20-bridge/erc20/meta_extension.go
  • node/exts/erc20-bridge/erc20/validator_membership_test.go
📝 Walkthrough

Walkthrough

Adds an active-validator membership gate to the ERC20 bridge vote_epoch write path (nonce==0), rejecting non-validator callers before vote persistence and reusing a precomputed validator address→power map. Also disables the local validator signer broadcast loop on nodes that are not active validators.

Changes

Cohort / File(s) Summary
Validator membership & voting flow
node/exts/erc20-bridge/erc20/meta_extension.go
Adds membership gate for vote_epoch with nonce==0 that builds a secp256k1 validator Ethereum-address→voting-power map and rejects non-active-validator callers before persistence. Refactors sumEpochVotingPower to accept the prebuilt map. Adds helpers: buildValidatorEthAddressMap, isLocalNodeActiveValidator, isActiveValidatorAddress. Modifies getValidatorSigner to return nil and log once when local node is not an active validator, disabling signer broadcast.
Validator membership tests
node/exts/erc20-bridge/erc20/validator_membership_test.go
New build-tagged test suite with mocks and helpers. Tests isLocalNodeActiveValidator (positive-power secp256k1 membership, sentry/non-membership, zero power, bad key types, nil signer/validators, empty identity), isActiveValidatorAddress, and getValidatorSigner behavior (non-validator nodes return (nil, nil) even if a signer exists).

Sequence Diagram(s)

sequenceDiagram
  participant Caller as "Caller"
  participant Meta as "MetaExtension\n(vote_epoch)"
  participant ValStore as "ValidatorStore\n(build map)"
  participant Signer as "Local Signer\n(getValidatorSigner)"
  rect rgba(200,200,255,0.5)
  Caller->>Meta: submit vote_epoch(nonce==0, addr)
  Meta->>ValStore: buildValidatorEthAddressMap()
  ValStore-->>Meta: validatorAddr→power map
  Meta->>Meta: isLocalNodeActiveValidator(map, localIdentity)
  alt local not active
    Meta->>Caller: return error (not active validator)
  else local active
    Meta->>Signer: getValidatorSigner()
    Signer-->>Meta: signer (non-nil)
    Meta->>Meta: sumEpochVotingPower(using map)
    Meta->>ValStore: persist epoch vote
    Meta->>Caller: success
  end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

consensus-change

Suggested reviewers

  • pr-time-tracker

Poem

I hop to the bridge with a tiny decree,
I count all the keys that a validator be,
If your secp's listed and power is true,
I'll let you sign through the morning dew,
Else I thump my foot and say “not you!” 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title mentions guarding non-validators from signing, which aligns with the actual changes that gate non-validator nodes from the signer broadcast loop and add validator membership checks.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch guardSign

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@holdex
Copy link
Copy Markdown

holdex Bot commented Apr 27, 2026

Time Submission Status

Member Status Time Action Last Update
MicBun ✅ Submitted 4h Update time Apr 27, 2026, 3:50 AM

You can submit time with the command. Example:

@holdex pr submit-time 15m

See available commands to help comply with our Guidelines.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
node/exts/erc20-bridge/erc20/meta_extension.go (1)

1355-1367: Solid defense-in-depth gate; consider sharing the validator→address derivation.

The early rejection before the DB query and signature verification is correct and the comment block clearly motivates it. One nit: each nonce==0 vote_epoch call now derives an Ethereum address (Keccak256 + pubkey decompress) for every validator inside isActiveValidatorAddress, and on the same handler invocation sumEpochVotingPower (lines ~3297–3325) walks the validator set again and rebuilds the same eth address → power map. For larger validator sets you could compute the map once per handler call (or memoize per block) and reuse it both for this membership check and the BFT power summation.

Not a blocker — just an easy follow-up if this becomes a hot path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@node/exts/erc20-bridge/erc20/meta_extension.go` around lines 1355 - 1367, The
early membership check in the vote_epoch handler redundantly derives validator
Ethereum addresses twice (once inside isActiveValidatorAddress and again inside
sumEpochVotingPower); refactor to compute the validator-address→power map once
per handler invocation (or memoize per block) and pass that map into
isActiveValidatorAddress and sumEpochVotingPower (or change
isActiveValidatorAddress to accept the precomputed map) so both the membership
gate and BFT power summation reuse the same address derivation and avoid
duplicated Keccak/decompress work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@node/exts/erc20-bridge/erc20/meta_extension.go`:
- Around line 1355-1367: The early membership check in the vote_epoch handler
redundantly derives validator Ethereum addresses twice (once inside
isActiveValidatorAddress and again inside sumEpochVotingPower); refactor to
compute the validator-address→power map once per handler invocation (or memoize
per block) and pass that map into isActiveValidatorAddress and
sumEpochVotingPower (or change isActiveValidatorAddress to accept the
precomputed map) so both the membership gate and BFT power summation reuse the
same address derivation and avoid duplicated Keccak/decompress work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 592a3d9b-17b5-4b9c-b89d-eca013893ee1

📥 Commits

Reviewing files that changed from the base of the PR and between 2adc7ff and 37f355a.

📒 Files selected for processing (2)
  • node/exts/erc20-bridge/erc20/meta_extension.go
  • node/exts/erc20-bridge/erc20/validator_membership_test.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
node/exts/erc20-bridge/erc20/meta_extension.go (1)

1355-1373: LGTM — solid defense-in-depth at the action boundary.

Building the validator eth-address map once and reusing it for the membership gate and the BFT power sum is the right shape: cheap-checks-first ordering (length → caller parse → canVoteEpoch → membership → signature verify), and rejecting before voteEpoch keeps non-validator rows out of epoch_votes so withdrawal proofs stay clean.

One tiny note: a validator using a non-secp256k1 key gets the same "not in the active validator set" error here even though they technically are in the set — they just can't participate in non-custodial voting. Optional, but the message could be slightly more diagnostic, e.g.

-								return fmt.Errorf("vote_epoch: caller %s is not in the active validator set", from.Hex())
+								return fmt.Errorf("vote_epoch: caller %s is not an active secp256k1 validator", from.Hex())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@node/exts/erc20-bridge/erc20/meta_extension.go` around lines 1355 - 1373, The
current membership rejection returns a generic "not in the active validator set"
when the eth address lookup via validatorEthMap (built by
buildValidatorEthAddressMap) fails; update the error to be more diagnostic by
detecting whether the caller's node exists in the validator set but lacks a
derived secp256k1 eth address (i.e. non-secp256k1 key) versus truly absent, and
return distinct messages accordingly (e.g., "caller %s is a validator but cannot
derive secp256k1 eth address; cannot participate in non-custodial voteEpoch" vs
the existing "caller %s is not in the active validator set"), using symbols
nonce/nonCustodialNonce, from, validatorEthMap, buildValidatorEthAddressMap and
voteEpoch to locate the check to modify.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@node/exts/erc20-bridge/erc20/meta_extension.go`:
- Around line 3193-3203: getValidatorSigner currently gates starting the
vote_epoch loop at OnStart using isLocalNodeActiveValidator, so a node promoted
after startup never begins signing; modify getValidatorSigner (or the startup
flow that calls it) to periodically re-check membership (e.g., spawn a
background ticker/goroutine that calls isLocalNodeActiveValidator and starts the
bridge signer when it becomes true) and ensure this background check respects
the same action-side gate around epoch_votes/vote_epoch to avoid races, or
alternatively add clear operator documentation to the runbook describing the
restart requirement for late-promoted validators; reference getValidatorSigner,
OnStart, isLocalNodeActiveValidator, vote_epoch and epoch_votes when
implementing or documenting the change.

---

Nitpick comments:
In `@node/exts/erc20-bridge/erc20/meta_extension.go`:
- Around line 1355-1373: The current membership rejection returns a generic "not
in the active validator set" when the eth address lookup via validatorEthMap
(built by buildValidatorEthAddressMap) fails; update the error to be more
diagnostic by detecting whether the caller's node exists in the validator set
but lacks a derived secp256k1 eth address (i.e. non-secp256k1 key) versus truly
absent, and return distinct messages accordingly (e.g., "caller %s is a
validator but cannot derive secp256k1 eth address; cannot participate in
non-custodial voteEpoch" vs the existing "caller %s is not in the active
validator set"), using symbols nonce/nonCustodialNonce, from, validatorEthMap,
buildValidatorEthAddressMap and voteEpoch to locate the check to modify.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 67a50d18-ece4-41f8-a425-1ba8b247d10a

📥 Commits

Reviewing files that changed from the base of the PR and between 37f355a and 825a9af.

📒 Files selected for processing (1)
  • node/exts/erc20-bridge/erc20/meta_extension.go

Comment thread node/exts/erc20-bridge/erc20/meta_extension.go
@MicBun MicBun merged commit 12ed615 into main Apr 27, 2026
4 checks passed
@MicBun MicBun deleted the guardSign branch April 27, 2026 03:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant