Skip to content

Add binary codec and transaction models for MPT#131

Open
e-desouza wants to merge 17 commits intomainfrom
feat/mpt-binary-codec
Open

Add binary codec and transaction models for MPT#131
e-desouza wants to merge 17 commits intomainfrom
feat/mpt-binary-codec

Conversation

@e-desouza
Copy link
Copy Markdown
Collaborator

High Level Overview of Change

Full MPT (Multi-Purpose Token) support for the binary codec and transaction/ledger models:

  • New Hash192 type in the binary codec (24-byte fixed-length hash for MPTokenIssuanceID)
  • Updated definitions.json with MPT transaction types, ledger entry types, and field definitions
  • Four new transaction models: MPTokenIssuanceCreate, MPTokenIssuanceDestroy, MPTokenIssuanceSet, MPTokenAuthorize
  • Two new ledger objects: MPToken, MPTokenIssuance
  • 40 tests covering field encoding, type codes, transaction serialization, signing prefixes, and serde roundtrips

Closes #119

Context of Change

The XRPL MPT amendment introduces four new transaction types (codes 54-57) and two new ledger entry types (codes 126-127). This required:

  1. A new Hash192 type -- MPTokenIssuanceID is a 24-byte (192-bit) hash, which didn't exist in the codec. Followed the same pattern as Hash128/Hash160/Hash256.

  2. Seven new field definitions in definitions.json: MPTokenIssuanceID (Hash192), ShareMPTID (Hash192), Holder (AccountID), AssetScale (UInt8), MaximumAmount (UInt64), MPTAmount (UInt64), MPTokenMetadata (Blob).

  3. Transaction models following existing patterns in the codebase (CommonFields with #[serde(flatten)], ValidateCurrencies derive macro, builder methods, flag enums with serde_repr).

  4. Validation logic where appropriate: MPTokenIssuanceCreate validates transfer_fee <= 50000, MPTokenIssuanceSet rejects mutually exclusive TfMPTLock + TfMPTUnlock flags.

The UInt192 type name in definitions.json was renamed to Hash192 to match the rippled naming convention (fixed-length hashes use the Hash prefix, not UInt).

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking change that only restructures code)
  • Tests (You added tests for code that already exists, or your new feature included in this PR)
  • Documentation Updates
  • Release

Before / After

Before:

  • Binary codec has no awareness of Hash192 or any MPT fields/types
  • No MPT transaction types in TransactionType enum
  • No MPT ledger entry types
  • Attempting to encode an MPT transaction would fail

After:

  • Hash192 type handles 24-byte hashes correctly (fixed-length, no VL prefix)
  • All four MPT transaction types serialize/deserialize correctly through both JSON (serde) and binary codec (encode/encode_for_signing/encode_for_multisigning)
  • MPToken and MPTokenIssuance ledger objects deserialize from JSON
  • 40 new tests across the transaction models, ledger objects, and binary codec

Test Plan

# Run all tests (40 new MPT-specific tests + existing suite):
cargo test --all-features

# Run only MPT-related tests:
cargo test mptoken --all-features
cargo test mpt --all-features
cargo test hash192 --all-features

# Verify no regressions:
cargo clippy --all-features -- -D warnings
cargo fmt --check

All pass with zero warnings and zero failures. The binary codec tests verify:

  • Field ID encoding/decoding for all 7 MPT fields
  • Transaction type code resolution (54-57)
  • Ledger entry type code resolution (126-127)
  • Field instance metadata (type, nth, serialized, signing, vl_encoded flags)
  • Full encode() for all 4 transaction types with assertion on specific hex patterns
  • encode_for_signing() prefix verification
  • encode_for_multisigning() prefix and suffix verification
  • Deterministic encoding (same input -> same output)

@e-desouza e-desouza force-pushed the feat/mpt-binary-codec branch from baf2e82 to 841449f Compare February 21, 2026 00:01
@e-desouza e-desouza marked this pull request as draft February 21, 2026 00:11
@e-desouza e-desouza force-pushed the feat/mpt-binary-codec branch from 841449f to 7925f94 Compare February 21, 2026 05:20
@e-desouza e-desouza marked this pull request as ready for review February 22, 2026 18:28
@e-desouza e-desouza force-pushed the feat/mpt-binary-codec branch from 7925f94 to d136241 Compare March 25, 2026 21:56
@e-desouza e-desouza requested review from Patel-Raj11 and pdp2121 and removed request for LimpidCrypto March 25, 2026 22:38
@e-desouza e-desouza force-pushed the feat/mpt-binary-codec branch 5 times, most recently from 2792500 to 84f099a Compare April 1, 2026 21:52
@pdp2121
Copy link
Copy Markdown
Collaborator

pdp2121 commented Apr 2, 2026

/ai-review

