sdk%feat(codec): add Checkable trait, move inherent validate() functions to it, drop DeploymentContext, trim down checks to be stateless#7
Conversation
📝 WalkthroughWalkthroughThis pull request introduces a new ChangesCheckable validation trait migration
CodeQL declaration slot type refactoring
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
|
Warning This pull request may have conflicts, please coordinate with the authors of these pull requests. Potential conflicts |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
pkgs/primitives/src/transaction.rs (1)
81-149:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject impossible
extra_payloadstates.Line 75 only serializes
extra_payloadwhenhas_extra_payload()is true, butcheck()never rejects payload bytes on spend or pre-v3 transactions. That lets an invalid in-memory transaction pass validation and then lose data onencode().As per coding guidelines, make invalid states unrepresentable.
Suggested fix
pub enum TxInvalid { EmptyInputs, EmptyOutputs, Oversize { size: usize }, PayloadOversize { size: usize }, + UnexpectedExtraPayload { size: usize }, OutputTooLarge { index: usize, value: u64 }, OutputTotalTooLarge { total: u64 }, DuplicateInputs { outpoint: OutPoint }, BadCoinbaseScriptLength { len: usize }, NullPrevout { index: usize }, } fn check(&self) -> Option<Self::Error> { + if !self.has_extra_payload() && !self.extra_payload.is_empty() { + return Some(TxInvalid::UnexpectedExtraPayload { + size: self.extra_payload.len(), + }); + } + let allows_empty_vin = matches!( self.tx_type, TxType::QuorumCommitment | TxType::MnhfSignal | TxType::AssetUnlock );Also applies to: 159-162
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkgs/primitives/src/transaction.rs` around lines 81 - 149, The check() implementation for Transaction must reject non-empty extra_payload when the transaction type/flags indicate no extra payload (i.e., when has_extra_payload() would be false) to prevent silent data loss on encode(); update Transaction::check to return a TxInvalid (e.g., PayloadNotAllowed or reuse a suitable variant) if self.extra_payload is non-empty but self.has_extra_payload() is false, and mirror this validation where encode/serialization logic assumes extra_payload is only present (the other affected area around the serialization branch that checks has_extra_payload()); reference Transaction::check, Transaction::has_extra_payload, the extra_payload field, and the encode/serialization branch so the invalid in-memory state is rejected rather than trimmed during encode.Source: Coding guidelines
pkgs/primitives/src/payload/cbtx.rs (1)
76-87:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate the version-gated option fields, not just
version.Lines 80-86 synthesize zero/default data when v2/v3 fields are missing, but
check()only boundsversion. ACoinbaseCommitmentwithversion = 3andNonein those fields currently passes validation and re-encodes as a different payload.check()should require the fields selected byversion, and reject extra fields on lower versions too.As per coding guidelines, make invalid states unrepresentable.
Also applies to: 106-116
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkgs/primitives/src/payload/cbtx.rs` around lines 76 - 87, The version-gated optional fields (merkle_root_quorums, best_cl_height_diff, best_cl_signature, credit_pool_balance) must be validated in check() according to self.version and rejected if missing for higher versions or present for lower versions; update the CoinbaseCommitment::check() to: require merkle_root_quorums.is_some() when version >= 2 and ensure it is None when version < 2; require best_cl_height_diff, best_cl_signature, and credit_pool_balance to be Some when version >= 3 and ensure they are None when version < 3. After that, remove the encode-side syntheses (the unwrap_or_default/unwrap_or(0) usages in encode) and encode the fields directly (or unwrap safely) because check() now guarantees presence/absence; apply the same validation logic to the alternate block noted at lines 106-116 as well.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pkgs/primitives/src/gov.rs`:
- Around line 416-421: The validation currently lowercases each byte in
self.name (via .to_ascii_lowercase()) before checking membership against
PROPOSAL_NAME_CHARS, making the check case-insensitive; change the check in the
validation routine (the method implementing check() that returns
ProposalInvalid::NameInvalidChars) to test the raw bytes against
PROPOSAL_NAME_CHARS (remove the .to_ascii_lowercase() call) OR alternatively
enforce normalization at write-time (normalize the stored name to lowercase when
setting the name) so that check() validates against the documented
lowercase-only contract.
In `@pkgs/primitives/src/payload/proregtx.rs`:
- Around line 162-218: The check() implementation for ProRegTx must forbid
states that encode() would normalize: ensure version vs net_info are consistent
(require NetInfo::Extended when version >= PROTX_VERSION_EXT_ADDR and require
NetInfo::Legacy when version < PROTX_VERSION_EXT_ADDR), and validate platform
fields per mn_type (when MnType::Regular the platform_* fields must be
None/empty and when MnType::Evo the platform_* fields must be
present/non-empty), also keep the existing Evo/version guard around
PROTX_VERSION_BASIC_BLS; update ProRegTx::check() to return ProTxInvalid
variants for these mismatches so a successful check guarantees encode() will not
alter or drop fields (refer to check(), encode(), NetInfo::Extended/Legacy,
PROTX_VERSION_EXT_ADDR, PROTX_VERSION_BASIC_BLS, and MnType::Regular/Evo and the
platform_* fields).
In `@pkgs/primitives/src/payload/proupservtx.rs`:
- Around line 129-162: The check() implementation for ProUpServTx must enforce
the same wire-shape invariants that encode() assumes: update ProUpServTx::check
to validate (1) version vs NetInfo: require NetInfo::Extended when version >=
PROTX_VERSION_EXT_ADDR and require NetInfo::Legacy when version <
PROTX_VERSION_EXT_ADDR (replace the current is_extended != (version == ...)
logic with explicit range checks to match encode()), (2) platform fields: if
platform type is Regular or Full ensure platform_p2p/platform_http/any
platform-specific Option payloads match what encode() would serialize (i.e.,
reject Regular with Some(platform fields) if encode would drop them), (3)
Evo/version-dependent fields: for mn_type == MnType::Evo enforce version >=
PROTX_VERSION_BASIC_BLS and that Evo-only fields (operator_public,
voting_public, payout scripts, and any platform proto fields expected by encode)
are present/non-default where encode would expect them, and (4) legacy addr/port
presence when NetInfo::Legacy is required; return appropriate ProTxInvalid
variants on mismatch. Use the symbols ProUpServTx::check, encode(),
NetInfo::Extended, NetInfo::Legacy, PROTX_VERSION_EXT_ADDR,
PROTX_VERSION_BASIC_BLS, and mn_type to locate and implement these checks.
- Around line 132-152: The check() implementation in proupservtx.rs currently
allows MnType::Unknown(_) to pass through, bypassing BadMnType handling and
Evo/Regular-specific validation in check_sptx_netinfo; update check() to
explicitly reject MnType::Unknown(_) (return Some(ProTxInvalid::BadMnType {
mn_type: self.mn_type.clone() } or equivalent) early—preferably right after the
version checks and before the Evo-specific branch and net_info
validation—mirroring the behavior in ProRegTx::check() so unknown mn_type values
cannot proceed into check_sptx_netinfo or version logic.
In `@pkgs/primitives/src/payload/quorum.rs`:
- Around line 137-143: FinalCommitment is marked infallible but can contain a
Commitment where version/quorum_index are inconsistent because
Commitment::encode() writes quorum_index when Some(_) while Commitment::decode()
only reads it for versions 2/4; fix by adding validation of the
version/quorum_index invariant inside FinalCommitment::check (make type Error
non-Infallible and return an error when version==1 && quorum_index.is_some() or
when version ∉ {1,2,4} with quorum_index mismatches), or alternatively enforce
the invariant inside Commitment by preventing encode()/construction of
commitments with invalid version/quorum_index pairs so they never appear in
FinalCommitment; also update SpecialPayload::check() to stop assuming quorum
commitments cannot fail if you choose the fallible FinalCommitment path.
---
Outside diff comments:
In `@pkgs/primitives/src/payload/cbtx.rs`:
- Around line 76-87: The version-gated optional fields (merkle_root_quorums,
best_cl_height_diff, best_cl_signature, credit_pool_balance) must be validated
in check() according to self.version and rejected if missing for higher versions
or present for lower versions; update the CoinbaseCommitment::check() to:
require merkle_root_quorums.is_some() when version >= 2 and ensure it is None
when version < 2; require best_cl_height_diff, best_cl_signature, and
credit_pool_balance to be Some when version >= 3 and ensure they are None when
version < 3. After that, remove the encode-side syntheses (the
unwrap_or_default/unwrap_or(0) usages in encode) and encode the fields directly
(or unwrap safely) because check() now guarantees presence/absence; apply the
same validation logic to the alternate block noted at lines 106-116 as well.
In `@pkgs/primitives/src/transaction.rs`:
- Around line 81-149: The check() implementation for Transaction must reject
non-empty extra_payload when the transaction type/flags indicate no extra
payload (i.e., when has_extra_payload() would be false) to prevent silent data
loss on encode(); update Transaction::check to return a TxInvalid (e.g.,
PayloadNotAllowed or reuse a suitable variant) if self.extra_payload is
non-empty but self.has_extra_payload() is false, and mirror this validation
where encode/serialization logic assumes extra_payload is only present (the
other affected area around the serialization branch that checks
has_extra_payload()); reference Transaction::check,
Transaction::has_extra_payload, the extra_payload field, and the
encode/serialization branch so the invalid in-memory state is rejected rather
than trimmed during encode.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ab540649-5982-4069-a11c-862a5b1b893b
📒 Files selected for processing (32)
contrib/codeql/decl.qlcontrib/codeql/lib/policy.qllpkgs/primitives/src/block.rspkgs/primitives/src/gov.rspkgs/primitives/src/lib.rspkgs/primitives/src/payload/assetlock.rspkgs/primitives/src/payload/assetunlock.rspkgs/primitives/src/payload/cbtx.rspkgs/primitives/src/payload/mnhftx.rspkgs/primitives/src/payload/mod.rspkgs/primitives/src/payload/proregtx.rspkgs/primitives/src/payload/proupregtx.rspkgs/primitives/src/payload/prouprevtx.rspkgs/primitives/src/payload/proupservtx.rspkgs/primitives/src/payload/quorum.rspkgs/primitives/src/transaction.rspkgs/primitives/src/validation.rspkgs/primitives/tests/assetlock.rspkgs/primitives/tests/assetunlock.rspkgs/primitives/tests/blocks.rspkgs/primitives/tests/cbtx.rspkgs/primitives/tests/coinbase.rspkgs/primitives/tests/data.rspkgs/primitives/tests/mnhftx.rspkgs/primitives/tests/proposals.rspkgs/primitives/tests/proregtx.rspkgs/primitives/tests/proupregtx.rspkgs/primitives/tests/prouprevtx.rspkgs/primitives/tests/proupservtx.rspkgs/primitives/tests/qctx.rspkgs/primitives/tests/spend.rspkgs/types/src/codec.rs
💤 Files with no reviewable changes (1)
- pkgs/primitives/src/lib.rs
| if !self | ||
| .name | ||
| .bytes() | ||
| .all(|b| PROPOSAL_NAME_CHARS.contains(&b.to_ascii_lowercase())) | ||
| { | ||
| return Err(ProposalInvalid::NameInvalidChars); | ||
| return Some(ProposalInvalid::NameInvalidChars); |
There was a problem hiding this comment.
The proposal-name validator is accidentally case-insensitive.
Line 419 lowercases each byte before the membership test, so MyProposal passes even though Line 87 documents lowercase-only names and PROPOSAL_NAME_CHARS only contains lowercase bytes. Validate the raw bytes instead, or normalize the stored name before validation so check() matches the documented contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pkgs/primitives/src/gov.rs` around lines 416 - 421, The validation currently
lowercases each byte in self.name (via .to_ascii_lowercase()) before checking
membership against PROPOSAL_NAME_CHARS, making the check case-insensitive;
change the check in the validation routine (the method implementing check() that
returns ProposalInvalid::NameInvalidChars) to test the raw bytes against
PROPOSAL_NAME_CHARS (remove the .to_ascii_lowercase() call) OR alternatively
enforce normalization at write-time (normalize the stored name to lowercase when
setting the name) so that check() validates against the documented
lowercase-only contract.
Additional Information
Dependency for sdk%refac(test): move corpus-based tests to
serdedirect deser, adddash-devtest utility module, inline tests into parent modules #8DeploymentContextassumes knowledge of the chain's state. Base SDK is a stateless library and thus, this falls outside our scope and has been dropped. This has the effect of vastly simplifying the trivial validation routines (referred to as "checks") that are meant to be fast and inexpensive.Breaking Changes
DeploymentContexthas been removed. Allvalidate()methods that accepted&DeploymentContextno longer exist.All
validate()inherent methods have been replaced byimpl Checkablewithfn check(&self) -> Option<Self::Error>. Callers should migrate from.validate().is_ok()to.check().is_none().How Has This Been Tested?
Checklist