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

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

Closed
caetanix opened this issue Mar 28, 2024 · 4 comments

Comments

@caetanix
Copy link

caetanix commented Mar 28, 2024

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
Copy link
Contributor

lisicky commented Mar 28, 2024

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
Copy link
Author

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
Copy link
Contributor

lisicky commented Apr 4, 2024

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
Copy link
Author

caetanix commented Apr 4, 2024

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

@caetanix caetanix closed this as completed Apr 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants