Skip to content

Commit

Permalink
Merge pull request #1380 from input-output-hk/fix/lw-10836-round-robi…
Browse files Browse the repository at this point in the history
…n-random-input-selector-now-splits-change-if-required
  • Loading branch information
AngelCastilloB authored Jul 22, 2024
2 parents 5e41a49 + 0998bc9 commit 5caa57e
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 3 deletions.
74 changes: 73 additions & 1 deletion packages/input-selection/src/RoundRobinRandomImprove/change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,19 +270,79 @@ export const coalesceChangeBundlesForMinCoinRequirement = (
return sortedBundles.filter((bundle) => bundle.coins > 0n || (bundle.assets?.size || 0) > 0);
};

/**
* Splits change bundles if the token bundle size exceeds the specified limit. Each bundle is checked,
* and if it exceeds the limit, it's split into smaller bundles such that each conforms to the limit.
* It also ensures that each bundle has a minimum coin quantity.
*
* @param changeBundles - The array of change bundles, each containing assets and their quantities.
* @param computeMinimumCoinQuantity - A function to compute the minimum coin quantity required for a transaction output.
* @param tokenBundleSizeExceedsLimit - A function to determine if the token bundle size of a set of assets exceeds a predefined limit.
* @returns The array of adjusted change bundles, conforming to the token bundle size limits and each having the necessary minimum coin quantity.
* @throws Throws an error if the total coin amount is fully depleted and cannot cover the minimum required coin quantity.
*/
const splitChangeIfTokenBundlesSizeExceedsLimit = (
changeBundles: Cardano.Value[],
computeMinimumCoinQuantity: ComputeMinimumCoinQuantity,
tokenBundleSizeExceedsLimit: TokenBundleSizeExceedsLimit
): Cardano.Value[] => {
const result: Cardano.Value[] = [];

for (const bundle of changeBundles) {
const { assets, coins } = bundle;
if (!assets || assets.size === 0 || !tokenBundleSizeExceedsLimit(assets)) {
result.push({ assets, coins });
continue;
}

const newValues = [];
let newValue = { assets: new Map(), coins: 0n };

for (const [assetId, quantity] of assets.entries()) {
newValue.assets.set(assetId, quantity);

if (tokenBundleSizeExceedsLimit(newValue.assets) && newValue.assets.size > 1) {
newValue.assets.delete(assetId);
newValues.push(newValue);
newValue = { assets: new Map([[assetId, quantity]]), coins: 0n };
}
}

newValues.push(newValue);

let totalMinCoin = 0n;
for (const value of newValues) {
const minCoin = computeMinimumCoinQuantity({ address: stubMaxSizeAddress, value });
value.coins = minCoin;
totalMinCoin += minCoin;
}

if (coins < totalMinCoin) {
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
}

newValues[0].coins += coins - totalMinCoin;
result.push(...newValues);
}

return result;
};

const computeChangeBundles = ({
utxoSelection,
outputValues,
uniqueTxAssetIDs,
implicitValue,
computeMinimumCoinQuantity,
tokenBundleSizeExceedsLimit,
fee = 0n
}: {
utxoSelection: UtxoSelection;
outputValues: Cardano.Value[];
uniqueTxAssetIDs: Cardano.AssetId[];
implicitValue: RequiredImplicitValue;
computeMinimumCoinQuantity: ComputeMinimumCoinQuantity;
tokenBundleSizeExceedsLimit: TokenBundleSizeExceedsLimit;
fee?: bigint;
}): (UtxoSelection & { changeBundles: Cardano.Value[] }) | false => {
const requestedAssetChangeBundles = computeRequestedAssetChangeBundles(
Expand All @@ -304,7 +364,17 @@ const computeChangeBundles = ({
if (!changeBundles) {
return false;
}
return { changeBundles, ...utxoSelection };

// Make sure the change outputs do not exceed token bundle size limit, this can happen if the UTXO set
// has too many assets and the selection strategy selects enough of them to violates this constraint for the resulting
// change output set.
const adjustedChange = splitChangeIfTokenBundlesSizeExceedsLimit(
changeBundles,
computeMinimumCoinQuantity,
tokenBundleSizeExceedsLimit
);

return { changeBundles: adjustedChange, ...utxoSelection };
};

const validateChangeBundles = (
Expand Down Expand Up @@ -365,6 +435,7 @@ export const computeChangeAndAdjustForFee = async ({
computeMinimumCoinQuantity,
implicitValue,
outputValues,
tokenBundleSizeExceedsLimit,
uniqueTxAssetIDs,
utxoSelection
});
Expand Down Expand Up @@ -395,6 +466,7 @@ export const computeChangeAndAdjustForFee = async ({
fee: estimatedCosts.fee,
implicitValue,
outputValues,
tokenBundleSizeExceedsLimit,
uniqueTxAssetIDs,
utxoSelection: pick(selectionWithChangeAndFee, ['utxoRemaining', 'utxoSelected'])
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,8 @@ const testInputSelection = (name: string, getAlgorithm: () => InputSelector) =>
getAlgorithm,
mockConstraints: {
...SelectionConstraints.MOCK_NO_CONSTRAINTS,
maxTokenBundleSize: 1
maxTokenBundleSize: 1,
minimumCoinQuantity: 1_000_000n
}
});
});
Expand Down
36 changes: 36 additions & 0 deletions packages/input-selection/test/RoundRobinRandomImprove.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { Cardano } from '@cardano-sdk/core';
import { MockChangeAddressResolver, SelectionConstraints } from './util';
import { TxTestUtil } from '@cardano-sdk/util-dev';
import {
babbageSelectionParameters,
cborUtxoSetWithManyAssets,
getCoreUtxosFromCbor,
txBuilder,
txEvaluator
} from './vectors';
import { defaultSelectionConstraints } from '../../tx-construction/src/input-selection/selectionConstraints';
import { roundRobinRandomImprove } from '../src/RoundRobinRandomImprove';

describe('RoundRobinRandomImprove', () => {
Expand Down Expand Up @@ -78,4 +86,32 @@ describe('RoundRobinRandomImprove', () => {
)
).toBeTruthy();
});

it('splits change outputs if they violate the tokenBundleSizeExceedsLimit constraint', async () => {
const utxoSet = getCoreUtxosFromCbor(cborUtxoSetWithManyAssets);
const maxSpendableAmount = 87_458_893n;
const assetsInUtxoSet = 511;

const constraints = defaultSelectionConstraints({
buildTx: txBuilder,
protocolParameters: babbageSelectionParameters,
redeemersByType: {},
txEvaluator: txEvaluator as never
});

const results = await roundRobinRandomImprove({
changeAddressResolver: new MockChangeAddressResolver()
}).select({
constraints,
outputs: new Set([TxTestUtil.createOutput({ coins: maxSpendableAmount })]),
preSelectedUtxo: new Set(),
utxo: utxoSet
});

expect(results.selection.inputs.size).toBe(utxoSet.size);
expect(results.selection.change.length).toBe(2);
expect(results.selection.change[0].value.assets!.size + results.selection.change[1].value.assets!.size).toBe(
assetsInUtxoSet
);
});
});
2 changes: 1 addition & 1 deletion packages/input-selection/test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"baseUrl": "."
},
"include": ["./**/*.ts"],
"include": ["./**/*.ts", "../../tx-construction/src/input-selection/selectionConstraints"],
"references": [
{
"path": "../src"
Expand Down
Loading

0 comments on commit 5caa57e

Please sign in to comment.