About the autheticity of Datum

As we know, anyone can send arbitrary Datum associated with an TxOut to a script address. A Validator is not involved. The result is a UTxO with Datum sitting at a script address. A subsequent Tx can use that UTxO+Datum and present it to a Validator.

At this point the Validator must fundamentally assume that the presented Datum may not be authentic (i.e. it may not have resulted from a valid operation). Let’s further assume, to validate the authenticity of a Datum it is sufficient to check the signatures of the Tx that produced that Datum, which must happen in on-chain code.

TxInfo gives me a list of TxInInfo from which I can get the TxOutRef, but not the Tx that contained the TxOut - it seems to be a dead end.

Perhaps I’m missing something - how can a Validator verify that a given UTxO+Datum was not injected accidentally/maliciously?

CrossRef: Discord

1 Like

Afaik, it cannot. Unless datum (redeemer to be precise) has some fields that can be validated in the script itself, means like filter out the malicious/accidental UtxOs for validation somehow.
For example an other input that must contain some specified address hardwired in the script itself. So, in that case you’re sure that it’s authenticated.

You cannot have that luxury, as validation is on-chain only operation in which you can have access only to the context, UtxOs and nothing else (i.e. no ledger state access).

Though, it could have changed by the time, as I do not follow Plutus development for long (though I do watch the Plutus pioneer’s lectures).

I think, the NFT like solution what Lars explained in the lecture #5 is a good solution for state tracking.

1 Like

Yes, I believe that you can guard against a datum in a malicious UTxO by only trusting a datum when the UTxO contains a specific native token. The token or NFT acts like a credential.

Yeah, that is why I mentioned NFTs above.

1 Like

Yes, always including an NFT or always signing the Datum would probably solve that issue. But then you could say that any Datum that isn’t signed or doesn’t contain an NFT is illegal because it opens a (massive) attack vector.

It is much harder to weed out bad UTxO in retrospect, which shouldn’t have been there in the first place. To me, it seems wrong to first accept arbitrary UTxO which then have to be validated in a much more convoluted way at spend time.

The more general problem is two fold (AFAICS). [Disclaimer: I’m not a Plutus expert at all - just a Pioneer ;-)]

#1 Plutus does not validate Tx that only send value to a script address - it only validates those Tx which spent value locked by a script address.

#2 Because of #1 a validator must assume that any given UTxO was fabricated and did not origin from a Tx that was previously validated. To make it worse, the on-chain API might not provide sufficient information to validate every UTxO in retrospect (i.e. access the full Tx context that every UTxO came from)

My understanding is that the UTXO carries the Datum hash, not the Datum itself. The Datum must be presented by the redeemer. Therefore, it should suffice that the data structure used for the datum contains a random field that only a legitimate redeemer can know.

1 Like

Yes, @waldmops there must be some piece of shared secret or unique item between the validator and every party that is allowed to produce Datum. However, all of these approaches seem like hacks to me. Would it not be better to also validate Tx that have the script address as a target? In that way, a validator could prevent invalid UTxO to get created in the first place.

I do not think so. This is by design. Validation only occurs when spending from script and not when paying to script and that makes sense to me.

As validation means unlocking the ADA resides in the scriptaddress based input you want to spend.

Yes, but I am not sure what changes have been made in the design.
As I created some diagram for the changes a couple of months ago but not sure how accurate it’s atm as I really do not follow the development anymore.

By design as I mentioned previously.

On-chain validaton does not require anything but what I mentioned above. So, I think where confusion comes.

  1. Any user needs to create a transaction (off-chain, has access to the ledger state) and that
  2. tx must be validated by every individual nodes (on-chain, do not have access to the ledger state).

These are very 2 different things. Second means you are only allowed to spend all ADA on the script address’ UtxO only (as one or more inputs). Script addresses similar to pubkey addresses can have multiple UtxOs i.e. in an UtxO the address and the value can be the same but the UtxO will be very different. So, I do not really think that there is an issue.