Copy link
Copy Markdown

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Choose a reason for hiding this comment

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

One correctness gap flagged inline: missing asset_scale range validation.

Review by Claude Opus 4.6 · Prompt: V13

Comment thread src/models/transactions/mptoken_issuance_create.rs
e-desouza

This comment was marked as resolved.

@pdp2121
Copy link
Copy Markdown
Collaborator

pdp2121 commented Apr 8, 2026

@e-desouza can you add integration tests for all transactions?

Comment thread src/models/ledger/objects/mptoken.rs
Comment thread src/models/ledger/objects/mptoken_issuance.rs
Comment thread src/models/ledger/objects/mptoken.rs
@e-desouza
Copy link
Copy Markdown
Collaborator Author

e-desouza commented Apr 10, 2026

Added in 380da30d. There's now an integration test for each of the four MPToken transaction types: create (with a second variant that sets metadata, scale, and fee), authorize (holder opt-in), set (locking via TfMPTLock), and destroy (on an empty issuance).

The authorize/set/destroy tests all need a real issuance to work against, so I added a create_mptoken_issuance() helper in tests/common that creates one and derives the MPTokenIssuanceID from the autofilled sequence + account ID.

Add a 192-bit (24-byte) hash type required by MPT fields such as
MPTokenIssuanceID. Follows the same implementation pattern as
Hash128/Hash160/Hash256 with full trait coverage (Hash, XRPLType,
TryFromParser, TryFrom<&str>, Display, AsRef<[u8]>) and unit tests.
Add MPTokenAuthorize, MPTokenIssuanceCreate, MPTokenIssuanceDestroy,
and MPTokenIssuanceSet to the TransactionType enum for use by MPT
transaction models.
Add the MPTokenIssuanceCreate transaction with fields for asset_scale,
maximum_amount, transfer_fee, and mptoken_metadata. Includes flags
(TfMPTCanLock, TfMPTRequireAuth, TfMPTCanEscrow, TfMPTCanTrade,
TfMPTCanTransfer, TfMPTCanClawback), transfer fee validation, builder
methods, and serde roundtrip tests.

Also adds InvalidFlagCombination variant to XRPLModelException for use
by MPTokenIssuanceSet validation.
Add the MPTokenIssuanceDestroy transaction with mptoken_issuance_id
field. Uses NoFlags since this transaction has no flag-specific
behavior. Includes builder method, serde roundtrip, and validation
tests.
Add the MPTokenIssuanceSet transaction with mptoken_issuance_id and
optional holder fields. Includes TfMPTLock/TfMPTUnlock flags with
validation that prevents setting both simultaneously. Builder methods,
serde roundtrip, and conflict detection tests included.
Add the MPTokenAuthorize transaction with mptoken_issuance_id and
optional holder fields. Includes TfMPTUnauthorize flag for opt-out
flows. Builder methods, serde roundtrip, holder opt-in, and
deauthorize flow tests included.
Add MPToken (account balance) and MPTokenIssuance (issuance definition)
ledger entry types with full serde support, LedgerEntryType enum
variants, and LedgerEntry enum variants. Includes serde roundtrip and
ledger entry type tests for both objects.
Tests cover:
- Field name encoding/decoding for all 7 new MPT fields (Hash192,
  AccountID, UInt8, UInt64, Blob types)
- Transaction type code resolution (codes 54-57)
- Ledger entry type code resolution (codes 126-127)
- Field instance metadata validation
- Full encode() roundtrip for all 4 MPT transaction types with
  hex pattern verification
- encode_for_signing() with signing prefix
- Flag serialization (combined TfMPTCanTransfer | TfMPTCanLock)
- Deterministic encoding output
…ture reference

Post-rebase fixup on feat/mpt-binary-codec:
- Fix #[path = "..."] attributes for external test modules: inline mod test{}
  resolves paths relative to the test/ subdirectory, not binarycodec/
- Remove test_encode_additional_fixtures which referenced
  load_additional_tx_fixtures() that does not exist in test_cases.rs
…uilds

- Change mod test gate from #[cfg(test)] to #[cfg(all(test, feature = "std"))]
  to match main's approach: the external test files and MPT encode tests all
  require serde_json/to_string which is unavailable in no_std builds
- Tighten alloc::boxed::Box import in exceptions.rs to only compile when
  both no_std and websocket are active (Box only used by XRPLWebSocketError)
The asset_scale field is documented as accepting values 0-9, but
rippled rejects values above 9. Without validation, values 10-255
would silently pass model validation and fail at submission time.

Add a _get_asset_scale_error helper (mirroring _get_transfer_fee_error)
and call it from get_errors(). Include tests for boundary values and
the error message format.
…gration tests

Update field-level doc comments on MPToken and MPTokenIssuance ledger
objects to match the canonical descriptions from xrpl.org.

