Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions .ci-config/xrpld.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[server]
port_rpc_admin_local
port_ws_public
port_ws_admin_local

# port_peer
Expand All @@ -16,7 +17,7 @@ admin = 0.0.0.0
protocol = http

[port_ws_public]
port = 8080
port = 80
ip = 0.0.0.0
protocol = ws

Expand Down Expand Up @@ -185,13 +186,23 @@ fixInvalidTxFlags
# 2.5.0 Amendments
PermissionedDEX
TokenEscrow
SingleAssetVault
LendingProtocol
PermissionDelegationV1_1
# 3.3.0-rc2 Amendments
MPTokensV1
# 3.1.0 Amendments
SingleAssetVault
DynamicMPT
LendingProtocol
# 3.1.3 Amendments
fixCleanup3_1_3
# 3.2.0 Amendments
fixCleanup3_2_0
# 3.3.0 Amednments
BatchV1_1
ConfidentialTransfer
Sponsor
fixCleanup3_3_0
# 3.4.0 Amendments
LendingProtocolV1_1
fixCleanup3_4_0

# This section can be used to simulate various FeeSettings scenarios for rippled node in standalone mode
[voting]
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Support for the [`LendingProtocol` amendment (XLS-66)](https://github.com/XRPLF/XRPL-Standards/pull/192): the `LoanBrokerSet`, `LoanBrokerDelete`, `LoanBrokerCoverDeposit`, `LoanBrokerCoverWithdraw`, `LoanBrokerCoverClawback`, `LoanSet`, `LoanPay`, `LoanManage` and `LoanDelete` transactions, the `Loan` and `LoanBroker` ledger objects, and `signing::sign_loan_set_by_counterparty` ([#162](https://github.com/XRPLF/xrpl-rust/pull/162)).
- Support for the `LendingProtocolV1_1` amendment ([XLS-0058](https://github.com/XRPLF/XRPL-Standards/pull/582), [XLS-0066 V1_1](https://github.com/XRPLF/XRPL-Standards/pull/587)): close-ended vaults (`VaultKind`, `SubscriptionDate` and `RedemptionDate` on `VaultCreate` and on the `Vault` ledger object), `VaultDelete.MemoData`, and `CredentialIDs` on `VaultWithdraw` and `LoanBrokerCoverWithdraw`.
- Support for the `fixCleanup3_4_0` counterparty signer prefixes: `encode_for_signing_counterparty` (`"CPT\0"`) and `encode_for_multisigning_counterparty` (`"CPM\0"`), used by `signing::sign_loan_set_by_counterparty`. A counterparty signature produced by an earlier version does not validate on a `fixCleanup3_4_0` server.
Comment on lines +18 to +19

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.

The fixCleanup3_4_0 limitation is stated backwards. sign_loan_set_by_counterparty always signs with CPT/CPM (src/signing/mod.rs lines 413 and 433), but rippled keeps expecting STX/SMT for counterparty signatures until fixCleanup3_4_0 is enabled (signingPrefix in src/libxrpl/protocol/Sign.cpp). So on a network that enables LendingProtocol first, every LoanSet signed by this SDK is rejected. Today Mainnet and Testnet have neither amendment enabled. This matches xrpl.js and xrpl-py, so I'm not asking for a legacy-prefix option, but users need to be told.

The current sentence only applies to builds of unreleased main (Lending isn't in 1.3.0), and it leaves out the direction that matters: signatures from this version are only valid where fixCleanup3_4_0 is enabled. The same note belongs in the sign_loan_set_by_counterparty docs (lines 145-159 of src/signing/mod.rs).

Line 18 has a related mix-up: CredentialIDs on VaultWithdraw and LoanBrokerCoverWithdraw isn't gated on LendingProtocolV1_1; rippled requires Credentials plus fixCleanup3_4_0 (VaultWithdraw::checkExtraFeatures, LoanBrokerCoverWithdraw::checkExtraFeatures). Suggested wording for both:

Suggested change
- Support for the `LendingProtocolV1_1` amendment ([XLS-0058](https://github.com/XRPLF/XRPL-Standards/pull/582), [XLS-0066 V1_1](https://github.com/XRPLF/XRPL-Standards/pull/587)): close-ended vaults (`VaultKind`, `SubscriptionDate` and `RedemptionDate` on `VaultCreate` and on the `Vault` ledger object), `VaultDelete.MemoData`, and `CredentialIDs` on `VaultWithdraw` and `LoanBrokerCoverWithdraw`.
- Support for the `fixCleanup3_4_0` counterparty signer prefixes: `encode_for_signing_counterparty` (`"CPT\0"`) and `encode_for_multisigning_counterparty` (`"CPM\0"`), used by `signing::sign_loan_set_by_counterparty`. A counterparty signature produced by an earlier version does not validate on a `fixCleanup3_4_0` server.
- Support for the `LendingProtocolV1_1` amendment ([XLS-0058](https://github.com/XRPLF/XRPL-Standards/pull/582), [XLS-0066 V1_1](https://github.com/XRPLF/XRPL-Standards/pull/587)): close-ended vaults (`VaultKind`, `SubscriptionDate` and `RedemptionDate` on `VaultCreate` and on the `Vault` ledger object) and `VaultDelete.MemoData`.
- Support for the `fixCleanup3_4_0` counterparty signer prefixes: `encode_for_signing_counterparty` (`"CPT\0"`) and `encode_for_multisigning_counterparty` (`"CPM\0"`), used by `signing::sign_loan_set_by_counterparty`. Counterparty signatures produced by this version are only valid on servers where `fixCleanup3_4_0` is enabled.
- `CredentialIDs` on `VaultWithdraw` and `LoanBrokerCoverWithdraw` (requires the `Credentials` and `fixCleanup3_4_0` amendments).

- `signing::combine_loan_set_counterparty_signers`, which merges counterparty signatures produced independently — on separate machines, say — into a single `LoanSet`.
- `signing::CounterpartySigningMode`, the third argument to `sign_loan_set_by_counterparty`: `Single`, `Multisign`, or `MultisignAs(account)` for a wallet signing with that account's RegularKey (parity with xrpl.js's `opts.multisign?: boolean | string`).
- `autofill` now covers a `LoanSet`'s counterparty signatures in the `Fee`, reading the counterparty's signer list (and resolving the counterparty from `LoanBrokerID` when the broker initiates). `Fee` is a signing field and so cannot be raised once the first party has signed, which previously made this the caller's problem.

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.

Same mix-up as get_counterparty_signers_count's doc comment: when the counterparty is resolved from LoanBrokerID, it's the borrower who initiated the LoanSet.

Suggested change
- `autofill` now covers a `LoanSet`'s counterparty signatures in the `Fee`, reading the counterparty's signer list (and resolving the counterparty from `LoanBrokerID` when the broker initiates). `Fee` is a signing field and so cannot be raised once the first party has signed, which previously made this the caller's problem.
- `autofill` now covers a `LoanSet`'s counterparty signatures in the `Fee`, reading the counterparty's signer list (and resolving the counterparty from `LoanBrokerID` when the borrower initiates). `Fee` is a signing field and so cannot be raised once the first party has signed, which previously made this the caller's problem.

- `ledger_entry` selectors for `Loan` and `LoanBroker` (`LoanIdentifier` and `LoanBrokerIdentifier`).
- Support for [XLS-0094D DynamicMPT](https://github.com/XRPLF/XRPL-Standards/pull/583).
- **XLS-0096 Confidential MPT:** support for the [XLS-0096 ConfidentialTransfer amendment](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0096-confidential-mpt). Adds the vendored `mpt-crypto` native crypto library via the internal `mpt-crypto` (safe Rust wrappers) and `mpt-crypto-sys` (FFI bindings, statically linked) crates.
- **`GenericRequest`:** an untyped, catch-all request type for XRPL RPC commands that don't have a dedicated typed model yet (e.g. `ledger_accept` on standalone rippled, `server_state`). Accepts a `command` string plus a free-form `params: serde_json::Map<String, Value>` bag; `Serialize` flattens `params` alongside `command`/`id` and strips any collision with those reserved keys. Slots into `XRPLRequest::Generic` and the existing `Request` trait so it flows through `client.request(...)` unchanged.

### Changed

- `definitions.json` regenerated from `server_definitions` on rippled 3.4.0-rc1: adds the `LendingProtocolV1_1` fields (`VaultKind`, `SubscriptionDate`, `RedemptionDate`, `LEVersion`), the smart-contract and confidential-MPT key-epoch fields, six new transaction results, and the `SponsorshipSet` / `EscrowCreate` format updates; drops the Hooks fields, which rippled no longer supports. No existing field's code, type or signing flag changed.
- **Breaking:** `XRPLSubmitAndWaitException::SubmissionFailed` changed from the tuple variant `SubmissionFailed(String)` to the struct variant `SubmissionFailed { result_code: String, message: Option<String> }`. Callers that pattern-matched on `SubmissionFailed(msg)` must now match `SubmissionFailed { result_code, message }` — the code (`temBAD_SIGNATURE`, `tecUNFUNDED_PAYMENT`, `txnNotFound`, ...) is available without substring-parsing the `Display` string. See [#371](https://github.com/XRPLF/xrpl-rust/issues/371) for the follow-up on typing the code itself.
- Both polling-timeout paths in `wait_for_final_transaction_result` now surface `XRPLSubmitAndWaitException::SubmissionTimeout` (retaining the ledger-sequence context). Previously the retry-cap branch (`c > 20`) returned `SubmissionTimeout` while the after-loop fall-through returned `SubmissionFailed { "submission_timeout" }`; `SubmissionFailed` is now reserved for definite rippled result codes.

Expand Down
7 changes: 7 additions & 0 deletions proptest-regressions/models/transactions/account_delete.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 98d6525f48ecdceefc3011f86b79524f2cff7fdacb0f866cde56124f1724e5c4 # shrinks to count = 1
125 changes: 123 additions & 2 deletions src/asynch/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ use crate::{
},
core::binarycodec::encode,
models::{
requests::{server_state::ServerState, submit::Submit},
results::{server_state::ServerState as ServerStateResult, submit::Submit as SubmitResult},
requests::{
account_info::AccountInfo, ledger_entry::LedgerEntry, server_state::ServerState,
submit::Submit,
},
results::{
ledger_entry::LedgerEntry as LedgerEntryResult,
server_state::ServerState as ServerStateResult, submit::Submit as SubmitResult,
},
transactions::{Transaction, TransactionType},
Model, XRPAmount, XRPLModelException,
},
Expand Down Expand Up @@ -194,6 +200,18 @@ where
};
}
let mut base_fee_decimal: BigDecimal = base_fee.try_into()?;
// A `LoanSet` also pays one reference base fee per counterparty signer
// (rippled `LoanSet::calculateBaseFee`: `normalCost + signerCount * base`).
// `Fee` is a signing field, so it cannot be raised after the first party
// signs — it has to be right here, before the transaction is distributed
// for counterparty signing.
if transaction.get_transaction_type() == &TransactionType::LoanSet {
if let Some(client) = client {

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.

With no client, nothing is added for the counterparty, so the public offline calculate_fee_per_transaction_type (the sync wrapper in src/transaction/mod.rs:114) returns 10 drops for a LoanSet. rippled always charges at least one extra base fee for the counterparty signature outside a Batch (LoanSet::calculateBaseFee), so the minimum is 20, and because both parties sign Fee, a low estimate means signing again. Suggest adding one net_fee for LoanSet when there's no client. autofill isn't affected because it always passes a client.

let counterparty_signers = get_counterparty_signers_count(transaction, client).await?;
let net_fee_decimal: BigDecimal = net_fee.clone().try_into()?;
base_fee_decimal += &(net_fee_decimal * BigDecimal::from(counterparty_signers));
}
}
if let Some(signers_count) = signers_count {
// rippled charges one extra reference base fee per signer on top of the
// transaction's own base cost (Transactor::calculateBaseFee:
Expand All @@ -210,6 +228,109 @@ where
.into())
}

/// The number of counterparty signatures a `LoanSet` will carry, for fee
/// purposes.
///
/// The counterparty is the `Counterparty` account when the transaction names
/// one; otherwise the loan broker initiated it and the counterparty is the

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.

The roles are reversed here. When Counterparty is absent, the borrower submitted the LoanSet and the broker owner is the counterparty (rippled LoanSet.cpp: counterparty = tx[~sfCounterparty].value_or(brokerOwner), borrower = counterparty == brokerOwner ? account : counterparty). The code is correct; only the comment is wrong. CHANGELOG.md line 22 has the same mistake.

Suggested change
/// one; otherwise the loan broker initiated it and the counterparty is the
/// one; otherwise the borrower submitted it and the counterparty is the

/// broker's owner, which is read from the `LoanBroker` ledger entry. An account
/// with a signer list contributes one signature per signer entry; an account
/// without one signs once.
///
/// This is an upper bound, not the exact count. rippled charges on the
/// signatures the transaction actually carries
/// (`max(1, |CounterpartySignature.Signers|)`), but those do not exist yet at
/// autofill time — the counterparty signs later, possibly on another machine —
/// so the signer list is the only thing available to size the fee from. A
/// counterparty that owns a signer list but single-signs therefore overpays by
/// one base fee per unused signer entry; it never underpays, which would cost a
/// `telINSUF_FEE_P` and a re-signing round trip, since `Fee` is a signing field.
/// Callers who know the exact count can set `Fee` themselves before calling
/// `autofill`, which skips this calculation entirely.
///
/// Mirrors `fetchCounterPartySignersCount` in xrpl.js's `autofill`, which sizes
/// the fee the same way.
async fn get_counterparty_signers_count<'a, T, F, C>(
transaction: &T,
client: &C,
) -> XRPLHelperResult<u32>
where
T: Transaction<'a, F>,
F: IntoEnumIterator + Serialize + Debug + PartialEq,
C: XRPLAsyncClient,
{
fn unquote(value: String) -> String {
value.trim_matches('"').to_string()
}

let counterparty = match transaction.get_field_value("Counterparty")? {
Some(counterparty) => unquote(counterparty),
None => {
let loan_broker_id = transaction
.get_field_value("LoanBrokerID")?
.map(unquote)
.ok_or_else(|| {
XRPLModelException::MissingField(
"LoanBrokerID (required to resolve the LoanSet counterparty)".to_string(),
)
})?;
let response = client
.request(
LedgerEntry {
index: Some(loan_broker_id.into()),
..Default::default()
}
.into(),
)
.await?;
let result: LedgerEntryResult = response.try_into()?;
let node = result.node.ok_or_else(|| {
XRPLModelException::MissingField("LoanBroker ledger entry".to_string())
})?;
node["Owner"]
.as_str()
.ok_or_else(|| XRPLModelException::MissingField("LoanBroker.Owner".to_string()))?
.to_string()
}
};

let response = client
.request(
AccountInfo::new(
None,
counterparty.into(),
None,
None,
None,
None,
Some(true),
)
.into(),
)
.await?;
let result = response
.raw_result
.ok_or_else(|| XRPLModelException::MissingField("account_info result".to_string()))?;

// `signer_lists` sits at the root of the response under api_version 2 but
// nested inside `account_data` under api_version 1, which is what rippled
// serves when the request does not ask for a version — so accept either.
let signer_lists = result.get("signer_lists").or_else(|| {
result
.get("account_data")
.and_then(|d| d.get("signer_lists"))
});

// An account without a signer list signs once; otherwise it contributes one
// signature per signer entry.
Ok(signer_lists
.and_then(|lists| lists.get(0))
.and_then(|list| list.get("SignerEntries"))
.and_then(|entries| entries.as_array())
.map(|entries| entries.len() as u32)
.unwrap_or(1))
}

async fn get_owner_reserve_from_response(
client: &impl XRPLAsyncClient,
) -> XRPLHelperResult<XRPAmount<'_>> {
Expand Down
8 changes: 8 additions & 0 deletions src/core/binarycodec/binary_wrappers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,14 @@ impl Iterator for BinaryParser {

pub(crate) const TRANSACTION_SIGNATURE_PREFIX: i32 = 0x53545800;
pub(crate) const TRANSACTION_MULTISIG_PREFIX: [u8; 4] = (0x534D5400u32).to_be_bytes();
/// Inner transaction to sign as the counterparty — "CPT\0" (`fixCleanup3_4_0`).
///
/// Each signature role covers a distinct prefix so a signature cannot be
/// replayed in another role (see rippled `signingPrefix`).
pub(crate) const COUNTERPARTY_TRANSACTION_SIGNATURE_PREFIX: [u8; 4] = (0x43505400u32).to_be_bytes();
/// Inner transaction to multi-sign as the counterparty — "CPM\0"
/// (`fixCleanup3_4_0`).
pub(crate) const COUNTERPARTY_TRANSACTION_MULTISIG_PREFIX: [u8; 4] = (0x43504D00u32).to_be_bytes();
pub(crate) const PAYMENT_CHANNEL_CLAIM_PREFIX: [u8; 4] = (0x434C4D00u32).to_be_bytes();
pub(crate) const BATCH_PREFIX: [u8; 4] = (0x42434800u32).to_be_bytes();

Expand Down
Loading
Loading