Helios Reference Inputs

Cardano Reference Inputs - Make Transactions Cheaper

With the Vasil Hardfork Cardano gained a few new abilities, the one I'm going to discuss today is Reference Inputs. A Reference Input is a Transaction Input that is not consumed by the transaction and so it doesn't need to be signed for. These inputs can provide Oracle-like data to a script, or even the script itself!

Reference Scripts

One of the main features of Reference Inputs is to provide a script for a Transaction. On Cardano, Transaction Fees are calculated based on the total size of the transaction; therefore if you can remove the script from the transaction entirely then you can make a significant dent in the fees that need to be paid! So how do we go about this?

Step 1: Create a UTxO that contains a Script

The first step is to create and execute a Transaction that will produce a UTxO with an attached script. The below code is an example of how this can be done:

// Make this an address that you have control of, but that
// you are happy to just leave! As if you spend the UTxO
// then the script is no longer usable!
const CONTRACTS_ADDRESS = "addrXXXXXXXX";

// Get our Program
const program = Program.new("HELIOS Script");
const compiled = program.compile(true);

// Get an instance of a Wallet (for now we are using Nami)
const wallet = await window.cardano.nami.enable();
const walletAddress = (await wallet.getUsedAddresses())[0];

// Create a TxOutput for the Script.
// Notice that we pass the 'program' into the 4th Parameter
const scriptOutput = new TxOutput(
  Address.fromBech32(CONTRACTS_ADDRESS),
  new Value(BigInt(5_000_000)),
  undefined,
  compiled
);
// Calculate the Min Lovelace for the output; this can be fairly large
scriptOutput.correctLovelace(networkParams);

const walletUtxos = await wallet.getUtxos();

const tx: Tx = await new Tx()
    .addOutput(scriptOutput)
    .finalize(
        networkParams, 
        Address.fromHex(walletAddress), 
        walletUTxOs
    );
// Sign and Submit the transaction!
const signed = await wallet.signTx(
  bytesToHex(tx.toCbor()),
  true
);
tx.addSignatures(
  TxWitnesses.fromCbor(hexToBytes(signed)).signatures
);

const txHash = await wallet.submitTx(bytesToHex(tx.toCbor()));

From the above, you will get the txHash; this will be needed in Step 2.

Step 2: Utilize the Reference Script

Now that we have a Reference Input (script) on-chain, we can now create a transaction that makes use of it.

// Address where our Scripts are kept, same as from Step 1.
const CONTRACTS_ADDRESS = "addrXXXXXXXX";
// Value from RefInput
// However if you get the UTxO from the WalletHelper
// Then it is all populated for you.
const refInputValue = new Value(...);
// txHash result from Step 1
const refTxHash = '';
// If you follow step 1 above then the Script Output will be in Position 0
const refTxIndex = 0; 
// Helios still needs the program to do the ExUnits calc etc.
const program = Program.new("HELIOS Script");
// Compile the program, producing a UplcProgram
const compiled = program.compile(true);

const tx = new Tx();
const scriptInput = new TxRefInput(
     TxId.fromHex(refTxHash),
     BigInt(refTxIndex),
     new TxOutput(
        Address.fromBech32(CONTRACTS_ADDRESS),
        refInputValue
  )
);
tx.addRefInput(scriptUTxO, compiled);

Things that can go wrong

The Validator Hash of the Reference script doesn't match the expected

This is caused by the script 'compiled' hashing to a different value as to what the UTxO you are trying to validate is needing. I have come across this when updating Helios version which has some new ways of compiling to UplcProgram.

The way to work around this "issue*" is to skip the compile step above. If you utilize the Blockfrost Script CBOR endpoint, you can get the Compiled CBOR code, then you can create the variable compiled above in the following way:

const scriptCbor = await blockfrostScriptCbor('hash');
const compiled = UplcProgram.fromCbor(hexToBytes(scriptCbor));

\ - This is not an issue it's a side effect of the 'compiler' being intricately linked to the API. By taking the compiled CBOR and skipping the compilation step we can decouple this linking and allow us to migrate the API while maintaining backward compatibility. Note however we need to consider how to create Datum and to a lesser extent redeemers (redeemers are usually much simpler that Datums). This is easily overcome if we use Inline Datums on our script outputs.*

**Revision 12-Apr-2023: Many thanks "Giovanni [EASY1]#2633" from Discord (https://twitter.com/CryptoJoe101)!

  • Fix code issues - Sorry!