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

cardano-cli generated CBOR to serialization lib [BABBAGE] #576

Closed serxasz closed 1 year ago

serxasz commented 1 year ago

I am creating a transaction using cardano-cli. After building, I sign it with some random private key, and return signed transaction CBOR to UI.

UI takes the CBOR, removes my random witness which signed the transaction (using serialization lib), signs the transaction using any wallet (nami/eternl), adds user key as a witness and submits the transaction.

Here is the JS piece of code:

import * as S from '@emurgo/cardano-serialization-lib-asmjs'

const signTransaction = useCallback(async (cbor, payload) => {
    const txCli = S.Transaction.from_bytes(Buffer.from(cbor, 'hex'))
    const txBody = txCli.body()
    const witnessSet = txCli.witness_set()
    witnessSet.vkeys()?.free()
    const tx = S.Transaction.new(txBody, witnessSet)
    const encodedTx = Buffer.from(tx.to_bytes()).toString('hex')

    let encodedTxVkeyWitnesses
    try {
        encodedTxVkeyWitnesses = await walletRef.current.signTx(encodedTx, true)
    } catch (e) {
        if (e.code === 2) {
            throw new WalletError('User declined', ERROR_CODES.USER_DECLINED, e, payload)
        }
        throw new WalletError('Wallet error, please try again.', ERROR_CODES.WALLET_ERROR, e, payload)
    }

    const txVkeyWitnesses = S.TransactionWitnessSet.from_bytes(
        Buffer.from(encodedTxVkeyWitnesses, 'hex'),
    )
    witnessSet.set_vkeys(txVkeyWitnesses.vkeys())
    const txSigned = S.Transaction.new(tx.body(), witnessSet)

    return Buffer.from(txSigned.to_bytes()).toString('hex')
}, [walletName, getWalletAddress])

await walletRef.current.signTx is just a Nami or Eternl wallet method.

In the alonzo era (cardano-cli transaction build-raw --alonzo-era), this JS code works fine, and transaction is successfully submitted.

When I am building the transaction using babbage-era (cardano-cli transaction build-raw --babbage-era), signTx() method fails. If I pass raw CBOR received from the backend without doing anything with serialization lib, signTx() method is working fine, only on submit I am getting errors about missing witness.

Logically, the only reason why signing fails, is that serialization lib corrupts the CBOR when serializing/deserializing it. Somewhere here:

const txCli = S.Transaction.from_bytes(Buffer.from(cbor, 'hex'))
const txBody = txCli.body()
const witnessSet = txCli.witness_set()
witnessSet.vkeys()?.free()
const tx = S.Transaction.new(txBody, witnessSet)
const encodedTx = Buffer.from(tx.to_bytes()).toString('hex')

I am using the newest serialization lib version 11.2.1.

Submitting the very same transaction with cardano-cli works fine.

Also worth mentioning, transaction is using Plutusv2 script.

Can anyone point me to the right direction, or spot the error? Maybe serialization lib syntax is bit different with babbage transactions?

lisicky commented 1 year ago

Hi @serxasz!

You don't need to do this.

witnessSet.vkeys()?.free()

Because every getter in the CSL returns a copy, and in that code line, you get a copy of keys and free it. If you want to make TransactionWitnessSet without vkeys, you should use TransactionWitnessSet.new() and set everything from the old TransactionWitnessSet to the new one except vkeys.

Deserialization/serialization roundtrip can give different CBOR of a tx because CSL does not store information about the original format. For example, an indefinite length array after deserialization/serialization roundtrip can be a definite length array with the same elements. Also, TransactionOutput can be serialized in two ways and CSL chooses the way with the smallest size. And it would be best if you kept it in mind when you work with CSL and third-party tool-created CBOR. Therefore, I recommend doing all signs after all changes by CSL.

But anyway, your code should not affect the signing process for Alonzo or Babbage style tx. But I can missed smth. Could you provide CBOR of a tx ?

  1. before deserialization (cbor function arg)
  2. before signTx (encodedTx)
  3. after all final changes (txSigned)

I need it for both cases (babbage and alonzo era tx)

serxasz commented 1 year ago

Thanks for your feedback!

After changing freeing to TransactionWitnessSet.new(), lib managed to deserialize the transaction instead of returning '0000'. Also copied auxilaryData when constructing tx with S.Transaction.new().

Have in mind, that transaction is pretty complex, containing inline datums, redeemers, scripts and minting policies.

After serializing the Transaction back to CBOR, it's 20x shorter, and missing all of the parameters mentioned above. As backend stuff is already complicated, didn't want to dig deep on the frontend side, so ended up sending built (not signed) CBOR to UI (building with --cddl-format flag), feeding it to wallet.signTx(), and sending the retrieved witness back to backend. Backend uses cardano-cli transaction assemble and submits the transaction.

Nevertheless, thanks for the quick response. This can be closed now

lisicky commented 1 year ago

Okie. Thanks for your answer @serxasz !