Skip to content

Conversation

@kuksag
Copy link

@kuksag kuksag commented Jan 14, 2026

Summary by CodeRabbit

  • New Features

    • Added ADR-36 payload support with signing, verification, and integration into multi-payload handling.
    • Enabled extraction/deserialization of application data from ADR-36 messages.
  • Tests

    • Added unit tests covering ADR-36 hashing, successful verification, and negative verification cases.
  • Chores

    • Added a new workspace package and wired it into workspace dependencies.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

📝 Walkthrough

Walkthrough

Adds a new adr36 crate providing ADR-36 payload and signed-payload types, integrates it into the workspace, exposes Adr36 payloads in core, implements extraction and verification paths, and updates MultiPayload to include an Adr36 variant with tests.

Changes

Cohort / File(s) Summary
Workspace config
Cargo.toml, core/Cargo.toml
Added adr36 to workspace members and added defuse-adr36 as a workspace dependency for core.
ADR-36 crate manifest
adr36/Cargo.toml
New crate manifest with package metadata, dependencies (defuse-crypto, near-sdk), dev-deps, and workspace lints.
ADR-36 implementation
adr36/src/lib.rs
New Adr36Payload and SignedAdr36Payload; Amino JSON prehash → SHA-256; Payload/SignedPayload impls; secp256k1 signature serde wiring; unit tests.
Core payload module
core/src/payload/mod.rs
Added pub mod adr36;.
Core payload extraction
core/src/payload/adr36.rs
Implemented ExtractDefusePayload<T> for Adr36Payload and SignedAdr36Payload (deserializes JSON from message).
MultiPayload integration
core/src/payload/multi.rs
Added Adr36(SignedAdr36Payload) variant; updated hash(), verify(), and extract_defuse_payload() match arms to handle Adr36.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Adr36 as adr36::lib
  participant Signer
  participant Core
  participant Secp as Secp256k1

  Client->>Adr36: create Adr36Payload(message, signer)
  Adr36->>Adr36: prehash() (Amino JSON → SHA-256)
  Client->>Signer: request sign(prehash)
  Signer->>Adr36: return signature
  Adr36->>Core: submit SignedAdr36Payload (MultiPayload::Adr36)
  Core->>Secp: verify(signature, payload.hash())
  Secp-->>Core: verification result (pubkey / None)
  Core->>Client: return verification outcome / extracted JSON
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • fusede
  • mitinarseny
  • pityjllk

Poem

🐰 A tiny crate hopped in with glee,
Messages hashed from sea to tree,
Signatures twitched, the pubkey shone,
JSON danced and found its home,
Rabbit smiles: "Checks passed — carry on!" 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding support for ADR-36 signatures across the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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.

❤️ Share

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

Copy link

@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: 2

