メインコンテンツまでスキップ

Accessing CIP25 data from Plutus

· 約7分

Cover image

We've successfully created a Cardano smart contract that succeeds in reading Cardano NFT (CIP25) metadata directly (no oracles or trusted sources!), which unlocks a new range of use-cases from dynamic NFTs, NFT AMMs and new interoperability standards.

We've open sourced the smart contract, and provide this blog post as an dive into the approach behind it.

Background

Almost all NFTs in Cardano follow a standard called CIP25, which defines how NFT info (name, image URL, etc.) should be defined in transaction metadata.

However, there exists one large historic problem: like transaction metadata is not directly accessible from Plutus, so dApps couldn't refer to the metadata for dApp logic, making it hard to support use-case with requiring stateful NFTs like games or NFT AMMs.

The fact that it isn't accessible from smart contracts is limiting for use-cases like gaming!

We want NFTs that ・evolve dynamically based on user actions ・can be traded with good liquidity (AMMs)

And we want to do this in a decentralized way (no centralized web 2.5 games)

This may sound strange, but this is a common problem in Ethereum (EVM) as well. In ERC721, metadata is not accessible from contracts unless extra work is done by the author. This similarly makes it hard to have dynamic NFTs or things like AMM DEXs in EVM as well, as the metadata may not be accessible from Solidity.

In EVM land, it's possible to solve this problem while still leveraging ERC721 either by

  • adding complexity in the contract (ex: Loot)
  • with oracles

In Cardano, this was solved by a new standard: CIP68, which relies on datums instead of transaction metadata (which can be references from Plutus contracts)

The CIP68 solution, although clever, has some issues:

  • Many tools only support CIP25
  • It double the number of UTXOs (2x min UTXO cost)
  • It uses Plutus datums (costlier than tx metadata)

Accessing transaction metadata from Plutus

But what if we could access tx metadata (CIP25 & more) directly from Plutus? It turns out you can using a clever trick: although Plutus cannot access metadata directly, but can access the tx hash (which contains the metadata!)

To see how this is useful, suppose you have a Plutus smart contract. It knows the final tx hash, so to prove the tx contains certain metadata, you just need to

  1. provide every other field
  2. check the hash matches

Fortunately, many fields are duplicated between the tx body and the Plutus context

So how can we get the remaining fields accessible to Plutus? It's tempting to simply include them in the redeemer (the standard way to pass in arbitrary data to a Plutus smart contract call). However, this isn't sufficient by itself because one of the fields of a transaction that isn't natively accessible from Plutus is the script data hash which itself is calculated from the redeemer value (so trying to include it inside the redeemer is recursive). Similarly, we can't use datums to pass this information either, as the datum is also part of the script data hash.

Fortunately, there is another way of passing data to a Plutus script that doesn't modify the script data hash: asset names! More concretely, assets in Cardano can have arbitrary names of up to 32 bytes, and the assets being spent in a transaction are accessible from Plutus. This means that, to break the recursive definition of script data hash, we can allow users to mint a token that encodes the script data hash as its token name. This solution create an extra min utxo cost, but the token is short-lived, so it can be reclaimed by the user later by burning the temporary token.

With that final trick, we're now able to write our Aiken smart contract that reconstructs the full transaction body onchain, allowing us to verify the that the transaction metadata asserts by a user really does match the data in the transaction!

Trade-offs and issues

Of course, every solution comes with trade-offs and issues. For ours,

  • Fees: Transactions pay a higher initial transaction fee. Fortunately, in our experience, this is only ~1 ADA. This may sound like a lot, but it's comparable to CIP68! (remember for CIP68 you need 2 UTXOs, and the min UTXO value is 1 ADA, so it requires at least 1 ADA in overhead). However, in the future, there are optimizations that could be made on the CIP68 concept to lower the overhead of references to make it more competitive.
  • Size limitation: Transactions in Cardano have a maximum size (8 KBs at the time of writing). Given our solution requires you to include the metadata your validation transaction (on top of everything else), it is possible you have a case where the original CIP25 metadata fit inside the tx size limit, but a transaction using out technique to validate the metadata doesn't.
  • Serialization format: Transactions in Cardano are encoded in CBOR which can have multiple equivalent representations. Therefore, Plutus may need to know the specific encoding used for a specific transaction in order to re-create it (see cddl-codegen if you want to learn more about this)
  • Awkward token: Our solution requires users to mint a token that is short-lived and simply burned after. Although the overhead cost of this is minimal, it's extra user & developer complexity
  • Fragility: As Cardano is an evolving protocol, there is no guarantee that future hardforks will not make a breaking change to the transaction binary format. Historically, hardforks for Cardano have always had backwards-compatible transaction formats specifically to avoid this kind of issue.
  • Updatability: CIP25, according to the specification, is updatable by posting new CIP25 metadata for your NFT collection. Our technique has no way of differentiating old data from new data, so this must be handled with care. Additionally, using our technique to power this kind of upgradability (think: a game where a contract checks the CIP25 metadata, looks at XP trait, increases the XP trait by 5, and submits new CIP25 metadata to override it) has some inefficiency as you may have to repeat the full CIP25 payload instead of just the difference. In both these cases, we do have future work to address this (follow for a future blog post!), but also our technique can be combined with new standards and ideas to tackle these problems.

However, given the benefits of this approach, we believe these tradeoffs are worth it for certain types of projects, including ours.

In fact, this idea is similar conceptually to the OP_CAT proposal in Bitcoin that enables accessing the "context" a Bitcoin transaction by constructing a tx body on chain using Bitcoin script op codes, and then checking the hash is the same hash of the tx by checking the signer signed that hash (and a bit of elliptic curve point magic). For Bitcoiners this opens up the possibility of checking that another input or output existed in that transaction using bitcoin script.

Trying it yourself

The code is all on Github free for anybody to use! Why is it embedded inside a Merkle tree management contract? That will have to be for a follow-up blog post 👀

You can find the code for the Aiken contracts here.

Credits to the following engineers for the work on this feature:

Learn more about Paima

Want to build your own game with Paima Engine? There's never been a better time to get started!


Want to just follow along? Find us on social media: