Skip to content

Commit daf08fa

Browse files
authored
Updates for Batch v1_1 (#808)
* Initial batch V1_1 implementation
1 parent f96f8f9 commit daf08fa

17 files changed

Lines changed: 1366 additions & 289 deletions

File tree

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/XrplBinaryCodec.java

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -144,29 +144,23 @@ public String encodeForMultiSigning(String json, String xrpAccountId) throws Jso
144144
* JSON and then checks for JsonNode values, this implementation instead accepts a well-typed Java object and operates
145145
* on that, for safety and correctness.
146146
*
147-
* @param batch A {@link Batch} containing JSON to be encoded.
147+
* <p>Per XLS-0056 V1_1, the payload is: {@code HashPrefix::Batch} + outer {@code Account} + sequence + {@code Flags}
148+
* + count + inner tx IDs, followed by the {@code batchSignerAddress} as the per-signer suffix.</p>
148149
*
149-
* @return hex encoded representations
150+
* @param batch A {@link Batch} to be encoded.
151+
* @param batchSignerAddress The {@link Address} of the BatchSigner entry (appended as a per-signer suffix).
150152
*
151-
* @throws JsonProcessingException if JSON is not valid.
153+
* @return An {@link UnsignedByteArray} with the signable bytes.
152154
*/
153-
public UnsignedByteArray encodeForBatchInnerSigning(Batch batch) throws JsonProcessingException {
155+
public UnsignedByteArray encodeForBatchInnerSigning(Batch batch, Address batchSignerAddress) {
154156
Objects.requireNonNull(batch);
157+
Objects.requireNonNull(batchSignerAddress);
155158
try {
156-
// Start with batch prefix (0x42434800 = "BCH\0")
157-
UnsignedByteArray signableBytes = UnsignedByteArray.fromHex(XrplBinaryCodec.BATCH_SIGNATURE_PREFIX);
158-
159-
// Add flags (4 bytes, big-endian)
160-
HashingUtils.addUInt32(signableBytes, (int) batch.flags().getValue());
161-
162-
// Add count of inner transactions (4 bytes, big-endian)
163-
HashingUtils.addUInt32(signableBytes, batch.rawTransactions().size());
159+
UnsignedByteArray signableBytes = buildBatchSigningPayload(batch);
164160

165-
// Add each inner transaction ID (32 bytes each)
166-
for (RawTransactionWrapper wrapper : batch.rawTransactions()) {
167-
final UnsignedByteArray transactionId = computeInnerBatchTransactionId(wrapper);
168-
signableBytes.append(transactionId);
169-
}
161+
// Append BatchSigner's own account ID as per-signer suffix (V1_1 single-sign suffix)
162+
String signerAccountIdHex = new AccountIdType().fromJson(new TextNode(batchSignerAddress.value())).toHex();
163+
signableBytes.append(UnsignedByteArray.fromHex(signerAccountIdHex));
170164

171165
return signableBytes;
172166
} catch (JsonProcessingException e) {
@@ -180,32 +174,70 @@ public UnsignedByteArray encodeForBatchInnerSigning(Batch batch) throws JsonProc
180174

181175
/**
182176
* Encode a {@link Batch} for multi-signing by a specific signer. This is used when a multi-sig account acts as a
183-
* BatchSigner with nested Signers. Per rippled's checkBatchMultiSign, this uses batch serialization (serializeBatch)
184-
* followed by appending the signer's account ID (finishMultiSigningData).
185-
*
186-
* @param batch The {@link Batch} to encode.
187-
* @param signerAddress The address of the signer (will be appended as account ID suffix).
177+
* BatchSigner with nested Signers. Per XLS-0056 V1_1 / rippled's {@code checkBatchMultiSign}, the payload is the base
178+
* batch serialization followed by {@code batchSignerAddress} then {@code nestedSignerAddress} (i.e.
179+
* {@code finishMultiSigningData(batchSignerAddress, nestedSignerAddress)}).
188180
*
189-
* @return An {@link UnsignedByteArray} containing the batch serialization with account ID suffix.
181+
* @param batch The {@link Batch} to encode.
182+
* @param batchSignerAddress The {@link Address} of the BatchSigner entry (outer multi-sig account).
183+
* @param nestedSignerAddress The {@link Address} of the individual signer within the BatchSigner's Signers list.
190184
*
191-
* @throws JsonProcessingException if there is an error processing the JSON.
185+
* @return An {@link UnsignedByteArray} containing the batch serialization with both account ID suffixes.
192186
*/
193-
public UnsignedByteArray encodeForBatchInnerMultiSigning(Batch batch, Address signerAddress)
194-
throws JsonProcessingException {
187+
public UnsignedByteArray encodeForBatchInnerMultiSigning(
188+
final Batch batch, final Address batchSignerAddress, final Address nestedSignerAddress
189+
) {
195190
Objects.requireNonNull(batch);
196-
Objects.requireNonNull(signerAddress);
191+
Objects.requireNonNull(batchSignerAddress);
192+
Objects.requireNonNull(nestedSignerAddress);
193+
try {
194+
UnsignedByteArray result = buildBatchSigningPayload(batch);
197195

198-
// Start with batch serialization (HashPrefix::batch + flags + count + tx IDs)
199-
UnsignedByteArray batchBytes = encodeForBatchInnerSigning(batch);
196+
// Append batchSignerAddress + nestedSignerAddress (finishMultiSigningData per V1_1)
197+
String batchSignerIdHex = new AccountIdType().fromJson(new TextNode(batchSignerAddress.value())).toHex();
198+
result.append(UnsignedByteArray.fromHex(batchSignerIdHex));
200199

201-
// Create a copy to avoid mutating the original (since UnsignedByteArray.append() mutates)
202-
UnsignedByteArray result = UnsignedByteArray.of(batchBytes.toByteArray());
200+
String nestedSignerIdHex = new AccountIdType().fromJson(new TextNode(nestedSignerAddress.value())).toHex();
201+
result.append(UnsignedByteArray.fromHex(nestedSignerIdHex));
203202

204-
// Append the signer's account ID (like finishMultiSigningData does in rippled)
205-
String accountIdHex = new AccountIdType().fromJson(new TextNode(signerAddress.value())).toHex();
206-
result.append(UnsignedByteArray.fromHex(accountIdHex));
203+
return result;
204+
} catch (JsonProcessingException e) {
205+
throw new RuntimeException(e.getMessage(), e);
206+
}
207+
}
208+
209+
/**
210+
* Builds the base batch signing payload (items 1–6 from XLS-0056 V1_1 §2.1.3.2), shared by both single-sign and
211+
* multi-sign paths: {@code HashPrefix::Batch} + outer {@code Account} + sequence + {@code Flags} + count + inner tx
212+
* IDs.
213+
*/
214+
private UnsignedByteArray buildBatchSigningPayload(Batch batch) throws JsonProcessingException {
215+
// Start with batch prefix (0x42434800 = "BCH\0")
216+
UnsignedByteArray signableBytes = UnsignedByteArray.fromHex(XrplBinaryCodec.BATCH_SIGNATURE_PREFIX);
217+
218+
// Add outer account ID (20 bytes)
219+
String accountIdHex = new AccountIdType().fromJson(new TextNode(batch.account().value())).toHex();
220+
signableBytes.append(UnsignedByteArray.fromHex(accountIdHex));
221+
222+
// Add sequence value (4 bytes): TicketSequence if Sequence==0, else Sequence
223+
final int sequenceValue = batch.sequence().longValue() == 0L ?
224+
batch.ticketSequence().map(ts -> ts.intValue()).orElse(0) :
225+
batch.sequence().intValue();
226+
HashingUtils.addUInt32(signableBytes, sequenceValue);
227+
228+
// Add flags (4 bytes, big-endian)
229+
HashingUtils.addUInt32(signableBytes, (int) batch.flags().getValue());
230+
231+
// Add count of inner transactions (4 bytes, big-endian)
232+
HashingUtils.addUInt32(signableBytes, batch.rawTransactions().size());
233+
234+
// Add each inner transaction ID (32 bytes each)
235+
for (RawTransactionWrapper wrapper : batch.rawTransactions()) {
236+
final UnsignedByteArray transactionId = computeInnerBatchTransactionId(wrapper);
237+
signableBytes.append(transactionId);
238+
}
207239

208-
return result;
240+
return signableBytes;
209241
}
210242

211243
/**

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractSignatureService.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.xrpl.xrpl4j.crypto.keys.PublicKey;
2828
import org.xrpl.xrpl4j.model.client.channels.UnsignedClaim;
2929
import org.xrpl.xrpl4j.model.ledger.Attestation;
30+
import org.xrpl.xrpl4j.model.transactions.Address;
3031
import org.xrpl.xrpl4j.model.transactions.Batch;
3132
import org.xrpl.xrpl4j.model.transactions.LoanSet;
3233
import org.xrpl.xrpl4j.model.transactions.Signer;
@@ -110,8 +111,10 @@ public Signature sign(final P privateKeyable, final UnsignedClaim unsignedClaim)
110111
}
111112

112113
@Override
113-
public Signature signInner(final P privateKeyable, final Batch batchTransaction) {
114-
return this.abstractTransactionSigner.signInner(privateKeyable, batchTransaction);
114+
public Signature signInner(
115+
final P privateKeyable, final Batch batchTransaction, final Address batchSignerAddress
116+
) {
117+
return this.abstractTransactionSigner.signInner(privateKeyable, batchTransaction, batchSignerAddress);
115118
}
116119

117120
@Override
@@ -120,8 +123,9 @@ public <T extends Transaction> Signature multiSign(final P privateKeyable, final
120123
}
121124

122125
@Override
123-
public Signature multiSignInner(final P privateKeyable, final Batch batchTransaction) {
124-
return abstractTransactionSigner.multiSignInner(privateKeyable, batchTransaction);
126+
public Signature multiSignInner(final P privateKeyable, final Batch batchTransaction,
127+
final Address batchSignerAddress) {
128+
return abstractTransactionSigner.multiSignInner(privateKeyable, batchTransaction, batchSignerAddress);
125129
}
126130

127131
@Override

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractTransactionSigner.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,14 @@ public Signature sign(P privateKeyable, Attestation attestation) {
8787
}
8888

8989
@Override
90-
public Signature signInner(final P privateKeyable, final Batch batchTransaction) {
90+
public Signature signInner(
91+
final P privateKeyable, final Batch batchTransaction, final Address batchSignerAddress
92+
) {
9193
Objects.requireNonNull(privateKeyable);
9294
Objects.requireNonNull(batchTransaction);
93-
final UnsignedByteArray signableBytes = this.signatureUtils.toSignableInnerBytes(batchTransaction);
95+
Objects.requireNonNull(batchSignerAddress);
96+
final UnsignedByteArray signableBytes = this.signatureUtils.toSignableInnerBytes(batchTransaction,
97+
batchSignerAddress);
9498
return this.signatureHelper(privateKeyable, signableBytes);
9599
}
96100

@@ -105,12 +109,16 @@ public <T extends Transaction> Signature multiSign(final P privateKeyable, final
105109
}
106110

107111
@Override
108-
public Signature multiSignInner(final P privateKeyable, final Batch batchTransaction) {
112+
public Signature multiSignInner(final P privateKeyable, final Batch batchTransaction,
113+
final Address batchSignerAddress) {
109114
Objects.requireNonNull(privateKeyable);
110115
Objects.requireNonNull(batchTransaction);
116+
Objects.requireNonNull(batchSignerAddress);
111117

112-
final Address address = derivePublicKey(privateKeyable).deriveAddress();
113-
final UnsignedByteArray signableBytes = this.signatureUtils.toMultiSignableInnerBytes(batchTransaction, address);
118+
final Address nestedSignerAddress = derivePublicKey(privateKeyable).deriveAddress();
119+
final UnsignedByteArray signableBytes = this.signatureUtils.toMultiSignableInnerBytes(
120+
batchTransaction, batchSignerAddress, nestedSignerAddress
121+
);
114122

115123
return this.signatureHelper(privateKeyable, signableBytes);
116124
}

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtils.java

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -153,21 +153,20 @@ public UnsignedByteArray toMultiSignableBytes(final Transaction transaction, fin
153153

154154

155155
/**
156-
* Convert a {@link Batch} transaction to bytes that can be signed by a Batch inner transaction signer. Per XLS-0056,
157-
* BatchSigners sign the inner transaction in a specific format: HashPrefix::batch + flags + count + inner tx IDs.
156+
* Convert a {@link Batch} transaction to bytes that can be signed by a Batch inner transaction signer (single-sign
157+
* path). Per XLS-0056 V1_1, the payload is: {@code HashPrefix::Batch} + outer {@code Account} + sequence +
158+
* {@code Flags} + count + inner tx IDs + {@code batchSignerAddress}.
158159
*
159-
* @param batch A {@link Batch} transaction.
160+
* @param batch A {@link Batch} transaction.
161+
* @param batchSignerAddress The {@link Address} of the BatchSigner entry signing this batch.
160162
*
161163
* @return An {@link UnsignedByteArray} containing the bytes to be signed.
162164
*/
163165
@Beta
164-
public UnsignedByteArray toSignableInnerBytes(final Batch batch) {
166+
public UnsignedByteArray toSignableInnerBytes(final Batch batch, final Address batchSignerAddress) {
165167
Objects.requireNonNull(batch);
166-
try {
167-
return binaryCodec.encodeForBatchInnerSigning(batch);
168-
} catch (JsonProcessingException e) {
169-
throw new RuntimeException(e.getMessage(), e);
170-
}
168+
Objects.requireNonNull(batchSignerAddress);
169+
return binaryCodec.encodeForBatchInnerSigning(batch, batchSignerAddress);
171170
}
172171

173172
/**
@@ -200,22 +199,22 @@ public UnsignedByteArray toCounterpartyMultiSignableBytes(final LoanSet transact
200199

201200
/**
202201
* Converts a {@link Batch} to multi-signable bytes for a specific signer. This is used when a multi-sig account acts
203-
* as a BatchSigner with nested Signers. Per rippled's checkBatchMultiSign, this uses batch serialization followed by
204-
* appending the signer's account ID.
202+
* as a BatchSigner with nested Signers. Per XLS-0056 V1_1 / rippled's {@code checkBatchMultiSign}, the payload is the
203+
* base batch serialization followed by {@code batchSignerAddress} then {@code nestedSignerAddress}.
205204
*
206-
* @param batch The {@link Batch} to convert.
207-
* @param signerAddress The {@link Address} of the signer.
205+
* @param batch The {@link Batch} to convert.
206+
* @param batchSignerAddress The {@link Address} of the BatchSigner entry (outer multi-sig account).
207+
* @param nestedSignerAddress The {@link Address} of the individual signer within the BatchSigner's Signers list.
208208
*
209-
* @return An {@link UnsignedByteArray} containing the batch serialization with account ID suffix.
209+
* @return An {@link UnsignedByteArray} containing the batch serialization with both account ID suffixes.
210210
*/
211211
@Beta
212-
public UnsignedByteArray toMultiSignableInnerBytes(final Batch batch, final Address signerAddress) {
212+
public UnsignedByteArray toMultiSignableInnerBytes(
213+
final Batch batch, final Address batchSignerAddress, final Address nestedSignerAddress
214+
) {
213215
Objects.requireNonNull(batch);
214-
Objects.requireNonNull(signerAddress);
215-
try {
216-
return binaryCodec.encodeForBatchInnerMultiSigning(batch, signerAddress);
217-
} catch (JsonProcessingException e) {
218-
throw new RuntimeException(e.getMessage(), e);
219-
}
216+
Objects.requireNonNull(batchSignerAddress);
217+
Objects.requireNonNull(nestedSignerAddress);
218+
return binaryCodec.encodeForBatchInnerMultiSigning(batch, batchSignerAddress, nestedSignerAddress);
220219
}
221220
}

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/TransactionSigner.java

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
import org.xrpl.xrpl4j.crypto.keys.PublicKey;
2626
import org.xrpl.xrpl4j.model.client.channels.UnsignedClaim;
2727
import org.xrpl.xrpl4j.model.ledger.Attestation;
28+
import org.xrpl.xrpl4j.model.transactions.Address;
2829
import org.xrpl.xrpl4j.model.transactions.Batch;
30+
import org.xrpl.xrpl4j.model.transactions.BatchSigner;
2931
import org.xrpl.xrpl4j.model.transactions.LoanSet;
3032
import org.xrpl.xrpl4j.model.transactions.Signer;
3133
import org.xrpl.xrpl4j.model.transactions.Transaction;
@@ -86,19 +88,24 @@ public interface TransactionSigner<P extends PrivateKeyable> {
8688
/**
8789
* Get a signature for a batch transaction using the supplied {@link P}.
8890
*
89-
* <p>Per XLS-0056, BatchSigners sign a specific format: HashPrefix::batch + flags + count + inner tx IDs.
90-
* This differs from both single-signing and multi-signing.</p>
91+
* <p>Per XLS-0056 V1_1, the payload is: {@code HashPrefix::Batch} + outer {@code Account} + sequence +
92+
* {@code Flags} + count + inner tx IDs, followed by {@code batchSignerAddress} (the {@link BatchSigner}'s own
93+
* account) as a per-signer suffix. Note that {@code batchSignerAddress} is <b>not</b> necessarily the address derived
94+
* from {@code privateKeyable} — e.g. when a {@link BatchSigner} is authorized via a regular key, the signing key
95+
* derives to the regular key's own address, but {@code batchSignerAddress} must be the underlying account's master
96+
* address.</p>
9197
*
9298
* <p>This method will be marked {@link Beta} until the featureBatch amendment is enabled on mainnet.
9399
* Its API is subject to change.</p>
94100
*
95-
* @param privateKeyable The {@link P} used to sign {@code batchTransaction}.
96-
* @param batchTransaction The {@link Batch} transaction to sign.
101+
* @param privateKeyable The {@link P} used to sign {@code batchTransaction}.
102+
* @param batchTransaction The {@link Batch} transaction to sign.
103+
* @param batchSignerAddress The {@link Address} of the {@link BatchSigner} entry that this signature is for.
97104
*
98105
* @return A {@link Signature} for the batch transaction.
99106
*/
100107
@Beta
101-
Signature signInner(P privateKeyable, Batch batchTransaction);
108+
Signature signInner(P privateKeyable, Batch batchTransaction, Address batchSignerAddress);
102109

103110
/**
104111
* Get a signature for the supplied unsigned transaction using the supplied {@link P}. The primary reason this
@@ -120,25 +127,28 @@ public interface TransactionSigner<P extends PrivateKeyable> {
120127
/**
121128
* Obtain a multi-signature for a batch transaction using the supplied {@link P}.
122129
*
123-
* <p>This is used when a multi-sig account acts as a BatchSigner with nested Signers.
124-
* Per rippled's checkBatchMultiSign, this uses batch serialization (HashPrefix::batch + flags + count + tx IDs)
125-
* followed by appending the signer's account ID.</p>
130+
* <p>This is used when a multi-sig account acts as a BatchSigner with nested Signers. Per XLS-0056 V1_1 /
131+
* rippled's {@code checkBatchMultiSign}, the payload is the base batch serialization followed by
132+
* {@code batchSignerAddress} (the outer multi-sig account) then the address derived from {@code privateKeyable} (the
133+
* individual nested signer).</p>
126134
*
127135
* <p>This method will be marked {@link Beta} until the featureBatch amendment is enabled on mainnet.
128136
* Its API is subject to change.</p>
129137
*
130-
* @param privateKeyable The {@link P} used to sign {@code batchTransaction}.
131-
* @param batchTransaction The {@link Batch} transaction to sign.
138+
* @param privateKeyable The {@link P} used to sign {@code batchTransaction}.
139+
* @param batchTransaction The {@link Batch} transaction to sign.
140+
* @param batchSignerAddress The {@link Address} of the BatchSigner entry (the outer multi-sig account that contains
141+
* the individual signer in its Signers list).
132142
*
133143
* @return A {@link Signature} for the batch transaction with multi-sig format.
134144
*/
135-
Signature multiSignInner(P privateKeyable, Batch batchTransaction);
145+
Signature multiSignInner(P privateKeyable, Batch batchTransaction, Address batchSignerAddress);
136146

137147
/**
138148
* Obtain a counterparty single-signature for the supplied {@link LoanSet} transaction. The counterparty signs the
139-
* same bytes as the first-party signer, but this method returns only the raw
140-
* {@link Signature} rather than a {@link SingleSignedTransaction} wrapper, since the counterparty's signature is
141-
* placed into the {@link org.xrpl.xrpl4j.model.transactions.CounterpartySignature} field, not the transaction's
149+
* same bytes as the first-party signer, but this method returns only the raw {@link Signature} rather than a
150+
* {@link SingleSignedTransaction} wrapper, since the counterparty's signature is placed into the
151+
* {@link org.xrpl.xrpl4j.model.transactions.CounterpartySignature} field, not the transaction's
142152
* {@code TxnSignature}.
143153
*
144154
* <p>This method will be marked {@link Beta} until the LendingProtocol amendment is enabled on mainnet. Its API
@@ -154,9 +164,9 @@ public interface TransactionSigner<P extends PrivateKeyable> {
154164

155165
/**
156166
* Obtain a counterparty multi-signature for the supplied {@link LoanSet} transaction. Unlike
157-
* {@link #multiSign(PrivateKeyable, Transaction)}, this method does <b>not</b> clear the {@code SigningPubKey}
158-
* field, preserving the first-party signer's public key in the signed data. The resulting bytes use the same
159-
* multi-signing prefix ({@code SMT\0}) and the counterparty signer's account ID suffix.
167+
* {@link #multiSign(PrivateKeyable, Transaction)}, this method does <b>not</b> clear the {@code SigningPubKey} field,
168+
* preserving the first-party signer's public key in the signed data. The resulting bytes use the same multi-signing
169+
* prefix ({@code SMT\0}) and the counterparty signer's account ID suffix.
160170
*
161171
* <p>This method will be marked {@link Beta} until the LendingProtocol amendment is enabled on mainnet. Its API
162172
* is subject to change.</p>

0 commit comments

Comments
 (0)