Add integration tests for all four MPToken transaction types:
- MPTokenIssuanceCreate (base + with_metadata)
- MPTokenAuthorize (holder opt-in)
- MPTokenIssuanceSet (lock issuance)
- MPTokenIssuanceDestroy (destroy empty issuance)

Add create_mptoken_issuance() helper in tests/common that creates an
issuance and returns the derived MPTokenIssuanceID for dependent tests.
…e) visibility

The FlagCollection tuple constructor is pub(crate), so integration tests
(which live outside the crate) cannot use it directly. Use Vec::into()
which invokes the public From<Vec<T>> impl instead.
@e-desouza e-desouza force-pushed the feat/mpt-binary-codec branch from 380da30 to 44cc257 Compare April 16, 2026 01:43
The rippleci/rippled:develop image updated after 2026-04-01 and broke
integration tests across all PRs (container exits before becoming healthy,
causing Connection refused on localhost:5005).

Pin to the last known-good digest and replace the simple until loop with
a bounded retry that checks container liveness, prints status per attempt,
and dumps container logs on failure.
Removing hardcoded fee: Some("10") lets sign_and_submit autofill compute
the correct fee for the pinned rippled image (was causing telINSUF_FEE_P).

Add TfMPTCanTransfer to the metadata test so transfer_fee is accepted
(was temMALFORMED). Add TfMPTCanLock in the create_mptoken_issuance
helper so the lock test has permission (was tecNO_PERMISSION).

All 5 MPT integration tests now pass against rippled develop.
- Raise MAX_MPT_ASSET_SCALE from 9 to 19 to match rippled preflight (no
  hard cap; practical ceiling is bounded by maxMPTokenAmount near 2^63).
- Replace NoFlags on the MPTokenIssuance and MPToken ledger objects with
  dedicated flag enums (MPTokenIssuanceFlag, MPTokenFlag) so the ledger
  flag bits returned by rippled are preserved instead of being dropped.
- MPTokenIssuanceSet now requires exactly one of TfMPTLock or TfMPTUnlock
  (DomainID-only modifications are not yet modelled); the previous flipped
  test_default assertion is corrected and split into passing and failing
  cases.
- Validate MPTokenIssuanceID across MPTokenIssuanceSet, MPTokenAuthorize,
  and MPTokenIssuanceDestroy as a 48-char ASCII hex string (24-byte
  Hash192 per XLS-33), and validate the optional holder field as a
  classic XRPL address. Unit-test fixtures updated to the spec-correct
  48-char form; integration fixtures continue to build IDs dynamically.
pdp2121 pushed a commit that referenced this pull request Apr 21, 2026
… #3270) (#291)

## Summary

The `rippled` binary was renamed to `xrpld` upstream, and the
`rippleci/rippled` image stopped receiving updates. Our integration
tests across every open PR started failing because the published
`develop` image exited before becoming healthy (`Connection refused` on
`localhost:5005`, **0 passed / 41 failed**).

