ErgoScript Design patterns

Another design pattern that I think will be very common in Ergo’s model is parallelizing actions in separate UTXOs and collecting all of the data/tokens together as inputs in a tx which folds the data/tokens down into one (or potentially more depending on the protocol) box.

This is in contrast to having 1 box that holds data pertaining to a dApp/protocol which everyone must sequentially spend in order to add their piece of data to. Instead all users can create their own box with the data in the registers (which can happen parallelized in the same block or across multiple) and once all have posted, then an accumulation transaction can be created which consumes all of them and produces a final box that has all of the data/tokens.

This is in essence how data accumulation pools work but this is a general design pattern that allows for building more efficient dApp protocols that have the potential to scale (and potentially just work better for certain use cases).

2 Likes

This is probably so well-known it doesn’t really warrant a note, but just in case, the commit/reveal pattern is useful for many contracts.

In the context of prizes for solving puzzles, e.g. in Mathematical Fun with ErgoScript, it is best to require that a commitment to the solution is mined first before the solution is revealed. Without a commitment, revealing the solution in an unconfirmed transaction is vulnerable to theft because the solution can be copied into a new transaction, and the reward sent somewhere else, assuming it gets mined.

3 Likes

In other examples, Namecoin uses this to prevent “front-running” attacks, where someone picks a domain and a miner steals that. And “rock papers scissors” in Ethereum and Ergo also use this to prevent second player from cheating.

It may be well known but people still fail to take this into account (see Section 4.2 attached paper).
Hence, good to explicitly list it out.

4 Likes

Another useful design pattern (developed upon ideas proposed by @robert) is the following.

To check that some given box (not necessarily SELF) was created after a certain height h:
Check that box.creationHeight > h (creation height is stored in in R3 and the protocol requires that creationHeight be <= actual height when the box is mined).

It seems more tricky to check that some given box was created before some height h. Probably there is some solution in ErgoScript which I have missed.

3 Likes

Another useful design pattern is to emulate executeFromVar or executeFromRegister in ErgoScript.

Currently ErgoScript does not support the above two instructions but ErgoTree does.

executeFromVar allows the box spender to specify some arbitrary script in a context var that will be executed.

executeFromRegister allows the box creator to specify some arbitrary script in a register of that box, which will be executed later on when the box is spent.

In order to emulate this in ErgoScript until it is supported at a later time, we can use the following logic:

If a box is given as part of the INPUTS array, the script in it will be executed
(Note: if a box is given as part of the DATA-INPUTS array, the script in it will not be executed)

Let us look at executeFromRegister for some given box B where we want to execute the script in R4. Instead of the script, this box will now contain a hash of the script for compactness.

The condition of the script in B will be

blake2b256(INPUTS(1).propositonBytes)) == SELF.R4[Coll[Byte]].get

2 Likes

Some contracts might have some kind of “daily withdrawal limit”. This is easily implemented by keeping track of the remaining limit within the current 24-hour period (which can be based on block height i.e. every 720 blocks). Once a new period starts, the limit can be reset.

4 Likes

Emulating context variables using registers:

In some situations, context variables may not be possible (for example, currently Appkit does not support them). We can achieve the same thing by encoding the data in context variables in a register of one of the outputs. For example, suppose we have the code

val x = getVar(0)[Int].get

This can be emulated by creating a new output box (or reusing an existing one) at some index, say 1, and storing that data in its register R4. Then the above code can be replaced by

val x = OUTPUTS(1).R4[Int].get

1 Like

To back up @scalahub’s point, we now have an example in the “ErgoScript By Example” that shows how to do this. Originally the Pin Lock script used a context variable, but then was switched to using R4 of the output: https://github.com/ergoplatform/ergoscript-by-example/blob/main/pinLockContract.md

Now that we have the above repo, I was thinking that we could also try to make many of these design patterns more approachable for new devs by making them into full fledged examples and submitting them to the repo. It’s a bit more effort on each of our parts, but I think it’d really help this become the go-to repo for all devs, and even be a good reference for any EUTXO blockchain potentially as well.

1 Like

Ensuring that each box protected by a particular script has its own output

This is important in cases where a box requires that some condition is present on a specific output. Now, if two such boxes are spent as inputs to the same transaction, without careful attention, the condition may hold for both input boxes on the same output. This can be exploited in cases where the condition is something like output.value >= SELF.value.

Preventing this class of attack can be done by ensuring that each output has a register containing a value which is unique to each input.

In the single chain swap example, this is done using:

output.R4[Coll[Byte]].get == SELF.id

It would use less space to use the input box index, which is an integer and is unique for each input box, but note that selfBoxIndex is currently broken (it always returns -1):

output.R4[Int] == selfBoxIndex

A workaround is to use:

val selfIndex = getVar(0)[Int].get

INPUTS(selfIndex).id == SELF.id &&
output.R4[Int] == selfIndex

Thanks to @kushti for the workaround suggestion.

2 Likes

A slightly more compact way to write the last suggestion is to avoid getVar altogether:

INPUTS(output.R4[Int].get).id == SELF.id
2 Likes

Proving that a box is created by a particular PK

The only thing that needs to be done is to issue a token in a transaction and put it in the output in which you want to prove you have created, also put the INPUTS(0).bytes in one of its registers!
Obviously, the token id is equal to the id of the first input of the transaction, hence a contract can make sure that the box is created by a particular person (in this case “9gAKe…”) by:

{
  val prev = INPUTS(0).R4[Coll[Byte]].get
  sigmaProp(INPUTS(0).tokens(0).id == blake2b256(prev) &&
    bytesToBox(prev).propositionBytes == PK("9gAKeRu1W4Dh6adWXnnYmfqjCTnxnSMtym2LPPMPErCkusCd6F3").propBytes)
}

bytesToBox is not a real function, please let me know the correct way to get Box from its serialized bytes (box.bytes).

The above does not necessarily make sure that INPUTS(0) is actually created by “9gAKe…” but makes sure that “9gAKe…” has participated in the chain of transactions leading to here!

The very use case that came to my mind and made me think about such a thing is for a particular person to give permission for some operation by following this design pattern (for example giving permission for spending some box to the assembler service), hence outsourcing it.
In other words, in a well-designed system in which it doesn’t allow the token to get in the hands of an attacker, we can assume that the person is giving permission for some predefined operation because no one but her could have created such a box!

3 Likes

However, how to prevent other tokens issued by that person to be interpreted as following this design pattern is kind of unsolved I think but I believe there are simple solutions to that as well.

2 Likes

To make sure some assets are distributed properly among many addresses (useful for airdrops for example):

  val properSending = {
    val imp = OUTPUTS.slice(1, OUTPUTS.size - 1)
    val appended = imp.fold(OUTPUTS(0).propositionBytes, {(x:Coll[Byte], b:Box) => {
      x ++ b.propositionBytes
    }})
    blake2b256(appended) == $outHash
  }

It both makes sure of proper outputs and preserves proper order. The same can be done for value.

2 Likes

Interesting idea. Maybe to prevent computing a large byte array before hashing, it may be more efficient to compute hashes intermittently:

val properSending = {
  val imp = OUTPUTS.slice(1, OUTPUTS.size - 2)
  val first = blake2b256(OUTPUTS(0).propositionBytes)
  val appended = imp.fold(first, {(x:Coll[Byte], b:Box) => {
    blake2b256(x ++ b.propositionBytes)
  }})
  appended == $outHash
}
1 Like

Yes. So maybe depends on the use-case. The classic memory vs computation tradeoff.

1 Like

Proof of token ownership:

This can be useful in various circumstances, for example, imagine a website that will let the user do a certain activity only if he owns the right token in his wallet. In order for the user to prove to the website that he owns the token, he can create a transaction and spend the box that contains the token (sending it to himself) and send the transaction (no broadcasting is needed) to the website, proving that he owns that token.

This was the simplest case possible; to make it practical, the website can send some randomness to the user and the user will have to put the randomness in one of the output’s registers, proving that he is providing the proof only for this specific interaction.

2 Likes

proveDlog(h, a)

In ErgoScript (and ErgoTree), proveDlog(a) is defined for the default generator which is fixed in the protocol. If we want to use a different generator h, then we can emulate this using proveDHTuple as follows

proveDlog(h, a) = proveDHTuple(h, h, a, a)

An example of this is in the Stealth Address Contract

2 Likes

Discovered by @anon_real :
As is it possible to store box in a register (and context extension of an input), it is also possible to prove that spent box had some properties. For that, we need to issue certificate NFT during spending, to check then that corresponding box had some properties (while certificate box is not spent). Used in auctions contracts by @anon_real to get original author of NFT to pay royalty.

3 Likes

Another use-case: Some Details About Ergo Auction House - #36 by anon_real

3 Likes

Kind of meta-pattern: check in a contract if it interacts with other contracts (e.g. LP with order contract, hodl pool with proxy etc) only state (outputs) related to the contract and leave checking other outputs to counter-parties. So in case of LP contract interacting with an order contract, LP would check only correctness of LP output, leaving checking user’s output(s) to the order contract.

1 Like