Skip to content

Commit 5caa57e

Browse files
Merge pull request #1380 from input-output-hk/fix/lw-10836-round-robin-random-input-selector-now-splits-change-if-required
2 parents 5e41a49 + 0998bc9 commit 5caa57e

File tree

5 files changed

+245
-3
lines changed

5 files changed

+245
-3
lines changed

packages/input-selection/src/RoundRobinRandomImprove/change.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,19 +270,79 @@ export const coalesceChangeBundlesForMinCoinRequirement = (
270270
return sortedBundles.filter((bundle) => bundle.coins > 0n || (bundle.assets?.size || 0) > 0);
271271
};
272272

273+
/**
274+
* Splits change bundles if the token bundle size exceeds the specified limit. Each bundle is checked,
275+
* and if it exceeds the limit, it's split into smaller bundles such that each conforms to the limit.
276+
* It also ensures that each bundle has a minimum coin quantity.
277+
*
278+
* @param changeBundles - The array of change bundles, each containing assets and their quantities.
279+
* @param computeMinimumCoinQuantity - A function to compute the minimum coin quantity required for a transaction output.
280+
* @param tokenBundleSizeExceedsLimit - A function to determine if the token bundle size of a set of assets exceeds a predefined limit.
281+
* @returns The array of adjusted change bundles, conforming to the token bundle size limits and each having the necessary minimum coin quantity.
282+
* @throws Throws an error if the total coin amount is fully depleted and cannot cover the minimum required coin quantity.
283+
*/
284+
const splitChangeIfTokenBundlesSizeExceedsLimit = (
285+
changeBundles: Cardano.Value[],
286+
computeMinimumCoinQuantity: ComputeMinimumCoinQuantity,
287+
tokenBundleSizeExceedsLimit: TokenBundleSizeExceedsLimit
288+
): Cardano.Value[] => {
289+
const result: Cardano.Value[] = [];
290+
291+
for (const bundle of changeBundles) {
292+
const { assets, coins } = bundle;
293+
if (!assets || assets.size === 0 || !tokenBundleSizeExceedsLimit(assets)) {
294+
result.push({ assets, coins });
295+
continue;
296+
}
297+
298+
const newValues = [];
299+
let newValue = { assets: new Map(), coins: 0n };
300+
301+
for (const [assetId, quantity] of assets.entries()) {
302+
newValue.assets.set(assetId, quantity);
303+
304+
if (tokenBundleSizeExceedsLimit(newValue.assets) && newValue.assets.size > 1) {
305+
newValue.assets.delete(assetId);
306+
newValues.push(newValue);
307+
newValue = { assets: new Map([[assetId, quantity]]), coins: 0n };
308+
}
309+
}
310+
311+
newValues.push(newValue);
312+
313+
let totalMinCoin = 0n;
314+
for (const value of newValues) {
315+
const minCoin = computeMinimumCoinQuantity({ address: stubMaxSizeAddress, value });
316+
value.coins = minCoin;
317+
totalMinCoin += minCoin;
318+
}
319+
320+
if (coins < totalMinCoin) {
321+
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
322+
}
323+
324+
newValues[0].coins += coins - totalMinCoin;
325+
result.push(...newValues);
326+
}
327+
328+
return result;
329+
};
330+
273331
const computeChangeBundles = ({
274332
utxoSelection,
275333
outputValues,
276334
uniqueTxAssetIDs,
277335
implicitValue,
278336
computeMinimumCoinQuantity,
337+
tokenBundleSizeExceedsLimit,
279338
fee = 0n
280339
}: {
281340
utxoSelection: UtxoSelection;
282341
outputValues: Cardano.Value[];
283342
uniqueTxAssetIDs: Cardano.AssetId[];
284343
implicitValue: RequiredImplicitValue;
285344
computeMinimumCoinQuantity: ComputeMinimumCoinQuantity;
345+
tokenBundleSizeExceedsLimit: TokenBundleSizeExceedsLimit;
286346
fee?: bigint;
287347
}): (UtxoSelection & { changeBundles: Cardano.Value[] }) | false => {
288348
const requestedAssetChangeBundles = computeRequestedAssetChangeBundles(
@@ -304,7 +364,17 @@ const computeChangeBundles = ({
304364
if (!changeBundles) {
305365
return false;
306366
}
307-
return { changeBundles, ...utxoSelection };
367+
368+
// Make sure the change outputs do not exceed token bundle size limit, this can happen if the UTXO set
369+
// has too many assets and the selection strategy selects enough of them to violates this constraint for the resulting
370+
// change output set.
371+
const adjustedChange = splitChangeIfTokenBundlesSizeExceedsLimit(
372+
changeBundles,
373+
computeMinimumCoinQuantity,
374+
tokenBundleSizeExceedsLimit
375+
);
376+
377+
return { changeBundles: adjustedChange, ...utxoSelection };
308378
};
309379

310380
const validateChangeBundles = (
@@ -365,6 +435,7 @@ export const computeChangeAndAdjustForFee = async ({
365435
computeMinimumCoinQuantity,
366436
implicitValue,
367437
outputValues,
438+
tokenBundleSizeExceedsLimit,
368439
uniqueTxAssetIDs,
369440
utxoSelection
370441
});
@@ -395,6 +466,7 @@ export const computeChangeAndAdjustForFee = async ({
395466
fee: estimatedCosts.fee,
396467
implicitValue,
397468
outputValues,
469+
tokenBundleSizeExceedsLimit,
398470
uniqueTxAssetIDs,
399471
utxoSelection: pick(selectionWithChangeAndFee, ['utxoRemaining', 'utxoSelected'])
400472
});

packages/input-selection/test/InputSelectionPropertyTesting.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,8 @@ const testInputSelection = (name: string, getAlgorithm: () => InputSelector) =>
298298
getAlgorithm,
299299
mockConstraints: {
300300
...SelectionConstraints.MOCK_NO_CONSTRAINTS,
301-
maxTokenBundleSize: 1
301+
maxTokenBundleSize: 1,
302+
minimumCoinQuantity: 1_000_000n
302303
}
303304
});
304305
});

packages/input-selection/test/RoundRobinRandomImprove.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { Cardano } from '@cardano-sdk/core';
22
import { MockChangeAddressResolver, SelectionConstraints } from './util';
33
import { TxTestUtil } from '@cardano-sdk/util-dev';
4+
import {
5+
babbageSelectionParameters,
6+
cborUtxoSetWithManyAssets,
7+
getCoreUtxosFromCbor,
8+
txBuilder,
9+
txEvaluator
10+
} from './vectors';
11+
import { defaultSelectionConstraints } from '../../tx-construction/src/input-selection/selectionConstraints';
412
import { roundRobinRandomImprove } from '../src/RoundRobinRandomImprove';
513

614
describe('RoundRobinRandomImprove', () => {
@@ -78,4 +86,32 @@ describe('RoundRobinRandomImprove', () => {
7886
)
7987
).toBeTruthy();
8088
});
89+
90+
it('splits change outputs if they violate the tokenBundleSizeExceedsLimit constraint', async () => {
91+
const utxoSet = getCoreUtxosFromCbor(cborUtxoSetWithManyAssets);
92+
const maxSpendableAmount = 87_458_893n;
93+
const assetsInUtxoSet = 511;
94+
95+
const constraints = defaultSelectionConstraints({
96+
buildTx: txBuilder,
97+
protocolParameters: babbageSelectionParameters,
98+
redeemersByType: {},
99+
txEvaluator: txEvaluator as never
100+
});
101+
102+
const results = await roundRobinRandomImprove({
103+
changeAddressResolver: new MockChangeAddressResolver()
104+
}).select({
105+
constraints,
106+
outputs: new Set([TxTestUtil.createOutput({ coins: maxSpendableAmount })]),
107+
preSelectedUtxo: new Set(),
108+
utxo: utxoSet
109+
});
110+
111+
expect(results.selection.inputs.size).toBe(utxoSet.size);
112+
expect(results.selection.change.length).toBe(2);
113+
expect(results.selection.change[0].value.assets!.size + results.selection.change[1].value.assets!.size).toBe(
114+
assetsInUtxoSet
115+
);
116+
});
81117
});

packages/input-selection/test/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"compilerOptions": {
44
"baseUrl": "."
55
},
6-
"include": ["./**/*.ts"],
6+
"include": ["./**/*.ts", "../../tx-construction/src/input-selection/selectionConstraints"],
77
"references": [
88
{
99
"path": "../src"

0 commit comments

Comments
 (0)