Handling ValueNotConservedUTxO with Cardano Serialization Lib

I have a wallet (addr_test1vp23tjzq9c9l526whmzek8wm36je7kwpkjautpx6qx07xjqztgxca) with the following UTXO.

+------------------------------------------------------------------+------+--------+------------------------------------------------------------------------------------------------------------------------------+
|                              TxHash                              | TxIx | Amount |                                                                                                                              |
+------------------------------------------------------------------+------+--------+------------------------------------------------------------------------------------------------------------------------------+
| 185f8f9ecca251f0a03e71f346695d095194c416a7b1518e63209a3cda5a531e |    0 |        | 19137471 lovelace + 1 b08b5ffeed926e231796d00c18c740f715535e026034a8221706d082.446567656e73436f696e32313638 + TxOutDatumNone |
| a9fe68df0352abae7f2872c3dee9b61b7f36ad3bacbdfda749e3a8518fad78ca |    0 |        | 4668338 lovelace + TxOutDatumNone                                                                                            |
| f01c48d72aadc8f96e5816f04f3ddd610956ed2dc2734a4e9b7ef60966d31b31 |    1 |        | 173296387 lovelace + 1 .... + TxOutDatumNone                                                                                 |
+------------------------------------------------------------------+------+--------+------------------------------------------------------------------------------------------------------------------------------+

I’m trying to build a self-transaction where I will input the first two UTXO ( ending cda5a531e and 18fad78ca), and basically, everything will be returned as a change to the same address (consider this as an experiment). Here is what I have tried so far…


// instantiate the tx builder with the Cardano protocol parameters - these may change later on
const linearFee = LinearFee.new(
	BigNum.from_str(protocolParams.min_fee_a.toString()),
	BigNum.from_str(protocolParams.min_fee_b.toString()),
)

const txBuilderCfg = TransactionBuilderConfigBuilder.new()
	.fee_algo(linearFee)
	.pool_deposit(BigNum.from_str(protocolParams.pool_deposit))
	.key_deposit(BigNum.from_str(protocolParams.key_deposit))
	.max_value_size(parseInt(protocolParams.max_val_size))
	.max_tx_size(protocolParams.max_tx_size)
	.coins_per_utxo_word(BigNum.from_str(protocolParams.coins_per_utxo_size))
	.build();

const txBuilder = TransactionBuilder.new(txBuilderCfg);


// Value of first utxo
const multi_asset_value = Value.new(BigNum.from_str('19137471'))
const multi_asset = MultiAsset.new();
const asset = Assets.new();

asset.insert(
	AssetName.from_hex('446567656e73436f696e32313638'),
	BigNum.from_str('1')
)

multi_asset.insert(
	ScriptHash.from_hex('b08b5ffeed926e231796d00c18c740f715535e026034a8221706d082'),
	asset
);

multi_asset_value.set_multiasset(multi_asset)


// Add first utxo with multi asset value
txBuilder.add_input(
	address,
	TransactionInput.new( TransactionHash.from_hex('185f8f9ecca251f0a03e71f346695d095194c416a7b1518e63209a3cda5a531e'), 0 ),
	multi_asset_value
)

// Second utxo with lovelace only
txBuilder.add_input(
	address,
	TransactionInput.new( TransactionHash.from_hex('a9fe68df0352abae7f2872c3dee9b61b7f36ad3bacbdfda749e3a8518fad78ca'), 0 ),
	Value.new(BigNum.from_str('4668338'))
)

txBuilder.set_ttl(latestBlock.slot + 4000);
txBuilder.add_change_if_needed(address);

const txBody = txBuilder.build();
const txHash = CSL.hash_transaction(txBody);
const witnesses = CSL.TransactionWitnessSet.new();

const vkeyWitnesses = CSL.Vkeywitnesses.new();
const vkeyWitness = CSL.make_vkey_witness(txHash, paymentKey);

vkeyWitnesses.add(vkeyWitness);
witnesses.set_vkeys(vkeyWitnesses);

const unsignedTx = txBuilder.build_tx();
const transaction = Transaction.new(
	txBody,
	witnesses,
	unsignedTx.auxiliary_data(), // transaction metadata
);

const tx = await getBlockFrost().txSubmit(transaction.to_hex())

When I tried to submit the transaction, I got the following error

transaction submit error ShelleyTxValidationError ShelleyBasedEraBabbage (ApplyTxError [UtxowFailure (UtxoFailure (FromAlonzoUtxoFail (ValueNotConservedUTxO (Value 23805809 (fromList [(PolicyID {policyID = ScriptHash \"b08b5ffeed926e231796d00c18c740f715535e026034a8221706d082\"},fromList [(446567656e73436f696e32313638,1)])])) (Value 23805809 (fromList [(PolicyID {policyID = ScriptHash \"b08b5ffeed926e231796d00c18c740f715535e026034a8221706d082\"},fromList [(6567656e,1)])])))))])

I know that ValueNotConservedUTxO means input != output + fee, but here the network is defining the fee and returning the change. I’m a bit clueless about what I did wrong.

Any help regarding this issue will be appreciated.

If you look at the details of the error, the ADA value 23805809 is perfectly conserved and the policy ID is also okay.

What is not conserved is the asset name. It is 446567656e73436f696e32313638 in the input, but 6567656e in the output (which is actually a small substring – “egen” – of the whole name – “DegensCoin2168”).

But I don’t know why. It does not seem to come from your code. Currently researching if there is a known issue with CSL, but I did not find anything up to now.

Nice catch; I also tried to decode the error message but couldn’t. Please keep me posted if you find something.

Looks like a bug in CSL to me. I tried:

"use strict"

const CSL = require("@emurgo/cardano-serialization-lib-nodejs/cardano_serialization_lib")

const asset_from_hex = CSL.AssetName.from_hex('446567656e73436f696e32313638')
console.log('AssetName.from_hex: ' + asset_from_hex.to_hex())

const asset_bytes = Uint8Array.from(Buffer.from('446567656e73436f696e32313638', 'hex'))

const asset_from_bytes = CSL.AssetName.from_bytes(asset_bytes)
console.log('AssetName.from_bytes: ' + asset_from_bytes.to_hex())

const asset_new = CSL.AssetName.new(asset_bytes)
console.log('AssetName.new: ' + asset_new.to_hex())

which resulted in:

AssetName.from_hex: 446567656e
AssetName.from_bytes: 446567656e
AssetName.new: 4e446567656e73436f696e32313638

So, your workaround would be to use:

    AssetName.new(Uint8Array.from(Buffer.from('446567656e73436f696e32313638', 'hex'))),

instead of:

    AssetName.from_hex('446567656e73436f696e32313638'),

Edit: It’s actually not a bug. from_bytes and from_hex do not take the pure asset name in hex as input, but the CBOR encoding of it. So, the first byte (44) is not interpreted as the letter D, but as specifying that we have a string of length 4, which is then egen.

That’s why the to_hex of the thing we create with new has an additional 4e in front of it which specifies that we have a string of length 15.

But it’s kind of inconsistent, for example, with how from_hex for ScriptHash behaves.

You are right. I tried transforming the asset name to hex, and it was returning a 4e, which I didn’t understand and thought was an incorrect response. But I tried to submit the transaction, and it went through.

Even though it’s not a bug, it’s super inconsistent to be considered as one.

1 Like