Skip to content

feat(utxo): add RXD ForkIdRxd sighash signing path#2713

Merged
shamardy merged 9 commits intoGLEECBTC:devfrom
cdonnachie:feat/rxd-forkid-sighash
Feb 23, 2026
Merged

feat(utxo): add RXD ForkIdRxd sighash signing path#2713
shamardy merged 9 commits intoGLEECBTC:devfrom
cdonnachie:feat/rxd-forkid-sighash

Conversation

@cdonnachie
Copy link

@cdonnachie cdonnachie commented Feb 15, 2026

Summary

Adds Radiant (RXD) sighash support by introducing a ForkIdRxd variant to SignatureVersion, keeping the signing dispatch clean and self-contained in mm2_bitcoin/script.

What changed

  • Added SignatureVersion::ForkIdRxd variant (serde: "fork_id_rxd") to route RXD signing through its own preimage path.
  • Implemented signature_hash_fork_id_rxd on TransactionInputSigner — follows the BIP143-style ForkId preimage but inserts an additional hashOutputHashes commitment before hashOutputs.
  • Added compute_hash_output_hashes_rxd which commits per-output summaries: value, double-SHA256 of scriptPubKey, push-ref count, and double-SHA256 of sorted push-refs.
  • Added sorted_push_refs_rxd script parser that extracts and deduplicates OP_PUSHINPUTREF / OP_PUSHINPUTREFSINGLETON 36-byte references from output scripts, with checked_add for wasm32 safety.
  • Extended Sighash::is_defined and Sighash::from_u32 to recognize ForkIdRxd alongside ForkId.
  • Minor clippy doctest fix in enum_derives (cherry-picked).

Coin configuration

RXD coins specify the signing path via config — no protocol-level inference needed:

{
    "fork_id": "0x40",
    "signature_version": "fork_id_rxd"
}

Why

RXD consensus requires a distinct sighash preimage that includes output-hash summaries with push-ref commitments. Routing via SignatureVersion (where all signing dispatch already happens) avoids adding chain_variant to TransactionInputSigner and the propagation issues that would cause (e.g. From<Transaction> hardcoding Standard).

Tests

  • test_rxd_forkid_sighash_vector: Verifies sighash against a real on-chain RXD transaction with signature verification.
  • test_sorted_push_refs_rxd_extracts_and_sorts_push_refs: Tests push-ref opcode parsing, deduplication, and sorting with crafted scripts containing OP_PUSHINPUTREF, OP_REQUIREINPUTREF, and OP_PUSHINPUTREFSINGLETON.
  • Extended existing test_sighash_forkid_is_defined with ForkIdRxd assertions.

Validation

  • Successful atomic swap (RXD/KMD) completed with the ForkIdRxd signing path.
  • Tested by @cipig with confirmed working swaps.
  • cargo fmt and build checks passed.

Files changed

  • mm2src/mm2_bitcoin/script/src/sign.rs — All RXD sighash logic and tests
  • mm2src/derives/enum_derives/src/lib.rs — Clippy doctest fix

@shamardy
Copy link
Collaborator

can you please rebase to latest dev branch?

- add ChainVariant::RXD and coin-scoped routing for RXD signing
- implement RXD hashOutputHashes computation and insert before hashOutputs in ForkId preimage
- enforce RXD fork_id default (0x40) when chain_variant=RXD
- keep BTC/BCH signing behavior unchanged
- Revert accidental SHA256-only change in RXD output-hash summarization
- Use DSHA256 for scriptPubKey hash, sortedPushRefs hash, and hashOutputHashes aggregate
@cdonnachie cdonnachie force-pushed the feat/rxd-forkid-sighash branch from 350f207 to 779faf8 Compare February 16, 2026 14:23
@cdonnachie cdonnachie changed the base branch from main to dev February 16, 2026 14:26
@cdonnachie
Copy link
Author

can you please rebase to latest dev branch?

done

@shamardy shamardy self-requested a review February 17, 2026 09:00
@shamardy
Copy link
Collaborator

Thanks for the PR, below is my first review to make sure we are on the right track before going deeper.

I think the approach of adding chain_variant to TransactionInputSigner is not the right fit here. ChainVariant is used for block header deserialization routing, not signing. All the existing helpers (is_btc(), is_qtum(), is_rvn(), is_pivx()) branch header parsing logic in the Reader. Adding it to the signer struct mixes two different concerns.

It also creates propagation issues. The From<Transaction> impl hardcodes ChainVariant::Standard, so any code path that does let signer: TransactionInputSigner = tx.into() (like sign_raw_utxo_tx or check_all_utxo_inputs_signed_by_pub) will silently use the wrong sighash for RXD.

I think a cleaner approach would be to add a ForkIdRxd variant to SignatureVersion instead. The signing algorithm is already dispatched via SignatureVersion, so this would be the natural place for it. Something like:

match sigversion {
    SignatureVersion::ForkId if sighash.fork_id => {
        self.signature_hash_fork_id(...)
    },
    SignatureVersion::ForkIdRxd if sighash.fork_id => {
        self.signature_hash_fork_id_rxd(...)
    },
    // ...
}

The mapping would happen once in utxo_conf_builder.rs::signature_version() where RXD maps to ForkIdRxd. This way there's no new field on TransactionInputSigner, no changes needed in utxo.rs or utxo_common.rs, and no propagation gaps. It would shrink the PR from 5 files to basically 2.