For a simple example to understand this:
There is a policy where the I send a money /w a simple datum that contains an account address, means only that account address can retrieve the money.

Step 1. I create a tx /w an script address and value output, and account address in the datum.
E.g. Utxo1 = [(sa1, 100 ADA, datumhash)]
Step 2. Only that one who has that account address private/signing key can spend that UtxO1 (so, I the tx crator control how much he can spend and he cannot spend all the ADA on the same address but in different utxos).

So, you need to sent money to that address and and optional datum, and you specify the datum (the one who send money to it). This means this is the initial state set by me that I controll the money I send to the scriptaddress and control who can spend it (nobody or everybody end between as an example).

So you cannot inject anything as unless you send something to into that address which will became an individual UtxO.

Hope, it clears the things a bit.

So, you as an adversarial must send something to that script address and you can only control that newly UtxO and not any others that have the same script address.

Here is some code to illustrate my point …

The contract uses a very simple counter as Datum. On every grab the validator checks these simple conditions …

A client app would first try to increment the counter and grab the 100 ADA award, if that fails it would send another Tx that just increments the counter. Like so …

image

All goes well and on the fifth grab, the payout to the caller actually happens …

Slot 00001: *** CONTRACT LOG: "Give Tx"
Slot 00001: *** CONTRACT LOG: "-------"
Slot 00001: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = af5e6d25b5ecb26185289a03d50786b7ac4425b21849143ed7e18bcd70dc4db8, txOutRefIdx = 6}"
Slot 00001: *** CONTRACT LOG: " -> Nothing"
Slot 00001: *** CONTRACT LOG: " TxOut {mytxAddress = PubKeyCredential 21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9, mytxValue = [(,\"\",99999490)], mytxDatum = Nothing}"
Slot 00001: *** CONTRACT LOG: " TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 0})}"
Slot 00001: TxnValidate 22ff289f00153aa0e4554d470e8f6aeff5f7438715cb40e553a7c7fb1139659d
Slot 00001: SlotAdd Slot 2

Slot 00002: *** CONTRACT LOG: "Grab Tx"
Slot 00002: *** CONTRACT LOG: "-------"
Slot 00002: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 22ff289f00153aa0e4554d470e8f6aeff5f7438715cb40e553a7c7fb1139659d, txOutRefIdx = 0}"
Slot 00002: *** CONTRACT LOG: " -> Nothing"
Slot 00002: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 22ff289f00153aa0e4554d470e8f6aeff5f7438715cb40e553a7c7fb1139659d, txOutRefIdx = 1}"
Slot 00002: *** CONTRACT LOG: " -> Just TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 0})}"
Slot 00002: *** CONTRACT LOG: " TxOut {mytxAddress = PubKeyCredential 21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9, mytxValue = [(,\"\",99999480)], mytxDatum = Nothing}"
Slot 00002: *** CONTRACT LOG: " TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 1})}"
Slot 00002: TxnValidate 85066a73a065ae019ba87cc9da1058249191232241b032f93005b9818df6aeb2
Slot 00002: SlotAdd Slot 3

Slot 00003: *** CONTRACT LOG: "Grab Tx"
Slot 00003: *** CONTRACT LOG: "-------"
Slot 00003: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 85066a73a065ae019ba87cc9da1058249191232241b032f93005b9818df6aeb2, txOutRefIdx = 0}"
Slot 00003: *** CONTRACT LOG: " -> Nothing"
Slot 00003: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 85066a73a065ae019ba87cc9da1058249191232241b032f93005b9818df6aeb2, txOutRefIdx = 1}"
Slot 00003: *** CONTRACT LOG: " -> Just TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 1})}"
Slot 00003: *** CONTRACT LOG: " TxOut {mytxAddress = PubKeyCredential 21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9, mytxValue = [(,\"\",99999470)], mytxDatum = Nothing}"
Slot 00003: *** CONTRACT LOG: " TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 2})}"
Slot 00003: TxnValidate 4a102d725c6ac48e4fe4c903b93cd140015d6de92c60ee32a345c085f22e153f
Slot 00003: SlotAdd Slot 4

Slot 00004: *** CONTRACT LOG: "Grab Tx"
Slot 00004: *** CONTRACT LOG: "-------"
Slot 00004: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 4a102d725c6ac48e4fe4c903b93cd140015d6de92c60ee32a345c085f22e153f, txOutRefIdx = 0}"
Slot 00004: *** CONTRACT LOG: " -> Nothing"
Slot 00004: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 4a102d725c6ac48e4fe4c903b93cd140015d6de92c60ee32a345c085f22e153f, txOutRefIdx = 1}"
Slot 00004: *** CONTRACT LOG: " -> Just TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 2})}"
Slot 00004: *** CONTRACT LOG: " TxOut {mytxAddress = PubKeyCredential 21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9, mytxValue = [(,\"\",99999460)], mytxDatum = Nothing}"
Slot 00004: *** CONTRACT LOG: " TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 3})}"
Slot 00004: TxnValidate 72e645800e499d09aea88b6b2f38e92fe90b93c415aabfb70abda193933863ec
Slot 00004: SlotAdd Slot 5

Slot 00005: *** CONTRACT LOG: "Grab Tx"
Slot 00005: *** CONTRACT LOG: "-------"
Slot 00005: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 72e645800e499d09aea88b6b2f38e92fe90b93c415aabfb70abda193933863ec, txOutRefIdx = 0}"
Slot 00005: *** CONTRACT LOG: " -> Nothing"
Slot 00005: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 72e645800e499d09aea88b6b2f38e92fe90b93c415aabfb70abda193933863ec, txOutRefIdx = 1}"
Slot 00005: *** CONTRACT LOG: " -> Just TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 3})}"
Slot 00005: *** CONTRACT LOG: " TxOut {mytxAddress = PubKeyCredential 21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9, mytxValue = [(,\"\",99999450)], mytxDatum = Nothing}"
Slot 00005: *** CONTRACT LOG: " TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 4})}"
Slot 00005: TxnValidate 0d240247c0b04ff9e620f35ecef5a54bc0478b48977b6394e46ed039c863840b
Slot 00005: SlotAdd Slot 6

Slot 00006: *** CONTRACT LOG: "Grab Tx"
Slot 00006: *** CONTRACT LOG: "-------"
Slot 00006: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 0d240247c0b04ff9e620f35ecef5a54bc0478b48977b6394e46ed039c863840b, txOutRefIdx = 0}"
Slot 00006: *** CONTRACT LOG: " -> Nothing"
Slot 00006: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 0d240247c0b04ff9e620f35ecef5a54bc0478b48977b6394e46ed039c863840b, txOutRefIdx = 1}"
Slot 00006: *** CONTRACT LOG: " -> Just TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 4})}"
Slot 00006: *** CONTRACT LOG: " TxOut {mytxAddress = PubKeyCredential 21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9, mytxValue = [(,\"\",99999440)], mytxDatum = Nothing}"
Slot 00006: *** CONTRACT LOG: " TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",400)], mytxDatum = Just (MyDatum {mdCount = 5})}"
Slot 00006: *** CONTRACT LOG: " TxOut {mytxAddress = PubKeyCredential 21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9, mytxValue = [(,\"\",100)], mytxDatum = Nothing}"
Slot 00006: TxnValidate 5c24fb1425988011dc83bf0bb642d5262071fc3e4496c9c3e7f925b431ec645c
Slot 00006: SlotAdd Slot 7
Slot 00007: *** USER LOG: Reached slot Slot {getSlot = 7}
Slot 00007: *** CONTRACT LOG: "Grab with payout: True"

Now, some piece of code that I do not control plays an attack on the contract like this …

i.e. It does not consume one of the script’s UTxO. Instead it places a new UTxO with a fabricated Datum that skips the winning count. The validator will not see this Tx. In a trace it looks like this …

