gnolang / gno

Gno: An interpreted, stack-based Go virtual machine to build succinct and composable apps + gno.land: a blockchain for timeless code and fair open-source.
https://gno.land/
Other
893 stars 373 forks source link

META contract-contract interactions on a single chain #757

Open moul opened 1 year ago

moul commented 1 year ago

Edit, disclaimer:

Our current objective is not to introduce type-unsafe features for inter-contract calls. In Version 1, we will continue to support the existing native import system and may potentially implement asynchronous methods for sending async messages via IBC or local IBC connections.

For scenarios involving account attraction, contract-based multisigs, and proxy patterns, we should prioritize exploring new solutions that align with Version 1 constraints before attempting to adapt patterns from other ecosystems.


The aim is to provide a summary of the available options for calling contracts, as well as to summarize the investigation of new techniques.

Method Type-Safety Dynamic calls (A)synchronous Status
Import and call (default) Y N Sync ✅ Available
std.{Send,Recv} TBD Y Async 🤔 In consideration
std.Call N Y Sync Won't do for v1
std.Ctx{Set,Get} Y N Sync Won't do for v1

UPDATE: we shouldn't implement .Call or .CtxSet,Get

Current options

Idiomatic go import

The recommended approach involves importing a package or a realm and invoking an exported function, which is highly idiomatic, maintains type-safety, and offers better reliability than a micro-service relying on TCP. Furthermore, this method could enable auto-complete functionality over time.

The proposed solution is limited in that it does not support importing a dynamic package with a variable name.

import "gno.land/r/demo/users"

func usernameOf(addr std.Address) string {
    user := users.GetUserByAddress(addr)
    if user == nil {
        return ""
    }
    return user.Name()
}

Source: https://github.com/gnolang/gno/blob/master/examples/gno.land/r/demo/boards/misc.gno

Potential upcoming solutions

IBC-compatible API

The proposed enhancements include a contract-contract interface using two monodirectional channels to support asynchronous and potentially synchronous calls, with a similar API to the upcoming IBC interface. The channels could also enable external transaction triggering based on chain hooks.

At this stage, it is unclear whether the contract-contract interface would be limited to simple types only, require marshalling, or enable specifying the expected type with simple typed channels.

One of the proposed enhancements involves introducing a new std.Call/Invoke("contract-addr", "method", params...) method."

Pseudo-code:

func Process() {
    // recv
    for _, event := std.Recv() { /* handle */ }

    // send
    std.Send("gno.land/r/demo/foobar", "Method", "arg1", "arg2")
}

Related work:

New std.Call/Invoke method, for dynamic synchronous calls

Warning: user security concerns, losing type-safety

The proposed approach is similar to the IBC one (above), with the exception that it involves a new std.Call helper that is necessarily synchronous.

Pseudo-code:

import "std"

func Foo() {
    ret, err := std.Call("gno.land/r/demo/users", "GetUserByAddress", "g1foobar...")
    println(ret.(string))
}
// or
func Bar() {
    var ret struct{...}
    err := std.Call(&ret, "gno.land/r/demo/users", "GetUserByAddress", "g1foobar...")
}

Related work:

Helpers to extend the calling context

Warning: it should be avoided if we can just update libraries to take a Context argument.

Providing helpers to taint the calling context with metadata, which would enable contracts to be called as before while extending how the dependent contract views its calling graph.

The proposed method for calling contracts has similar limitations to the existing method, in that it is static. However, it adds the capability to simulate various contract-contract interactions, such as specifying that an intermediary contract should be considered as an account and store assets.

Pseudo-code:

import (
    "gno.land/r/demo/foobar"
    "gno.land/p/demo/rules"
)

func VaultExec() {
    // assertIsVaultOwner()
    std.SetContext(rules.PackageAsAccount, true)
    foobar.Transfer(...)
    std.SetContext(rules.PackageAsAccount, false)
}
// or
func VaultExec() {
    // assertIsVaultOwner()
    rules.ExecAsPkg(func() {
        foobar.Transfer(...)
    })
}

Related work:

moul commented 1 year ago

Update from https://github.com/gnolang/meetings/issues/5

We need:

jaekwon commented 1 year ago

