Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Patches: Redeemer ordering + Value invariant #14

Merged
merged 1 commit into from
May 7, 2024
Merged
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
7 changes: 7 additions & 0 deletions .changeset/serious-ladybugs-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@blaze-cardano/emulator": patch
"@blaze-cardano/sdk": patch
"@blaze-cardano/tx": patch
---

Values enforce no 0-quantity assets, redeemers have correct indexes, transaction network id is attached, various emulator patches
12 changes: 7 additions & 5 deletions packages/blaze-emulator/src/emulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export class Emulator {
}
this.params = params;
this.evaluator = evaluator ?? makeUplcEvaluator(params, 1, 1);
this.addUtxo = this.addUtxo.bind(this);
this.removeUtxo = this.removeUtxo.bind(this);
}

stepForwardBlock(): void {
Expand Down Expand Up @@ -570,7 +572,7 @@ export class Emulator {

// Minimum collateral amount included
const minCollateral = BigInt(
this.params.collateralPercentage * Number(body.fee()),
Math.ceil(this.params.collateralPercentage * Number(body.fee())),
);

// If any scripts have been invoked, minimum collateral must be included
Expand Down Expand Up @@ -604,7 +606,7 @@ export class Emulator {
.coin()}, MinADA: ${minAda}`,
);

const length = output.toCbor().length / 2;
const length = output.amount().toCbor().length / 2;
if (length > this.params.maxValueSize)
throw new Error(
`Output ${index}'s value exceeds the maximum allowed size. Output: ${length} bytes, Maximum: ${this.params.maxValueSize} bytes`,
Expand Down Expand Up @@ -684,9 +686,9 @@ export class Emulator {
netValue = V.sub(netValue, new Value(fee));
if (!V.empty(netValue))
throw new Error(
`Value not conserved. Leftover Value: ${netValue.coin()}, ${
netValue.multiasset()?.entries() ?? ""
}`,
`Value not conserved. Leftover Value: ${netValue.coin()}, ${Array.from(
netValue.multiasset()?.entries() ?? [],
)}`,
);

this.acceptTransaction(tx);
Expand Down
1 change: 1 addition & 0 deletions packages/blaze-sdk/src/blaze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class Blaze<ProviderType extends Provider, WalletType extends Wallet> {
const myUtxos = await this.wallet.getUnspentOutputs();
const changeAddress = await this.wallet.getChangeAddress();
return new TxBuilder(params)
.setNetworkId(await this.wallet.getNetworkId())
.addUnspentOutputs(myUtxos)
.setChangeAddress(changeAddress)
.useEvaluator((x, y) => this.provider.evaluateTransaction(x, y));
Expand Down
81 changes: 74 additions & 7 deletions packages/blaze-tx/src/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
Slot,
PoolId,
StakeDelegationCertificate,
NetworkId,
} from "@blaze-cardano/core";
import {
CborSet,
Expand Down Expand Up @@ -53,6 +54,7 @@ import {
StakeDelegation,
CertificateType,
blake2b_256,
RedeemerTag,
} from "@blaze-cardano/core";
import * as value from "./value";
import { micahsSelector } from "./coinSelection";
Expand Down Expand Up @@ -101,6 +103,7 @@ export class TxBuilder {
private scriptSeen: Set<ScriptHash> = new Set(); // A set of script hashes that have been processed.
private changeAddress?: Address; // The address to send change to, if any.
private rewardAddress?: Address; // The reward address to delegate from, if any.
private networkId?: NetworkId; // The network ID for the transaction.
private changeOutputIndex?: number; // The index of the change output in the transaction.
private plutusData: TransactionWitnessPlutusData = new Set(); // A set of Plutus data for witness purposes.
private requiredWitnesses: Set<Ed25519PublicKeyHex> = new Set(); // A set of public keys required for witnessing the transaction.
Expand All @@ -116,6 +119,10 @@ export class TxBuilder {
private additionalSigners = 0;
private evaluator?: Evaluator;

private consumedMintHashes: Hash28ByteBase16[] = [];
private consumedWithdrawalHashes: Hash28ByteBase16[] = [];
private consumedSpendInputs: string[] = [];

/**
* Constructs a new instance of the TxBuilder class.
* Initializes a new transaction body with an empty set of inputs, outputs, and no fee.
Expand All @@ -130,6 +137,16 @@ export class TxBuilder {
);
}

private insertSorted<T extends string>(arr: T[], el: T) {
const index = arr.findIndex((x) => x.localeCompare(el) > 0);
if (index == -1) {
arr.push(el);
} else {
arr.splice(index, 0, el);
}
return index;
}

/**
* Sets the change address for the transaction.
* This address will receive any remaining funds not allocated to outputs or fees.
Expand Down Expand Up @@ -159,6 +176,11 @@ export class TxBuilder {
return this;
}

setNetworkId(networkId: NetworkId) {
this.networkId = networkId;
return this;
}

/**
* The additional signers field is used to add additional signing counts for fee calculation.
* These will be included in the signing phase at a later stage.
Expand Down Expand Up @@ -234,6 +256,8 @@ export class TxBuilder {
redeemer?: PlutusData,
unhashDatum?: PlutusData,
) {
const oref = utxo.input().transactionId() + utxo.input().index().toString();
const insertIdx = this.insertSorted(this.consumedSpendInputs, oref);
// Retrieve the current inputs from the transaction body for manipulation.
const inputs = this.body.inputs();
const values = [...inputs.values()];
Expand All @@ -245,9 +269,7 @@ export class TxBuilder {
val.transactionId() == utxo.input().transactionId(),
)
) {
throw new Error(
"Cannot add duplicate reference input to the transaction",
);
throw new Error("Cannot add duplicate input to the transaction");
}
// Add the new input to the array of inputs and update the transaction body.
values.push(utxo.input());
Expand Down Expand Up @@ -287,9 +309,17 @@ export class TxBuilder {
}
// Prepare and add the redeemer to the transaction, including execution units estimation.
const redeemers = [...this.redeemers.values()];
for (const redeemer of redeemers) {
if (
redeemer.tag() == RedeemerTag.Spend &&
redeemer.index() >= insertIdx
) {
redeemer.setIndex(redeemer.index() + 1n);
}
}
redeemers.push(
Redeemer.fromCore({
index: 256,
index: insertIdx,
purpose: RedeemerPurpose["spend"],
data: redeemer.toCore(),
executionUnits: {
Expand Down Expand Up @@ -337,6 +367,10 @@ export class TxBuilder {
assets: Map<AssetName, bigint>,
redeemer?: PlutusData,
) {
const insertIdx = this.insertSorted(
this.consumedMintHashes,
PolicyIdToHash(policy),
);
// Retrieve the current mint map from the transaction body, or initialize a new one if none exists.
const mint: TokenMap = this.body.mint() ?? new Map();
// Iterate over the assets map and add each asset to the mint map under the specified policy.
Expand All @@ -352,11 +386,19 @@ export class TxBuilder {
this.requiredPlutusScripts.add(PolicyIdToHash(policy));
// Retrieve the current list of redeemers and prepare to add a new one.
const redeemers = [...this.redeemers.values()];
for (const redeemer of redeemers) {
if (
redeemer.tag() == RedeemerTag.Mint &&
redeemer.index() >= insertIdx
) {
redeemer.setIndex(redeemer.index() + 1n);
}
}
// Create and add a new redeemer for the minting action, specifying execution units.
// Note: Execution units are placeholders and are replaced with actual values during the evaluation phase.
redeemers.push(
Redeemer.fromCore({
index: 256,
index: insertIdx,
purpose: RedeemerPurpose["mint"], // Specify the purpose of the redeemer as minting.
data: redeemer.toCore(), // Convert the provided PlutusData redeemer to its core representation.
executionUnits: {
Expand Down Expand Up @@ -992,6 +1034,12 @@ export class TxBuilder {
"Cannot complete transaction without setting change address",
);
}
if (!this.networkId) {
throw new Error(
"Cannot complete transaction without setting a network id",
);
}
this.body.setNetworkId(this.networkId);
// Gather all inputs from the transaction body.
const inputs = [...this.body.inputs().values()];
// Perform initial checks and preparations for coin selection.
Expand Down Expand Up @@ -1212,6 +1260,17 @@ export class TxBuilder {
* @throws {Error} If the reward account does not have a stake credential or if any other error occurs.
*/
addWithdrawal(address: RewardAccount, amount: bigint, redeemer?: PlutusData) {
const withdrawalHash =
Address.fromBech32(address).getProps().delegationPart?.hash;
if (!withdrawalHash) {
throw new Error(
"addWithdrawal: The RewardAccount provided does not have an associated stake credential.",
);
}
const insertIdx = this.insertSorted(
this.consumedWithdrawalHashes,
withdrawalHash,
);
// Retrieve existing withdrawals or initialize a new map if none exist.
const withdrawals: Map<RewardAccount, bigint> =
this.body.withdrawals() ?? new Map();
Expand All @@ -1222,11 +1281,19 @@ export class TxBuilder {
// If a redeemer is provided, process it for script validation.
if (redeemer) {
const redeemers = [...this.redeemers.values()];
for (const redeemer of redeemers) {
if (
redeemer.tag() == RedeemerTag.Reward &&
redeemer.index() >= insertIdx
) {
redeemer.setIndex(redeemer.index() + 1n);
}
}
// Add the redeemer to the list of redeemers with execution units based on transaction parameters.
redeemers.push(
Redeemer.fromCore({
index: 256, // TODO: Determine the correct index for the redeemer.
purpose: RedeemerPurpose["mint"], // TODO: Confirm the purpose of the redeemer.
index: insertIdx,
purpose: RedeemerPurpose["withdrawal"], // TODO: Confirm the purpose of the redeemer.
data: redeemer.toCore(),
executionUnits: {
memory: this.params.maxExecutionUnitsPerTransaction.memory,
Expand Down
11 changes: 8 additions & 3 deletions packages/blaze-tx/src/value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ export function merge(a: Value, b: Value): Value {
for (const key of bma.keys()) {
const a = ma.get(key);
const b = bma.get(key)!;
if (a) {
ma.set(key, a + b);
const newVal = a ? a + b : b;
if (newVal == 0n) {
ma.delete(key);
} else {
ma.set(key, b);
ma.set(key, newVal);
}
}
}
Expand Down Expand Up @@ -186,6 +187,10 @@ export function makeValue(
}
const tokenMap: Map<AssetId, bigint> = new Map();
for (const [asset, qty] of assets) {
if (qty == 0n)
throw new Error(
"Cannot create a Value object with a zero quantity asset.",
);
tokenMap.set(AssetId(asset), qty);
}
return Value.fromCore({
Expand Down