One more thing, it would be good to add at least one RXD sighash test vector from a known transaction to verify correctness end-to-end. The Radiant node repo has src/test/sighash_tests.cpp that could be a source for this.

@cdonnachie
Copy link
Author

Thanks, agreed. I’ll switch dispatch to SignatureVersion::ForkIdRxd and remove chain_variant from TransactionInputSigner to eliminate propagation gaps. I’ll also add an RXD on-chain sighash test vector.

@cdonnachie
Copy link
Author

Hi @shamardy ,

  • Implemented reviewer request to route RXD signing via SignatureVersion::ForkIdRxd instead of passing chain_variant through TransactionInputSigner.
  • RXD default mapping now comes from config (protocol.chain_variant == "RXD" -> signature_version = fork_id_rxd, fork_id = 0x40).
  • Removed chain_variant from signer construction paths in UTXO preimage builders.
  • Added a real RXD on-chain sighash vector test in sign.rs.
  • Validated locally with focused test pass (test_rxd_forkid_sighash_vector) and build checks.

If you’d like this structured differently, I can adjust. I will be out of country on vacation for 5 days starting Thursday, so my response time may be delayed.

@cdonnachie
Copy link
Author

cdonnachie commented Feb 17, 2026

Just for reference, I have run this through a successful swap with the latest change (reduced json):

{
  "type": "Taker",
  "uuid": "71997f0d-9ddb-4587-a558-66fc23c6ec10",
  "pair": "RXD/KMD",
  "amounts": {
    "taker_amount": "10 RXD",
    "maker_amount": "0.04 KMD"
  },
  "sighash_type": "0x41 (ALL|FORKID)",
  "htlc": {
    "secret_hash": "c5e61f8d5102af8e88e4634d3e8f3d4f3cf06cbc",
    "secret_revealed": "e998d47a14fb649037f61a7dda3b67b15dd09da02ca4598f703f1f7f6eae0164"
  },
  "transactions": {
    "taker_fee_tx": "3684bbf882f271ff716110bcfce12123057d41da72892e8961683d297a3f1a09",
    "maker_payment_tx": "df4f861a07f9c85c3316199cf2164624a7bec30a08b9561dd513e84d2c723789",
    "taker_payment_tx": "fb77ee357a4fa2d657f4dbc2d48c132676f98c3ca585aae8986f42eab123278c",
    "taker_payment_spent_tx": "73300377eeb6fb73ff7732b0d560f232b062b004560774f52b17e7e366f4874b",
    "maker_payment_spent_tx": "8a3da3efdc5037a8491251832cf6b9763f75cce49c474734f863e00595df1463"
  },
  "status": {
    "finished": true,
    "success": true
  },
  "mm_version": "2.6.0-beta_779faf886"
}

coins:

 {
    "coin": "RXD",
    "name": "rxd",
    "fname": "Radiant",
    "rpcport": 7332,
    "pubtype": 0,
    "p2shtype": 5,
    "wiftype": 128,
    "txfee": 1000000,
    "segwit": false,
    "fork_id": "0x40",
    "signature_version": "fork_id_rxd",
    "mm2": 1,
    "sign_message_prefix": "Bitcoin Signed Message:\n",
    "required_confirmations": 2,
    "avg_blocktime": 300,
    "protocol": { "type": "UTXO", "chain_variant": "RXD" },
    "derivation_path": "m/44'/512'",
}

Copy link
Collaborator

@shamardy shamardy left a comment

Choose a reason for hiding this comment

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

Thanks for the fixes. I have more comments below, since you mentioned you'll be on vacation, would it be ok if I continue this PR, I can push fixes to your branch directly.

@shamardy
Copy link
Collaborator

There are some nits / small changes that I will fix and push.

shamardy and others added 2 commits February 18, 2026 15:17
Prevent integer overflow on 32-bit targets (wasm32) where
OP_PUSHDATA2/4 declared lengths could wrap pos + push_len, bypassing
bounds checks. Also revert stray whitespace in reader.rs and
cherry-pick clippy doctest fix from tron branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace wildcard with explicit SighashBase variants in
  compute_hash_output_hashes_rxd for exhaustive matching
- Move stream inside match arms to match existing compute_hash_outputs
  pattern
- Use matches! macro for ref-opcode filtering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cdonnachie
Copy link
Author

There are some nits / small changes that I will fix and push.

Thanks for doing this, much appreciated, my rust coding is rusty :)

Copy link

@cipig cipig left a comment

Choose a reason for hiding this comment

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

Image

works fine with

    "fork_id": "0x40",
    "signature_version": "fork_id_rxd",

@github-actions github-actions bot added the [] label Feb 21, 2026
@shamardy shamardy changed the title feat(rxd): add ForkId sighash path and RXD chain_variant wiring feat(rxd): add ForkId sighash path Feb 23, 2026
@shamardy shamardy changed the title feat(rxd): add ForkId sighash path feat(utxo): add RXD ForkIdRxd sighash signing path Feb 23, 2026
@shamardy shamardy merged commit 02a8a3c into GLEECBTC:dev Feb 23, 2026
31 of 38 checks passed
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.

3 participants