To clarify the above, we need first to figure out how the "ABI" works for argument and return values. I suggest we just use a slice of stringified primitive values. Later after the prototype we can consider compatibility with ETH ABI, or some other byte form; while also considering how to support structures (non-primitive types).

We don't need Encode or Decode exposed in Gno. From the perspective of Gno it should be transparent how it is encoded/decoded. Maybe what we need is to use DefineNative as in stdlibs/stdlibs.go -- then in there we can stringify the arguments and queue it up. We would need a utility method that panics upon encountering non-primitive values. And later we can figure out how to improve this to support Solidity ABI or something else.

ajnavarro commented 1 year ago

Talking about the API, I like the idiomatic Go import.

Another idea trying to merge a dynamic approach with type safety, we can do something like this:


type myInterface interface {
     Method(arg string) string
}

i, ok := std.GetRealm("gno.land/r/contract/v2").(myInterface)
if !ok {
 [....]
}

out := i.Method("test")
albttx commented 1 year ago

Here is a list of the std.Get* functions and their use

std.Get*() functions what they do
GetChainID return ChainID
GetHeight return block height
GetOrigSend return Coins sent
GetOrigCaller return user address (tx.orign)
GetOrigPkgAddr return the first calling Realm
GetCallerAt given a frame index, return the caller
GetBanker return a BankerType
GetTimestamp deprecated
CurrentRealmPath return realm path (gno.land/r/example)
std.Get* potential new one What they do
GetCaller/GetSender return the previous Realm calling, if not the user
GetCallersCount (?) return the numbers of callers, currently we can't know GetCallerAt(x) limit
GetPkgAddr return the current PkgAddr
moul commented 1 year ago

GetCaller/GetSender | return the previous Realm calling, if not the user

How about GetLastCaller or GetPreviousCaller? Additionally, would a GetRealmAddr function that returns the calling realm or nil be useful?

GetCallersCount (?) | return the numbers of callers, currently we can't know GetCallerAt(x) limit

Options include using GetCallersCount, returning a struct{addr string, index int} instead of a string, or returning nil if overflow occurs.

GetPkgAddr | return the current PkgAddr

👍, This would be useful for reusable contracts with init() functions, as well as for the Render() function when creating links using the [](prefix+path) syntax.

albttx commented 1 year ago

How about GetLastCaller or GetPreviousCaller?

I understand this point, i'm fine with GetLastCaller but i prefer GetCaller for the reason that a "Caller" already express the sens of "Last" or "previous"

Additionally, would a GetRealmAddr function that returns the calling realm or nil be useful?

i don't this so, people will manage with something like

addr := std.GetRealmAddr()
if addr == nil {
   // handle this
}

So it's the same as

addr := std.GetCaller()
if addr ==  std.GetOrigCaller() {
   // handle this
}

GetPkgAddr | return the current PkgAddr

We need a better definition of the difference between a Realm and a Package. which is clear for us but could be unclear for a Gno realm dev.

Because there is "no need" that a package hold assets. Package "context" should be the realm context

moul commented 1 year ago

i don't this so, people will manage with something like...

I was proposing in addition to GetCaller, not to replace it.

Because there is "no need" that a package hold assets. Package "context" should be the realm context

In certain cases, I believe it would be prudent to determine the originating package, allowing for the white-listing of a trustworthy intermediate package, thus enabling its use by anyone.

Realm is a way more common, but both make sense IMO.

albttx commented 1 year ago

I was proposing in addition to GetCaller, not to replace it.

Yes i understand that, but since it's taking the same amount of LoC, i don't see the usage.

But i could be easy to add if someone express the need of this function :)

[...] allowing for the white-listing of a trustworthy intermediate package,

I love that idea! but what do you think putting this whitelisting inside of gno.mod ?


And do you think we should keep GetCallerAt() ? Do you have an example of when it could be needed ?

moul commented 1 year ago

For those joining late:

Our current objective is not to introduce type-unsafe features for inter-contract calls. In Version 1, we will continue to support the existing native import system and may potentially implement asynchronous methods for sending async messages via IBC or local IBC connections.

For scenarios involving account attraction, contract-based multisigs, and proxy patterns, we should prioritize exploring new solutions that align with Version 1 constraints before attempting to adapt patterns from other ecosystems.