This PR mirrors the upstream fix in xrpl.js:
[XRPLF/xrpl.js#3270](XRPLF/xrpl.js#3270).
Switching to `rippleci/xrpld:develop` is the **actual root-cause fix**
rather than pinning an old digest of the deprecated image.

## Changes

`.github/workflows/integration_test.yml`:
- `RIPPLED_DOCKER_IMAGE` -> `XRPLD_DOCKER_IMAGE:
rippleci/xrpld:develop`.
- `docker run` simplified to `${IMAGE} --standalone` (the `xrpld` image
handles `mkdir` + launch internally; no more `bash -c "mkdir -p
/var/lib/rippled/db/ && rippled -a"` wrapper).
- Volume mount changed from `/etc/opt/ripple/` to `/etc/opt/xrpld/`.
- Container name: `rippled-service` -> `xrpld-service`.
- Removed the docker `--health-cmd` (which shelled out to the renamed
`rippled` CLI and always failed) in favour of a direct JSON-RPC poll
against `http://localhost:5005/`.
- Always dump container logs on the stop step for post-mortem
visibility.

`.ci-config/rippled.cfg` -> `.ci-config/xrpld.cfg`:
- `path=/var/lib/rippled/db/nudb` -> `path=/var/lib/xrpld/db/nudb`.
- `[database_path] /var/lib/rippled/db` -> `/var/lib/xrpld/db`.
- `[debug_logfile] /var/log/rippled/debug.log` ->
`/var/log/xrpld/debug.log`.

## Verification

Validated on throwaway PR #292 (now closed): **Integration Test green in
2m53s** on this exact workflow. Unit tests, Build & Lint, Quality Check
also pass.

## Related follow-up

The 7 in-flight PRs (#130, #131, #151, #153, #156, #157, #158) currently
carry a stopgap commit pinning `rippleci/rippled:develop` to a specific
digest. After this PR merges to `main`, those branches should:
1. Rebase on `main` to pick up the xrpld switch, or
2. Cherry-pick this commit and drop the stopgap digest pin.

## Test plan

- [x] Validated end-to-end on PR #292
- [x] Build & Lint, Unit Test, Integration Test, Quality Check all pass
- [ ] Merge and confirm subsequent PRs inherit the fix without manual
cherry-pick

## Credit

Approach lifted from @ckeshava's
[xrpl.js#3270](XRPLF/xrpl.js#3270).
self
}

fn _get_transfer_fee_error(&self) -> XRPLModelResult<()> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should add another validation for TransferFee: "The field must not be present if the tfMPTCanTransfer flag is not set."

#[serde(flatten)]
pub common_fields: CommonFields<'a, MPTokenIssuanceCreateFlag>,
/// The number of decimal places for the MPT value. Must be in the
/// range 0-9. Defaults to 0 if not provided.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: the range in the comment should be 0-19

pub transfer_fee: Option<u16>,
/// Arbitrary hex-encoded metadata for the issuance.
#[serde(rename = "MPTokenMetadata")]
pub mptoken_metadata: Option<Cow<'a, str>>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we implement the same implementation for metadata in light of XLS-89 as this PR:
XRPLF/xrpl.js#3117

index: Some(Cow::from(
"BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769",
)),
ledger_index: None,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's update the test to use realistic values of ledger_index. This MPToken object should not be seen on a real-world XRPL network because ledger_index is an essential field.

LsfMPTCanTransfer = 0x0020,
/// The issuer can clawback MPTs from holders.
LsfMPTCanClawback = 0x0040,
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we also include the following list of Mutable flags in this PR?

Image

I need to make use of the last flag in that list for ConfidentialMPT work.

/// between a standard unit and a corresponding fractional unit. The
/// asset scale is a non-negative integer (0, 1, 2, ...) and defaults
/// to 0.
pub asset_scale: Option<u8>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fields like asset_scale and transfer_fee are declared as "soeDEFAULT". My understanding is that these fields will always have a certain value, even if they are not explicitly specified by the user. Option is not the best way to represent those values, because they will never be None.

Ref: https://github.com/XRPLF/rippled/blob/dbd646bd537835a8f7b0b3b78d079f5e931dcee5/include/xrpl/protocol/detail/ledger_entries.macro#L392

Comment on lines +162 to +184
fn test_minimal_issuance() {
let issuance = MPTokenIssuance {
common_fields: CommonFields {
flags: FlagCollection(vec![]),
ledger_entry_type: LedgerEntryType::MPTokenIssuance,
index: None,
ledger_index: None,
},
issuer: "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B".into(),
asset_scale: None,
maximum_amount: None,
outstanding_amount: "0".into(),
transfer_fee: None,
mptoken_metadata: None,
sequence: 1,
owner_node: None,
previous_txn_id: "0000000000000000000000000000000000000000000000000000000000000000"
.into(),
previous_txn_lgr_seq: 0,
};

assert!(issuance.validate().is_ok());
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this test useful? MPTokenIssuance model does not have any validation-methods. In this implementation, we do not throw any error under any circumstance, why do we have error validation in the unit tests?

However, looking at this test case -- we should validate the index and the ledger_index fields with a non-null sanity check.

Furthermore, we should be grouping the values like index, ledger_index, previous_txn_[seq|lgr_index] into the CommonFields type because these are found on all the modern ledger objects.

if let Some(holder) = self.holder.as_deref() {
validate_holder_address(holder)?;
}
self.validate_currencies()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
self.validate_currencies()

I do not see any amount-related fields in this transaction. We can remove this validation.


/// Validates that a `holder` string decodes as a classic XRPL address.
pub(crate) fn validate_holder_address(holder: &str) -> XRPLModelResult<()> {
if decode_classic_address(holder).is_err() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If users want to use x-addresses instead, this creates a point of friction. Is there a specific reason for only allowing classic addresses?


// AssetScale = 2: field ID 0x0510 + 0x02
assert!(
hex.contains("051002"),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

instead of doing sub-string match, it is more rigorous to test for the exact match of the serialized transaction. Please use the test examples from rippled, xrpl.js and other libraries to test the binary-codec of xrpl-rust.

}

#[test]
fn test_encode_mptoken_issuance_create_with_flags() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This file has tests covering serialization of transactions in an end-to-end fashion. However, the round-trip serialization <-> de-serialization of Hash192 values is missing in this file. Adding that will significantly increase the robustness of the Hash192 binary-codec. This does not have to be for every transaction-field, instead it can be tested for an arbitrary Hash192 type.

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.

Add binary codec support for MPT

4 participants