image

The attack is successful because the winning condition (i.e. count mod 5 == 0) is skipped and the payout never happens …

Slot 00001: *** CONTRACT LOG: "Give Tx"
Slot 00001: *** CONTRACT LOG: "-------"
Slot 00001: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = af5e6d25b5ecb26185289a03d50786b7ac4425b21849143ed7e18bcd70dc4db8, txOutRefIdx = 6}"
Slot 00001: *** CONTRACT LOG: " -> Nothing"
Slot 00001: *** CONTRACT LOG: " TxOut {mytxAddress = PubKeyCredential 21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9, mytxValue = [(,\"\",99999490)], mytxDatum = Nothing}"
Slot 00001: *** CONTRACT LOG: " TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 0})}"
Slot 00001: TxnValidate 22ff289f00153aa0e4554d470e8f6aeff5f7438715cb40e553a7c7fb1139659d
Slot 00001: SlotAdd Slot 2

Slot 00005: *** CONTRACT LOG: "Grab Tx"
Slot 00005: *** CONTRACT LOG: "-------"
Slot 00005: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 72e645800e499d09aea88b6b2f38e92fe90b93c415aabfb70abda193933863ec, txOutRefIdx = 0}"
Slot 00005: *** CONTRACT LOG: " -> Nothing"
Slot 00005: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 72e645800e499d09aea88b6b2f38e92fe90b93c415aabfb70abda193933863ec, txOutRefIdx = 1}"
Slot 00005: *** CONTRACT LOG: " -> Just TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 3})}"
Slot 00005: *** CONTRACT LOG: " TxOut {mytxAddress = PubKeyCredential 21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9, mytxValue = [(,\"\",99999450)], mytxDatum = Nothing}"
Slot 00005: *** CONTRACT LOG: " TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",500)], mytxDatum = Just (MyDatum {mdCount = 4})}"
Slot 00005: TxnValidate 0d240247c0b04ff9e620f35ecef5a54bc0478b48977b6394e46ed039c863840b
Slot 00005: SlotAdd Slot 6
Slot 00006: *** CONTRACT LOG: "Grab with payout: False"

Slot 00006: *** CONTRACT LOG: "Attack Tx"
Slot 00006: *** CONTRACT LOG: "---------"
Slot 00006: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = af5e6d25b5ecb26185289a03d50786b7ac4425b21849143ed7e18bcd70dc4db8, txOutRefIdx = 1}"
Slot 00006: *** CONTRACT LOG: " -> Nothing"
Slot 00006: *** CONTRACT LOG: " TxOut {mytxAddress = PubKeyCredential 39f713d0a644253f04529421b9f51b9b08979d08295959c4f3990ee617f5139f, mytxValue = [(,\"\",99999989)], mytxDatum = Nothing}"
Slot 00006: *** CONTRACT LOG: " TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",1)], mytxDatum = Just (MyDatum {mdCount = 6})}"
Slot 00006: TxnValidate 493badaa5c2694e5944c7cd1a8295f9e0c1a61139e0de4e99143aae1813cef9e
Slot 00006: SlotAdd Slot 7
Slot 00007: *** CONTRACT LOG: "Attack datum placed: MyDatum {mdCount = 6}"

