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
234 stars 125 forks source link

Building a transaction that will require both payment and stake key? #605

Closed iftieaq closed 1 year ago

iftieaq commented 1 year ago

Hi,

I was just wondering, is it possible to build a transaction with Cardano Serialization Lib where both payment and stake key will be required without involving any staking operation such as delegating a pool? I need this to prevent the Franken address attack.

Usually, when a build a simple transaction such as making a payment, the transaction usually requires the buyer's payment key. Here is an example transaction I made with Cardano Serialization Lib.

// Payload from API user
const payload = {
    address: 'addr_test...',
    utxos: [...]
}

const whitelist = ['stake_test1...', 'stake12']
const paymentAddress = 'addr_test1';
const amount = 10 * 1000000

const buyerStakeAddress = RewardAddress.new(
    Address.from_bech32(payload.address).network_id(),
    BaseAddress.from_address(Address.from_bech32(payload.address)).stake_cred()
)

// Franken address will get pass this
if( !whitelist.includes(buyerStakeAddress.to_address().to_bech32()) )
    return false

const protocol_params = await this.get_protocol_parameters(NetworkInfo.testnet_preprod().network_id())
const linearFee = LinearFee.new(
    BigNum.from_str(protocol_params.min_fee_a.toString()),
    BigNum.from_str(protocol_params.min_fee_b.toString()),
)

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

const txBuilder = TransactionBuilder.new(txBuilderCfg);
const inputs = TransactionUnspentOutputs.new()

payload.utxos.forEach(raw => {
    inputs.add(TransactionUnspentOutput.from_hex(raw))
})

txBuilder.add_output(TransactionOutput.new(
    Address.from_bech32(paymentAddress),
    Value.new( BigNum.from_str(amount.toString()) )
))

txBuilder.add_inputs_from(inputs, CoinSelectionStrategyCIP2.RandomImprove)
txBuilder.set_ttl(protocol_params.slot + 3600)
txBuilder.add_change_if_needed(Address.from_bech32(payload.address));

const body = txBuilder.build()
const witnesses = TransactionWitnessSet.new();
const auxFinal = AuxiliaryData.new()
const transaction: Transaction = Transaction.new( body, witnesses, auxFinal )

return transaction.to_hex()

But this transaction is prone to Franken address attack as it only requires payment key. How do I build a transaction that will require both payment and staking signature from the user?

lisicky commented 1 year ago

Hi @iftieaq , sorry for long answer. Did you manage your issue ?

gitmachtl commented 1 year ago

You can normally include all kinds of keys in transactions, but they are not required. Also, HW-Wallets will not sign a random transaction with the stake secret key if there is no need for.

iftieaq commented 1 year ago

Hi @iftieaq , sorry for long answer. Did you manage your issue ?

Yes, I did. It seems CSL has a helper function called add_required_signer that enforces a signature from any stake or wallet address as required.

txBuilder.add_required_signer(BaseAddress.from_address(Address.from_bech32('addr_....)).stake_cred().to_keyhash())
lisicky commented 1 year ago

Thanks @iftieaq ! But I recommend to consider @gitmachtl comment. Because when you sign a tx you can check the tx body and after you signature no one can change the tx body and output addresses too otherwise tx will be invalid.