Emurgo / cardano-serialization-lib

This is a library, written in Rust, for serialization & deserialization of data structures used in Cardano's Haskell implementation of Alonzo along with useful utility functions.
Other
230 stars 124 forks source link

when a transaction has 2 inputs the fees are miscalculated, and uses 1 ADA as fee #668

Closed caetanix closed 3 months ago

caetanix commented 4 months ago

I use this library and blockfrost to submit the transaction, but i detect when an address have 2 inputs of 1 ADA, and after send 1 ADA to another address, the fee of this transaction is 1 ADA.

I have created an example here: https://preview.beta.explorer.cardano.org/en/address/addr_test1qqu0cf4zt3mklfdjyzada4cxddspkdyd7e5k9rsrg07et6j4agylhzua083ngsellerp0t5s7up3swfslg5heqwn7v5q8vrpjq

2 inputs of 1 ADA 1 output of 2 ADA (1 ADA sent + 1 ADA fee)

this is my code...

const bip39 = require('bip39'); const Blockfrost = require('@blockfrost/blockfrost-js'); const CardanoWasm = require('@emurgo/cardano-serialization-lib-nodejs');

exports.transaction = async (req, res) => {

// remove here some requirements from API

try {

    const API = new Blockfrost.BlockFrostAPI({
        projectId: paramProjectId,
    });

    const bip32PrvKey = mnemonicToPrivateKey(paramMnemonic);
    const { signKey, address } = deriveAddressPrvKey(bip32PrvKey, env.MAINNET);

    if (paramPublicKey !== address) {
        throw Error('public key does not match mnemonic');
    }

    // Retrieve protocol parameters
    const protocolParams = await API.epochsLatestParameters();

    // Retrieve utxo for the address
    let utxo = await API.addressesUtxosAll(address);
    if (utxo.length === 0) {
        throw Error('public key does not have enough funds');
    }

    // Get current blockchain slot from latest block
    const latestBlock = await API.blocksLatest();
    const currentSlot = latestBlock.slot;
    if (!currentSlot) {
        throw Error('failed to fetch slot number');
    }

    // Get balance from address
    const output = await API.addresses(address)
    if (output.amount[0].quantity) {
        const balance = parseInt(output.amount[0].quantity);
        if (paramAmount > balance) {
            throw Error('amount is greater than balance');
        }
    }

    // Prepare transaction
    const { txBody } = composeTransaction(address, paramAddress, paramAmount, utxo, {
        protocolParams,
        currentSlot,
    });

   // Sign transaction
    const transaction = signTransaction(txBody, signKey);

    // txSubmit endpoint returns transaction hash on successful submit
    const txHash = await API.txSubmit(transaction.to_bytes());

    console.log('Transaction successfully submitted: '+txHash);

    // Before the tx is included in a block it is a waiting room known as mempool
    // Retrieve transaction from Blockfrost Mempool
    let mempoolTx = null;

    try {
        mempoolTx = await API.mempoolTx(txHash);
    }
    catch (e) {
        console.log('mempoolTx error: ' + e.message)
    }

    res.json({
        'hash': txHash,
        'mempool': mempoolTx,
    });
}
catch (e) {
    console.log(e)
    res.status(400).json( { 'error' : e.message } );
}

};

const deriveAddressPrvKey = (bipPrvKey, mainnet) => { const networkId = mainnet ? CardanoWasm.NetworkInfo.mainnet().network_id() : CardanoWasm.NetworkInfo.testnet_preview().network_id(); const accountIndex = 0; const addressIndex = 0;

const accountKey = bipPrvKey
    .derive(harden(1852)) // purpose
    .derive(harden(1815)) // coin type
    .derive(harden(accountIndex)); // account #

const utxoKey = accountKey
    .derive(0) // external
    .derive(addressIndex);

const stakeKey = accountKey
    .derive(2) // chimeric
    .derive(0)
    .to_public();

const baseAddress = CardanoWasm.BaseAddress.new(
    networkId,
    CardanoWasm.StakeCredential.from_keyhash(
        utxoKey.to_public().to_raw_key().hash()
    ),
    CardanoWasm.StakeCredential.from_keyhash(stakeKey.to_raw_key().hash())
);

const address = baseAddress.to_address().to_bech32();

return { signKey: utxoKey.to_raw_key(), address: address };

};

const harden = (num) => { return 0x80000000 + num; };

const mnemonicToPrivateKey = (mnemonic) => { const entropy = bip39.mnemonicToEntropy(mnemonic);

return CardanoWasm.Bip32PrivateKey.from_bip39_entropy(
    Buffer.from(entropy, "hex"),
    Buffer.from("")
);

};

const composeTransaction = (address, outputAddress, outputAmount, utxos, params) => {

const txBuilder = CardanoWasm.TransactionBuilder.new(
    CardanoWasm.TransactionBuilderConfigBuilder.new()
        .fee_algo(
            CardanoWasm.LinearFee.new(
                CardanoWasm.BigNum.from_str(params.protocolParams.min_fee_a.toString()),
                CardanoWasm.BigNum.from_str(params.protocolParams.min_fee_b.toString()),
            ),
        )
        .pool_deposit(CardanoWasm.BigNum.from_str(params.protocolParams.pool_deposit))
        .key_deposit(CardanoWasm.BigNum.from_str(params.protocolParams.key_deposit))
        .max_value_size(parseInt(params.protocolParams.max_val_size))
        .max_tx_size(parseInt(params.protocolParams.max_tx_size))
        .coins_per_utxo_byte(CardanoWasm.BigNum.from_str(params.protocolParams.coins_per_utxo_size))
        .build(),
);

const outputAddr = CardanoWasm.Address.from_bech32(outputAddress);
const changeAddr = CardanoWasm.Address.from_bech32(address);

const ttl = params.currentSlot + 7200;

txBuilder.set_ttl(ttl);
txBuilder.add_output(
    CardanoWasm.TransactionOutput.new(
        outputAddr,
        CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(outputAmount.toString())),
    ),
);

const lovelaceUtxos = utxos.filter(u => !u.amount.find(a => a.unit !== 'lovelace'));
const unspentOutputs = CardanoWasm.TransactionUnspentOutputs.new();

for (const utxo of lovelaceUtxos) {
    const amount = utxo.amount.find(a => a.unit === 'lovelace')?.quantity;

    if (!amount) continue;

    const inputValue = CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(amount.toString()));

    const input = CardanoWasm.TransactionInput.new(
        CardanoWasm.TransactionHash.from_bytes(Buffer.from(utxo.tx_hash, 'hex')),
        utxo.output_index,
    );

    const output = CardanoWasm.TransactionOutput.new(changeAddr, inputValue);
    unspentOutputs.add(CardanoWasm.TransactionUnspentOutput.new(input, output));
}

txBuilder.add_inputs_from(unspentOutputs, CardanoWasm.CoinSelectionStrategyCIP2.LargestFirst);
txBuilder.add_change_if_needed(changeAddr);

const txBody = txBuilder.build();
const txHash = Buffer.from(CardanoWasm.hash_transaction(txBody).to_bytes()).toString('hex');

return {
    txHash,
    txBody,
};

};

const signTransaction = (txBody, signKey) => { const txHash = CardanoWasm.hash_transaction(txBody); const witnesses = CardanoWasm.TransactionWitnessSet.new(); const vkeyWitnesses = CardanoWasm.Vkeywitnesses.new();

vkeyWitnesses.add(CardanoWasm.make_vkey_witness(txHash, signKey));
witnesses.set_vkeys(vkeyWitnesses);

return CardanoWasm.Transaction.new(txBody, witnesses);

};

lisicky commented 4 months ago

Hi @caetanix ! Unfortunately I don't know which CSL version you use. Could you tell me which one do you use ? Also could you isolate a code example to reproduce it on our side, you can create a fake address and fake utxos. And CBOR of your tx would be very helpful. Also by the provided link I see only one 1 ada tx, and this tx has only 1 output that doesn't match with your code where you create and output and also call add_change_if_needed(). The add_change_if_needed() also should produce an output except case when you don't have enough ada to cover a new output

caetanix commented 4 months ago

Hello, i'm using last version, "@emurgo/cardano-serialization-lib-nodejs": "11.5.0",

I have an account with 8k ADA, and when i use this same code, to sent it works good, generates 2 outputs.

Here is the hash: https://preview.beta.explorer.cardano.org/en/transaction/5a7e9c2f823a7a0220e1d0a601c3057fa75c2ceded6d718503152bc8b591dde2/utxOs

CBOR: 132,164,0,129,130,88,32,65,242,12,0,156,135,86,252,27,234,41,100,228,160,248,219,51,177,115,141,235,222,151,140,231,45,71,55,11,202,10,179,1,1,130,130,88,57,0,56,252,38,162,92,119,111,165,178,32,186,222,215,6,107,96,27,52,141,246,105,98,142,3,67,253,149,234,85,234,9,251,139,157,121,227,52,67,63,254,70,23,174,144,247,3,24,57,48,250,41,124,129,211,243,40,26,0,15,66,64,130,88,57,0,43,234,83,159,55,158,211,201,6,131,41,253,212,153,54,21,1,129,245,29,98,9,151,213,102,56,187,58,41,73,60,115,106,195,142,239,126,90,7,198,119,97,34,87,161,39,176,38,129,37,165,25,180,133,146,129,27,0,0,0,2,13,52,60,15,2,26,0,2,146,45,3,26,2,176,64,193,161,0,129,130,88,32,40,195,29,133,252,151,31,210,115,170,154,177,233,178,164,5,66,88,233,198,176,31,135,109,61,53,164,89,168,148,192,171,88,64,240,251,228,183,223,124,246,180,215,156,77,155,197,87,247,152,14,188,109,132,73,183,236,167,72,118,38,99,78,130,207,56,24,64,239,63,195,162,146,176,37,70,110,19,200,254,195,180,45,75,121,64,76,110,97,125,185,122,234,64,241,69,28,3,245,246

So, i have done another test... I have sent 4x1 ADA to an address... and after i sent 1 ADA

Here is the hash: https://preview.beta.explorer.cardano.org/en/transaction/70d79ff0c623246b5e5411c97d479a98259bf5f991439e84cb2b1884cfba1616/utxOs

CBOR: 132,164,0,130,130,88,32,65,242,12,0,156,135,86,252,27,234,41,100,228,160,248,219,51,177,115,141,235,222,151,140,231,45,71,55,11,202,10,179,0,130,88,32,90,126,156,47,130,58,122,2,32,225,208,166,1,195,5,127,167,92,44,237,237,109,113,133,3,21,43,200,181,145,221,226,0,1,129,130,88,57,0,43,234,83,159,55,158,211,201,6,131,41,253,212,153,54,21,1,129,245,29,98,9,151,213,102,56,187,58,41,73,60,115,106,195,142,239,126,90,7,198,119,97,34,87,161,39,176,38,129,37,165,25,180,133,146,129,26,0,15,66,64,2,26,0,15,66,64,3,26,2,176,65,93,161,0,129,130,88,32,194,67,146,111,157,0,96,11,184,28,36,195,117,97,164,67,176,213,7,253,20,220,80,15,8,205,249,80,170,100,245,244,88,64,47,169,164,122,126,219,101,103,75,166,61,23,221,90,163,220,26,35,216,149,10,134,15,141,22,25,137,107,3,219,54,75,249,237,14,96,156,200,204,188,112,54,20,74,182,131,186,241,184,209,26,46,100,221,145,87,119,161,96,166,220,2,74,6,245,246

The transaction uses 2 inputs (of previous 4x1 ADA), but only 1 output... 1 ADA + 1 ADA of FEE The address still have 2 ADA, so have enough to generate another outputs... but address have 4 ADA, but uses 4 utxo..

But maybe the problem is relatated to the use 2 inputs of 1 ADA ? and because uses 1 ADA to sent.... and the second input is also 1 ADA, and if the fee is only 168977, the input will keep only 831023 and is less than minimum_coins, so the blockchain uses all the remain fee ?

I test with balance of 4 ADA, i have sent 2 ADA, so the transaction uses 3 inputs of 1 ADA, and 1 output only.... 2 ADA of output and 1 ADA of fee

Here is the hash: https://preview.beta.explorer.cardano.org/en/transaction/57ead77ade60009dd61cfac23c6467ddbcf48830bfea31c6ff0864ecca2df027/utxOs

CBOR 132,164,0,131,130,88,32,19,233,72,255,160,220,155,25,86,82,155,192,36,227,30,86,11,67,204,247,250,191,207,155,30,55,4,59,114,51,29,119,0,130,88,32,56,43,235,114,116,43,53,135,48,24,10,139,234,11,179,222,98,153,126,162,165,119,34,159,212,209,216,211,192,12,239,94,0,130,88,32,221,100,53,225,231,3,117,142,81,189,235,6,138,1,38,181,13,200,3,191,128,160,85,209,54,182,6,115,64,142,236,144,0,1,129,130,88,57,0,43,234,83,159,55,158,211,201,6,131,41,253,212,153,54,21,1,129,245,29,98,9,151,213,102,56,187,58,41,73,60,115,106,195,142,239,126,90,7,198,119,97,34,87,161,39,176,38,129,37,165,25,180,133,146,129,26,0,30,132,128,2,26,0,15,66,64,3,26,2,176,68,182,161,0,129,130,88,32,194,67,146,111,157,0,96,11,184,28,36,195,117,97,164,67,176,213,7,253,20,220,80,15,8,205,249,80,170,100,245,244,88,64,178,205,228,122,200,206,222,56,202,2,31,152,106,57,127,176,13,115,3,100,238,241,141,118,83,99,37,17,109,128,218,197,162,208,198,101,248,22,61,11,247,79,34,199,86,127,205,57,104,174,86,167,229,38,228,183,56,42,100,92,209,46,110,1,245,246

full code: ` const API = new Blockfrost.BlockFrostAPI({ projectId: paramProjectId, });

    const entropy = bip39.mnemonicToEntropy(paramMnemonic);
    const bip32PrvKey = CardanoWasm.Bip32PrivateKey.from_bip39_entropy(
        Buffer.from(entropy, "hex"),
        Buffer.from("")
    );

    const networkId = CardanoWasm.NetworkInfo.testnet_preview().network_id();
    const accountIndex = 0;
    const addressIndex = 0;

    const accountKey = bip32PrvKey
        .derive(harden(1852)) // purpose
        .derive(harden(1815)) // coin type
        .derive(harden(accountIndex)); // account #

    const utxoKey = accountKey
        .derive(0) // external
        .derive(addressIndex);

    const stakeKey = accountKey
        .derive(2) // chimeric
        .derive(0)
        .to_public();

    const signKey = utxoKey.to_raw_key();

    // Retrieve protocol parameters
    const protocolParams = await API.epochsLatestParameters();

    // Retrieve utxo for the address
    let utxo = await API.addressesUtxosAll(paramPublicKey);

    // Get current blockchain slot from latest block
    const latestBlock = await API.blocksLatest();
    const currentSlot = latestBlock.slot;

    // Prepare transaction
    const params = {
        protocolParams,
        currentSlot,
    };

    const txBuilder = CardanoWasm.TransactionBuilder.new(
        CardanoWasm.TransactionBuilderConfigBuilder.new()
            .fee_algo(
                CardanoWasm.LinearFee.new(
                    CardanoWasm.BigNum.from_str(params.protocolParams.min_fee_a.toString()),
                    CardanoWasm.BigNum.from_str(params.protocolParams.min_fee_b.toString()),
                ),
            )
            .pool_deposit(CardanoWasm.BigNum.from_str(params.protocolParams.pool_deposit))
            .key_deposit(CardanoWasm.BigNum.from_str(params.protocolParams.key_deposit))
            .max_value_size(parseInt(params.protocolParams.max_val_size))
            .max_tx_size(parseInt(params.protocolParams.max_tx_size))
            .coins_per_utxo_byte(CardanoWasm.BigNum.from_str(params.protocolParams.coins_per_utxo_size))
            .build(),
    );

    const outputAddr = CardanoWasm.Address.from_bech32(paramAddress);
    const changeAddr = CardanoWasm.Address.from_bech32(paramPublicKey);

    const ttl = params.currentSlot + 7200;

    txBuilder.set_ttl(ttl);
    txBuilder.add_output(
        CardanoWasm.TransactionOutput.new(
            outputAddr,
            CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(paramAmount.toString())),
        ),
    );

    const lovelaceUtxos = utxo.filter(u => !u.amount.find(a => a.unit !== 'lovelace'));
    const unspentOutputs = CardanoWasm.TransactionUnspentOutputs.new();

    for (const utxo of lovelaceUtxos) {
        const amount = utxo.amount.find(a => a.unit === 'lovelace')?.quantity;

        if (!amount) continue;

        const inputValue = CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(amount.toString()));

        const input = CardanoWasm.TransactionInput.new(
            CardanoWasm.TransactionHash.from_bytes(Buffer.from(utxo.tx_hash, 'hex')),
            utxo.output_index,
        );

        const output = CardanoWasm.TransactionOutput.new(changeAddr, inputValue);
        unspentOutputs.add(CardanoWasm.TransactionUnspentOutput.new(input, output));
    }

    txBuilder.add_inputs_from(unspentOutputs, CardanoWasm.CoinSelectionStrategyCIP2.LargestFirst);
    txBuilder.add_change_if_needed(changeAddr);

    const txBody = txBuilder.build();

    // Sign transaction
    const txHash = CardanoWasm.hash_transaction(txBody);
    const witnesses = CardanoWasm.TransactionWitnessSet.new();
    const vkeyWitnesses = CardanoWasm.Vkeywitnesses.new();

    vkeyWitnesses.add(CardanoWasm.make_vkey_witness(txHash, signKey));
    witnesses.set_vkeys(vkeyWitnesses);

    const transaction = CardanoWasm.Transaction.new(txBody, witnesses);

    console.log(transaction.to_bytes().toString())

    // txSubmit endpoint returns transaction hash on successful submit
    const hash = await API.txSubmit(transaction.to_bytes());

    console.log('Transaction successfully submitted: '+hash);

`

lisicky commented 3 months ago

Let me add some explanations: Current implementation of add_inputs_from doesn't take into account change, add_change_if_needed calculates also fee and if (outputs + fee) < total_inputs_value it will try to add a change output. But each output has minimal ada value there is a case when your ((total_outputs_value + fee) - (total_inputs)) is less than minimal ada for change output and in this case all leftovers will be added into fee because it is impossible to create change output due insufficient ada. And seems it is your case with 2 ada inputs and 1 ada output

caetanix commented 3 months ago

Hi again, yes its related with ADA amounts, so everything its OK! Thanks for your help! Best Regards