ScorexFoundation / sigmastate-interpreter

ErgoScript compiler and ErgoTree Interpreter implementation for Ergo blockchain
MIT License
62 stars 40 forks source link

Allow construction of new objects #981

Open aslesarenko opened 3 months ago

aslesarenko commented 3 months ago

Problem

Currently there is no way in ErgoScript to create objects of pre-defined types such as AvlTree, Header, Box, etc. It is possible to introduce new objects only by deserializing from context var or from register. But this is very limited approach and more like a workaround.

Solution

kushti commented 3 months ago

What is the motivation for those constructors? Any example ? Why BigInt is in the list?

kushti commented 3 months ago

You already can do bigInt("")

For compile-time creation of objects of (AvlTree, Header, Box, BigInt, GroupElement) you can use deserializeRaw from hardcoded bytes. Creating header from fields in runtime ? Any useful example ?

aslesarenko commented 3 months ago

What is the motivation for those constructors? Any example ? Why BigInt is in the list?

The motivation is to have single ErgoTree encoding for different use cases. This is actually pretty standard way of encoding new ObjectType(...) operations on languages. We could have it from network launch along with MethodCall, but we didn't have time for this.

CreateAvlTree is one example. Another is val h = Header(...), as well as basically any other type.

BigInt maybe not the best example, as there is already byteArrayToBigInt method, but why not. If any type can have a constructor, why not BigInt?

aslesarenko commented 3 months ago

You already can do bigInt("")

This only allow constant argument, where as NewObject will allow any expressions in arguments.

aslesarenko commented 3 months ago

For compile-time creation of objects of (AvlTree, Header, Box, BigInt, GroupElement) you can use deserializeRaw from hardcoded bytes.

yes, key words are hardcoded bytes, this may be both advantage and disadvantage, depending on use case.

Creating header from fields in runtime ? Any useful example ?

kushti commented 3 months ago

I guess these constructors can be done on the compiler level anyway, so under the hood the compiler will replace a constructor with expression which is building a bytestring and then do deserializeRaw or deserializeTyped for it

Thus I am moving it to 6.x

aslesarenko commented 3 months ago

expression which is building a bytestring

So, of some arguments come from GetVar, then they should be serialized and then concatenated to the rest of the bytes? And this will happen during contract execution? Isn't it too much efforts to avoid having normal operation?

kushti commented 3 months ago

It depends on usage, I haven't seen any use-case still. Especially considering that in practice you can go the other way, so instead of constructing e.g. a box with certain fields, expect for a box being provided and then calculate predicates over its fields (which is what most of apps are doing btw)

aslesarenko commented 3 months ago

I haven't seen any use-case still.

I described the usecase where parameters can come from different sources. And the most natural way is to have constructor call. Any work around looks ugly at use, and maybe more difficult in implementation. The only thing we save here is time spent on v6.0. We can move this to v7.0, but implementing it via pure compilation+serialization is even more complex.

kushti commented 3 months ago

But where do you need for constructing e.g. box from multiple sources instead of checking a box of interest ?

Even smaller chance to find an use case for a header.

Luivatra commented 3 months ago

I have wanted to make a reusable function with an option as parameter, which works fine when feeding it the result of an option producing function, but sometimes it is an actual value and creating a Some out of that would be nice.

aslesarenko commented 3 months ago

Even smaller chance to find an use case for a header.

cmon, what kind of argument it is? Users found use cases for almost all language features I've put into the language. Even Box in a register, which was added to DataSerializer just because it was easy to do and because I wanted to have generic serializer which works for any type with descriptor, and Box was such type.

The same is here, NewObject is generic feature which will work for any type. I'm pretty sure people will find use cases.

kushti commented 3 months ago

As no use-cases provided, I can move it to 7.0 or leave for 6.x. Frontend only implementation looks promising, as it is better not to touch consensus-critical level when possible

aslesarenko commented 3 months ago

As no use-cases provided

No use case for Header, but what about AvlTree, which we started from. Why lack of usecase for Header is so critical, what about other types?

kushti commented 3 months ago

For AvlTree byte machinery is not hard:

val digest = SELF.R4[Coll[Byte]].get
val flag = Coll(0.toByte) // read-only
val keyLength = Coll(32.toByte)
val valueLength = Coll(0.toByte) // arb length

val treeBytes = digest ++ flag ++ keyLength ++ valueLength
deserialize[AvlTree](treeBytes).digest == digest

so here you have fields coming from different sources etc

the example is artificial though, as in practice (keyLength, valueLength) are coming from the same source as digest

aslesarenko commented 3 months ago

(keyLength, valueLength) are coming from the same source as digest

To your point, if they come say from a register as AvlTree, then

@contract MyContract(flags: Byte) = {
  val tree = SELF.R4[AvlTree].get
  AvlTree(tree.digest, flags, tree.keyLength, tree.valueLength).digest == digest
}

The this examples not only much easier to understand/read, but also always works, while your code is only good while values fit into single bytes, user need to take care about format, and also know how to encode.

kushti commented 3 months ago

But there is simpler solution:

@contract MyContract(flags: Byte) = {
  val tree = SELF.R4[AvlTree].get
  tree.updateOperations(flags).digest == digest
}
aslesarenko commented 3 months ago

But there is simpler solution:

Yes, that is because AvlTree have only fields. In case you compare not digest but some computable property or the result of a method call, then you have to construct an object.

kushti commented 3 months ago

Yes, that is because AvlTree have only fields. In case you compare not digest but some computable property or the result of a method call, then you have to construct an object.

updateOperations(flags) is constructing an object, and then you can call any tree method on it