Slot 00007: *** CONTRACT LOG: "Grab Tx"
Slot 00007: *** CONTRACT LOG: "-------"
Slot 00007: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 0d240247c0b04ff9e620f35ecef5a54bc0478b48977b6394e46ed039c863840b, txOutRefIdx = 0}"
Slot 00007: *** CONTRACT LOG: " -> Nothing"
Slot 00007: *** CONTRACT LOG: " TxIn mytxRef = TxOutRef {txOutRefId = 493badaa5c2694e5944c7cd1a8295f9e0c1a61139e0de4e99143aae1813cef9e, txOutRefIdx = 1}"
Slot 00007: *** CONTRACT LOG: " -> Just TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",1)], mytxDatum = Just (MyDatum {mdCount = 6})}"
Slot 00007: *** CONTRACT LOG: " TxOut {mytxAddress = PubKeyCredential 21fe31dfa154a261626bf854046fd2271b7bed4b6abe45aa58877ef47f9721b9, mytxValue = [(,\"\",99999440)], mytxDatum = Nothing}"
Slot 00007: *** CONTRACT LOG: " TxOut {mytxAddress = ScriptCredential f891e2dfe14f184559e4c3027cff54f3f1d956df7317ee6a5c15b035b4a6e69f, mytxValue = [(,\"\",1)], mytxDatum = Just (MyDatum {mdCount = 7})}"
Slot 00007: TxnValidate 6279ee3d8261eb67dea206407490ec97bd277a0d9f9177e26b20420f648cdb2a
Slot 00007: SlotAdd Slot 8
Slot 00008: *** CONTRACT LOG: "Grab with payout: False"
Slot 00008: *** USER LOG: Reached slot Slot {getSlot = 8}

In this particular example, the attack is successful because the off-chain grab endpoint always selects the UTxO with the highest count. To fix this, the author of the contract would first have to be aware of these kind of attacks and then proactively take countermeasures. For example, it could reject UTxO that have a count > 0 that were created by a Tx that did not consume a script UTxO. When we require to consume a UTxO, the validator kicks in and we could have rejected the attacker’s Tx.

In essence, the script assumes that there is only ever one UTxO with the counter Datum. It is conceptually a singleton, which must be enforced for example by an NFT that is attached to the UTxO that carries the singleton Datum.

Without the NFT enforcer, the attack could also be worse. For example, if the contract had decoupled UTxO that hold the payout value from a “Data UTxO” that holds the Datum but no value. The attacker could have played an UTxO with the winning count and sucessfully grabed the reward (every time until the script address runs dry).

Bottom line: there seems to be a lot of burden placed on the script author and the validator implementation. The validator impl has to account for all sorts of UTxO that could never have been created by “normal” endpoint operations.

Perhaps over time we’ll see more high level constrains, for example for “singleton datum” that somehow automatically does the NFT thing or similar.

Also, it ocurred to me that these kind of attacks would be a lot harder if every Tx related to a script address would be validated and not just those which consume UTxO already locked by the script. There may be good reason not to validate the pay-to-script case, but currently I can’t think of it.

Cheers - May this help to write better contracts

Maybe it helps if we think of the Datum as a machine state and the UTXO set as the memory in which that machine state is stored.

This means that, in order to read the state, we need to address the memory in some way. Finding the one UTXO that carries a certain NFT is a pretty elegant way to find the corresponding Datum hash.

That way we could even implement something that resembles multithreading: The state in every thread is found by following its corresponding NFT.

2 Likes

Yes, and it’s by design. You cannot have state without something attached to it, because the validator have no clue anything about the ledger state (outside world), though, the tx creator has.

It only validates based on the utxos and their optional redeem. But, you can create several different utxos and the validator would not have clue where they came from and how. That’s why Lars uses NFTs, as one of the solutions, for carrying state for his examples.

It’s not a bug it’s the feature (I would not say limitation but constraints would be a better world) of the EUTXO model. And very easy to create an NFT /w native script for this type of use cases.

In simple words, validation does not have state, if you want introduce state, you should either have access to the global ledger state (highly not recommended as it’s very expensive and every node can have slightly different state of the ledger) or using something (based on the use cases e.g. NFT) that can carry state.

You cannot have that luxury, as it would be extremely computational expensive what you cannot have in validation.

I told it already, you only validate inputs (unlock money from UtxOs i.e. spend them) and not where you send the money to. I meant it validates a lot of other things (general accounting properties, sanity checks, address check etc.) but not where do you want to send and spend the money.

This is by design.

Yes, exactly.

Interesting discussion. I recall the stateless web and then cookies came along … see similar semantics and NFTs as the Cardano cookie.