🤖 Fix all issues with AI agents
In `@adr36/Cargo.toml`:
- Around line 1-19: The Cargo.toml declares an unused dependency
defuse-near-utils; remove the defuse-near-utils workspace entry from the
[dependencies] section (delete the line containing "defuse-near-utils.workspace
= true") so the crate no longer depends on it and then run cargo check to
confirm no missing imports.

In `@adr36/src/lib.rs`:
- Around line 118-119: Rename the test function symbol test_expected_sh256_hash
to the correctly spelled test_expected_sha256_hash; update the #[test] function
declaration for test_expected_sh256_hash to fn test_expected_sha256_hash() and
adjust any references or calls to that test name (if any) so they match the new
identifier to ensure the test runs and naming is consistent.
🧹 Nitpick comments (2)
adr36/src/lib.rs (2)

93-101: Consider extracting shared fix_v_in_signature helper.

This function is noted as copy-paste from erc191/lib.rs. Consider extracting it to a shared location in defuse-crypto to avoid duplication and ensure consistent behavior across payload types.


141-153: Consider strengthening the negative test assertion.

The current test uses assert_ne! which only verifies the returned public key doesn't match REFERENCE_PUBKEY. If the goal is to verify that a wrong signature fails verification entirely, consider also checking that verify() returns None or a different public key.

More explicit negative test
     #[test]
     fn test_reference_signature_verification_fails() {
         let payload = Adr36Payload::new(
             REFERENCE_MESSAGE.to_string(),
             REFERENCE_SECP256K1_SIGNER.to_string(),
         );
         let signature = fix_v_in_signature(WRONG_REFERENCE_SIGNATURE);
 
-        assert_ne!(
-            SignedAdr36Payload { payload, signature }.verify(),
-            Some(REFERENCE_PUBKEY)
-        );
+        let result = SignedAdr36Payload { payload, signature }.verify();
+        // Verify either verification fails entirely (None) or recovers a different key
+        assert!(
+            result.is_none() || result != Some(REFERENCE_PUBKEY),
+            "Expected verification to fail or recover different pubkey"
+        );
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 057dc1e and 3efb3dc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • adr36/Cargo.toml
  • adr36/src/lib.rs
  • core/Cargo.toml
  • core/src/payload/adr36.rs
  • core/src/payload/mod.rs
  • core/src/payload/multi.rs
🧰 Additional context used
🧬 Code graph analysis (2)
core/src/payload/adr36.rs (2)
core/src/payload/mod.rs (2)
  • extract_defuse_payload (43-43)
  • extract_defuse_payload (50-52)
core/src/payload/multi.rs (1)
  • extract_defuse_payload (106-117)
adr36/src/lib.rs (2)
core/src/payload/multi.rs (2)
  • hash (67-78)
  • verify (85-96)
crypto/src/signature.rs (1)
  • sig (159-159)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Check
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Security Audit - report
🔇 Additional comments (15)
Cargo.toml (1)

5-5: LGTM!

The workspace member and path dependency are correctly added following the existing alphabetical ordering and patterns.

Also applies to: 49-49

core/Cargo.toml (1)

10-10: LGTM!

The dependency is correctly added using the workspace pattern, consistent with other payload-related dependencies.

core/src/payload/mod.rs (1)

1-1: LGTM!

Module declaration correctly placed in alphabetical order before erc191.

core/src/payload/multi.rs (3)

5-5: LGTM!

Import correctly added for the new ADR-36 payload type.


76-76: LGTM!

The Adr36 variant is correctly integrated into all three trait implementations:

  • Payload::hash() delegates to the inner payload's hash
  • SignedPayload::verify() maps to PublicKey::Secp256k1 (correct for Cosmos SDK which uses secp256k1)
  • ExtractDefusePayload::extract_defuse_payload() delegates extraction

The pattern is consistent with other payload types.

Also applies to: 94-94, 115-115


54-58: LGTM!

The Adr36 variant is well-documented with references to the ADR-36 specification and Keplr wallet implementation docs. The ADR-36 prehash construction (base64-encoded message within a standard JSON structure), signature verification using secp256k1, and comprehensive test coverage with reference vectors from the cosmos-sdk all confirm the implementation is correct and spec-compliant.

core/src/payload/adr36.rs (2)

5-15: LGTM!

The delegation pattern for SignedAdr36Payload is consistent with how other signed payload types work in the codebase. The #[inline] hint is appropriate for this thin wrapper.


17-26: LGTM!

The JSON parsing from self.message is the correct approach for ADR-36 payloads, where the message field contains the serialized DefusePayload. This aligns with the prehash() implementation in adr36/src/lib.rs that base64-encodes the message.

adr36/src/lib.rs (7)

7-20: LGTM!

Good documentation with links to the ADR-36 specification and Keplr usage docs. The Bech32 address format explanation for the signer field is helpful for maintainability.


22-26: LGTM!

The const fn constructor is appropriate here. Input validation (e.g., Bech32 format) can reasonably be deferred to signature verification time.


54-59: LGTM!

The SHA-256 hash of the prehashed bytes is correct per ADR-36 specification.


61-68: LGTM!

The SignedAdr36Payload structure correctly wraps the payload with a Secp256k1 signature, using the AsCurve serde wrapper for proper serialization.


70-75: LGTM!

Correctly delegates to the inner payload's hash, which is the standard pattern for signed payloads.


77-84: LGTM!

The SignedPayload implementation correctly uses Secp256k1 verification. The integration with MultiPayload in core/src/payload/multi.rs (line 94) properly maps the result to PublicKey::Secp256k1.


28-51: No action required. The JSON key ordering is correct.

The serde_json::json! macro uses BTreeMap by default, which automatically sorts keys in lexicographic order. This is the correct behavior for ADR-36's canonical JSON serialization requirement. The keys in the prehash JSON are already properly ordered: account_number, chain_id, fee, memo, msgs, sequence. No preserve_order feature is needed; the default behavior already produces deterministic, sorted output for correct signature verification.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

pityjllk
pityjllk previously approved these changes Jan 15, 2026
Copy link
Collaborator

@pityjllk pityjllk left a comment

Choose a reason for hiding this comment

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

lgtm

Copy link
Collaborator

@mitinarseny mitinarseny left a comment

Choose a reason for hiding this comment

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

@kuksag thanks for the PR! Overall, looks good to me, see just a minor comment below

{
"type": "sign/MsgSignData",
"value": {
"data": STANDARD.encode(self.message.as_bytes()),
Copy link
Collaborator

Choose a reason for hiding this comment

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

According to docs:

It's applications developers decision how Data should be treated, by treated we mean the serialization and deserialization process and the Object Data should represent.

Since we already represent self.message as String, does it make sense to avoid base64 encoding here and embed message as-is?

Copy link
Author

@kuksag kuksag Jan 20, 2026

Choose a reason for hiding this comment

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

Good catch. I'm gonna say I've overlooked that, because I was referencing implementations of the most used wallets on Cosmos SDK, and they wrap a message into base64:

To comply with the standard and to have something working in practise, we can verify the signature against both b64(message) (as of right now) and plaindata.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, ok, if that's a convention across ecosystem - I'm ok with leaving it in current state, i.e. always encode as base64

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants