Skip to content

fix(validator-config-v2): ed25519 decode check on migration, digest preimage collision fix#2858

Open
howydev wants to merge 8 commits intomainfrom
howy/sigp-audit-fixes
Open

fix(validator-config-v2): ed25519 decode check on migration, digest preimage collision fix#2858
howydev wants to merge 8 commits intomainfrom
howy/sigp-audit-fixes

Conversation

@howydev
Copy link
Contributor

@howydev howydev commented Feb 25, 2026

Summary

Two fixes to ValidatorConfigV2 (TIP-1017):

1. Ed25519 decode check during migration

migrate_validator now validates that V1 public keys are decodable Ed25519 curve points, reverting with InvalidPublicKey if not. V1 did not enforce this, so invalid keys could have been stored.

2. Digest preimage collision fix

The signature digest for addValidator and rotateValidator used abi.encodePacked(..., ingress, egress) which is ambiguous — e.g. ("192.168.0.1:800", "12.3.4.5") and ("192.168.0.1:8001", "2.3.4.5") produce the same byte sequence. Fixed by adding a uint8 length prefix before ingress to disambiguate the boundary.

Files changed

  • Spec: tips/tip-1017.md
  • Solidity: ValidatorConfigV2.sol, IValidatorConfigV2.sol
  • Solidity tests: ValidatorConfigV2.t.sol, invariants/ValidatorConfigV2.t.sol
  • Rust: crates/precompiles/src/validator_config_v2/mod.rs, dispatch.rs
  • Genesis: xtask/src/genesis_args.rs

@howydev howydev force-pushed the howy/sigp-audit-fixes branch from 2ba7db6 to 4c8566a Compare February 25, 2026 17:55
@github-actions
Copy link

github-actions bot commented Feb 25, 2026

📊 Tempo Precompiles Coverage

precompiles

Coverage: 20796/21859 lines (95.14%)

File details
File Lines Coverage
src/account_keychain/dispatch.rs 36/41 87.80%
src/account_keychain/mod.rs 1131/1150 98.35%
src/error.rs 139/158 87.97%
src/ip_validation.rs 10/10 100.00%
src/lib.rs 328/339 96.76%
src/nonce/dispatch.rs 19/23 82.61%
src/nonce/mod.rs 252/260 96.92%
src/stablecoin_dex/dispatch.rs 349/353 98.87%
src/stablecoin_dex/error.rs 51/51 100.00%
src/stablecoin_dex/mod.rs 2997/3093 96.90%
src/stablecoin_dex/order.rs 362/362 100.00%
src/stablecoin_dex/orderbook.rs 651/683 95.31%
src/storage/evm.rs 321/347 92.51%
src/storage/hashmap.rs 128/140 91.43%
src/storage/mod.rs 5/5 100.00%
src/storage/packing.rs 526/552 95.29%
src/storage/thread_local.rs 146/195 74.87%
src/storage/types/array.rs 211/262 80.53%
src/storage/types/bytes_like.rs 323/338 95.56%
src/storage/types/mapping.rs 148/148 100.00%
src/storage/types/mod.rs 67/91 73.63%
src/storage/types/primitives.rs 564/567 99.47%
src/storage/types/set.rs 454/474 95.78%
src/storage/types/slot.rs 282/296 95.27%
src/storage/types/vec.rs 1078/1095 98.45%
src/test_util.rs 194/231 83.98%
src/tip20/dispatch.rs 584/616 94.81%
src/tip20/mod.rs 1783/1854 96.17%
src/tip20/rewards.rs 444/487 91.17%
src/tip20/roles.rs 187/206 90.78%
src/tip20_factory/dispatch.rs 26/29 89.66%
src/tip20_factory/mod.rs 543/555 97.84%
src/tip403_registry/dispatch.rs 406/443 91.65%
src/tip403_registry/mod.rs 1338/1423 94.03%
src/tip_fee_manager/amm.rs 1111/1147 96.86%
src/tip_fee_manager/dispatch.rs 278/289 96.19%
src/tip_fee_manager/mod.rs 495/510 97.06%
src/validator_config/dispatch.rs 210/221 95.02%
src/validator_config/mod.rs 606/692 87.57%
src/validator_config_v2/dispatch.rs 205/218 94.04%
src/validator_config_v2/mod.rs 1808/1905 94.91%

contracts

Coverage: 209/383 lines (54.57%)

File details
File Lines Coverage
src/lib.rs 1/71 1.41%
src/precompiles/account_keychain.rs 24/30 80.00%
src/precompiles/nonce.rs 9/18 50.00%
src/precompiles/stablecoin_dex.rs 36/48 75.00%
src/precompiles/tip20.rs 52/70 74.29%
src/precompiles/tip20_factory.rs 6/12 50.00%
src/precompiles/tip403_registry.rs 12/15 80.00%
src/precompiles/tip_fee_manager.rs 21/45 46.67%
src/precompiles/validator_config.rs 12/26 46.15%
src/precompiles/validator_config_v2.rs 36/48 75.00%

Total: 21005/22242 lines (94.44%)

📦 Download full HTML report

@tempoxyz-bot
Copy link

tempoxyz-bot commented Feb 25, 2026

🐺 Ralph Security Review

4c8566a

Worker Engine Progress Status
pr-2858-w1 gemini-3.1-pro-preview 🚨🚨🚨 Done
pr-2858-w2 amp/deep Done

Findings

# Finding Severity Verification Threads
1 Invalid Ed25519 Public Keys in V1 Permanently Brick V2 Migration High ✅ Verified audit · verify
2 Malicious Validators Can Permanently Block Deactivation by Front-Running via transferValidatorOwnership Medium ✅ Verified audit · verify
3 Malicious V1 Validators Can Permanently Brick V2 Migration via Duplicate Ingress IPs Critical ⏳ Pending audit

❌ Consolidation failed · Thread


📜 24 events

🔍 pr-2858-w1 iter 1/3 [audit-ripple.md]
🔍 pr-2858-w2 iter 1/3 [audit-focused.md]
🚨 pr-2858-w1 iter 1 — finding | Thread
🚨 Finding: Invalid Ed25519 Public Keys in V1 Permanently Brick V2 Migration (High) | Thread
🔍 pr-2858-w1 iter 2/3 [audit-historical.md]
🔬 Verifying: Invalid Ed25519 Public Keys in V1 Permanently Brick V2 Migration | Thread
pr-2858-w2 iter 1 — clear | Thread
🔍 pr-2858-w2 iter 2/3 [audit-ripple.md]
📋 Verify: Invalid Ed25519 Public Keys in V1 Permanently Brick V2 Migration → ✅ Verified | Thread
🚨 pr-2858-w1 iter 2 — finding | Thread
🚨 Finding: Malicious Validators Can Permanently Block Deactivation by Front-Running via transferValidatorOwnership (Medium) | Thread
🔍 pr-2858-w1 iter 3/3 [audit-focused.md]
🔬 Verifying: Malicious Validators Can Permanently Block Deactivation by Front-Running via transferValidatorOwnership | Thread
pr-2858-w2 iter 2 — clear | Thread
🔍 pr-2858-w2 iter 3/3 [audit-historical.md]
pr-2858-w2 iter 3 — clear | Thread
🏁 pr-2858-w2 done
📋 Verify: Malicious Validators Can Permanently Block Deactivation by Front-Running via transferValidatorOwnership → ✅ Verified | Thread
🚨 pr-2858-w1 iter 3 — finding | Thread
🚨 Finding: Malicious V1 Validators Can Permanently Brick V2 Migration via Duplicate Ingress IPs (Critical) | Thread
🏁 pr-2858-w1 done
🔬 Verifying: Malicious V1 Validators Can Permanently Brick V2 Migration via Duplicate Ingress IPs | Thread
📦 Consolidation: running | Thread
📦 Consolidation: failed | Thread

Copy link

@tempoxyz-bot tempoxyz-bot left a comment

Choose a reason for hiding this comment

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

🐺 Ralph Review

The signature digest hardening (adding uint8(ingress.len) / uint8(egress.len) length prefixes) is well-implemented and consistent across Rust, Solidity, and spec. However, the strict migration validation introduces upgrade-liveness risks, and a pre-existing ZELLIC-24 regression remains in the V2 precompile.


🚨 [SECURITY] ZELLIC-24 Regression: Validators Can Block Deactivation via Address Front-Running
File: crates/precompiles/src/validator_config_v2/mod.rs:411 (not in this diff, but in the modified file)

V2's deactivateValidator and rotateValidator use validatorAddress as the lookup key. Since validators can change their address via transferValidatorOwnership, a malicious validator can front-run owner deactivation by transferring to a new address, causing the owner's tx to revert with ValidatorNotFound. This is a regression of ZELLIC-24, which V1 fixed with index-based targeting (change_validator_status_by_index).

Recommended Fix: Reintroduce index-based targeting for owner administrative operations.


⚠️ [ISSUE] V2 Migration Also Bricked by Duplicate Ingress IPs from V1
File: crates/precompiles/src/validator_config_v2/mod.rs:~592 (not in this diff)

Same root cause class as the inline finding below: V1 allows duplicate ingress IPs (same IP, different port) but migrate_validator calls require_unique_ingress_ip which strips the port and rejects duplicates. A malicious V1 validator can set their inboundAddress to share another validator's IP, permanently blocking migration.

Recommended Fix: During migration, quarantine validators with duplicate IPs in a deactivated state rather than reverting.

self.require_new_address(v1_val.validatorAddress)?;
self.require_new_pubkey(v1_val.publicKey)?;

PublicKey::decode(v1_val.publicKey.as_slice())

Choose a reason for hiding this comment

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

🚨 [SECURITY] V2 Migration Permanently Bricked by Invalid V1 Public Key

This PublicKey::decode check will revert if any V1 validator has an invalid Ed25519 public key. Since V1 never validated keys (only checked non-zero), invalid keys can exist in V1 state. Because migrate_validator enforces strict sequential indexing (call.idx == current_count) and initialize_if_migrated requires all V1 validators to be migrated, a single invalid key permanently blocks the entire V2 upgrade. The owner cannot fix the key in V1 (only the validator's ECDSA address can call update_validator) and cannot skip the index.

The test test_migration_rejects_invalid_ed25519_pubkey verifies the revert but does not cover the catastrophic downstream effect: migration is permanently stuck and V2 can never initialize.

Recommended Fix: Instead of reverting, gracefully handle invalid keys during migration — zero out the public key and force the validator into a deactivated state, allowing the migration index to advance.

@howydev howydev requested a review from a team February 25, 2026 21:38
The test needs a non-zero key (so V1 accepts it) that is also not a
valid Ed25519 curve point (so V2 rejects it during migration). The
previous simplification to [0u8;32] was rejected by V1's zero-check,
and [0xFF;32] is actually a valid Ed25519 point. Restore dynamic
search starting from 1.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019c96b6-f605-736c-859a-4fae7636a1e2
@howydev howydev force-pushed the howy/sigp-audit-fixes branch from 197edd4 to e1507e0 Compare February 25, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants