Interrealm Specification
Introduction
All modern popular programming langauges are designed for a single programmer user. Programming languages support the importing of program libraries natively for components of the single user's program, but this does not hold true for interacting with components of another user's (other) program. Gno is an extension of the Go language for multi-user programming. Gno allows a massive number of programmers to iteratively and interactively develop a single shared program such as Gno.land.
The added dimension of the program domain means the language should be extended to best express the complexities of programming in the inter-realm (inter-user) domain. In other words, Go is a restricted subset of the Gno language in the single-user context. (In this analogy client requests for Go web servers don't count as they run outside of the server program).
Realm Write Access
Objects that are directly or indirectly reachable (referenced) from the realm package's global variables (and are not already associated with another realm) are said to reside in the realm (memory space).
An object can only be mutated if the object resides in the same realm as the current realm in the Gno Machine's execution context.
Go's language rules for value access through selector/index expressions are the
same within the same realm, but exposed values through selector/index
expressions are read-only when performed by an external realm; a realm cannot
directly modify another realm's objects. Thus a Gno package's global variales
even when exposed (e.g. var MyGlobal int = 1
) is safe from external
manipulation (e.g. import "realm"; realm.MyGlobal = 2
). For users to
manipulate them a function or method must be provided.
Realm crossing occurs when a function is called with the Gno cross(fn)(...)
syntax.
package main
import "gno.land/r/alice/realm1"
func main() {
bread := cross(realm1.MakeBread)("flour", "water")
(In Linux/Unix operating systems user processes can cross-call into the kernel by calling special syscall functions, but user processes cannot directly cross-call into other users' processes. This makes the GnoVM a more complete multi-user operating system than traditional operating systems)
Besides explicit realm crossing via the cross(fn)(...)
Gno syntax, implicit
realm crossing occurs when calling a method of a receiver object stored in an
external realm. Implicitly crossing into (borrowing) a receiver object's
storage realm allows the method to directly modify the receiver as well as all
other objects directly reachable from the receiver stored in the same realm as
the receiver. Unlike explicit crosses, implicit crosses do not shift or
otherwise effect the current realm context; std.CurrentRealm()
does not
change unless a method is called like cross(receiver.Method)(args...)
.
Realms hold objects in residence and they also have a Gno address to send and receive coins from. Coins can only be spent from the current realm context.
Realm Boundaries
A realm boundary is defined as a change in realm in the call frame stack
from one realm to another, whether explicitly crossed with cross(fn)()
or implictly borrow-crossed into a different receiver's storage realm.
A realm may cross into itself with an explicit cross-call.
When returning from a realm boundary, all new reachable objects are assigned object IDs and stored in the current realm, ref-count-zero objects deleted (full "disk-persistent cycle GC" will come after launch) and any modified ref-count and Merkle hash root computed. This is called realm finalization.
Readonly Taint Specification
otherrealm.Foo
is a direct selector expression so the value is tainted
with the N_Readonly
attribute.
Same for externalobject.FieldA
where externalobject
resides in an external
realm (as compared to the current realm context).
Same for externalobject[0]
, direct index expressions also taint the resulting
value with the N_Readonly
attribute.
The readonly taint follows any subsequently derived values and cannot be overcome.
The readonly taint also prohibits mutations even if the base object resides in the current realm. This protects realms against mutating objects it doesn't intend to (e.g. by an exploit where a realm's own object is passed to the same realm's mutator function by a malicious third party, where the first object was not intended to be passed in that way).
Objects returned from functions or methods are not readonly tainted. So if
func (eo object) GetA() any { return eo.FieldA }
then externalobject.GetA()
returns an object that is not tainted. The return object's fields would still
be protected from external realm direct modification, but the return object
could be passed back to the realm for mtuation; or the object may be mutated
through its own methods.
cross(fn)()
and crossing()
Specification
Gno extends Go's type system with interrealm rules. These rules can be checked during the static type-checking phase (but at the moment they are partially dependent on runtime checks).
All functions in Gno execute under a realm context as determined by the call stack. Objects that reside in a realm can only be modified if the realm context matches.
A function declared in p packages when called:
- inherits the last realm for package declared functions and closures.
- inherits the last realm when a method is called on unreal receiver.
- implicitly crosses to the receiver's resident realm when a method of the receiver is called. The receiver's realm is also called the "borrow realm".
A function declared in a realm package when called:
- explicitly crosses to the realm in which the function is declared if the
function begins with a
crossing()
statement. The new realm is called the "current realm". - otherwise follows the same rules as for p packages.
The crossing()
statement must be the first statement of a function's body.
It is illegal to use anywhere else, and cannot be used in p packages. Functions
that begin with the crossing()
statement are called "crossing functions".
A crossing function declared in a realm different than the last explicitly
crossed realm must be called like cross(fn)(...)
. That is, functions of
calls that result in explicit realm crossings must be wrapped with cross()
.
std.CurrentRealm()
returns the current realm that was last explicitly crossed
to.
std.PreviousRealm()
returns the realm explicitly crossed to before that.
A crossing function declared in the same realm package as the callee may be
called normally OR like cross(fn)(...)
. When called normally there will be no
realm crossing, but when called like cross(fn)(...)
there is technically a
realm crossing and the current realm and previous realm returned are the same.
The current realm and previous realm do not depend on any implicit crossing to
the receiver's borrowed/storage realm even if the borrowed realm is the last
realm of the call stack equal to m.Realm
. In other words std.CurrentRealm()
may be different than m.Realm
(the borrow realm) when a receiver is called on
a foreign object.
Calls of methods on receivers residing in realms different than the current
realm must not be called like cross(fn)(...)
if the method is not a
crossing function itself, and vice versa. Or it could be said that implicit
crossing is not real realm crossing. (When you sign a document with someone
else's pen it is still your signature; signature:pen :: current:borrowed.
A crossing method declared in a realm cannot modify the receiver if the object resides in a different realm. However not all methods are required to be crossing methods, and crossing methods may still read the state of the receiver (and in general anything reachable is readable).
New unreal objects reachable from the borrowed realm (or current realm if there
was no method call that borrowed) become persisted in the borrowed realm (or
current realm) upon finalization of the foreign object's method (or function).
(When you put an unlabeled photo in someone else's scrapbook the photo now
belongs to the other person). In the future we will introduce an attach()
function to prevent a new unreal object from being taken.
MsgCall can only call (realm) crossing functions.
MsgRun will run a file's main()
function in the user's realm and may call
both crossing functions and non-crossing functions.
A realm package's initialization (including init()
calls) execute with current
realm of itself, and it std.PreviousRealm()
will panic unless the call stack
includes a crossing function called like cross(fn)(...)
.
cross
and crossing
Design Goals
P package code should behave the same even when copied verbatim in a realm package.
Realm crossing with respect to std.CurrentRealm()
and std.PreviousRealm()
is important enough to be explicit and warrants type-checking.
A crossing function of a realm should be able to call another crossing function of the same realm without necessarily explicitly crossing realms.
Sometimes the previous realm and current realm must be the same realm, such as when a realm consumes a service that it offers to external realms and users.
A method should be able to modify the receiver and associated objects of the same borrowed realm.
A method should be able to create new objects that reside in the same realm by default in order to maintain storage realm consistency and encapsulation and reduce fragmentation.
In the future an object may be migrated from one realm to another when it loses all references in one realm and gains references in another. The behavior of the object should not change after migration because this type of migration is implicit and generally not obvious without more language features.
Code declared in p packages (or declared in "immutable" realm packages) can help different realms enforce contracts trustlessly, even those that involve the caller's current realm. Otherwise two mutable (upgradeable) realms cannot export trust unto the chain because functions declared in those two realms can be upgraded.
Both crossing()
and cross(fn)(...)
statements may become special syntax in
future Gno versions.
attach()
panic()
and revive(fn)
panic()
behaves the same within the same realm boundary, but when a panic
crosses a realm boundary (as defined in Realm
Finalization) the Machine aborts the program. This is
because in a multi-user environment it isn't safe to let the caller recover
from realm panics that often leave the state in an invalid state.
This would be sufficient, but we also want to write our tests to be able
to detect such aborts and make assertions. For this reason Gno provides
the revive(fn)
builtin.
abort := revive(func() {
cross(func() {
crossing()
panic("cross-realm panic")
})
})
abort == "cross-realm panic"
revive(fn)
will execute 'fn' and return the exception that crossed
a realm finalization boundary.
This is only enabled in testing mode (for now), behavior is only partially
implemented. In the future revive(fn)
will be available for non-testing code,
and the behavior will change such that fn()
is run in transactional
(cache-wrapped) memory context and any mutations discarded if and only if there
was an abort.
TL;DR: revive(fn)
is Gno's builtin for STM (software transactional memory).
Application
P package code cannot contain crossing functions, nor use crossing()
. P
package code also cannot import R realm packages. But code can call named
crossing functions e.g. those passed in as parameters.
You must declare a public realm function to be crossing()
if it is intended to
be called by end users, because users cannot MsgCall non-crossing functions
(for safety/consistency) or p package functions (there's no point).
Utility functions that are a common sequence of non-crossing logic can be offered in realm packages as non-crossing functions. These can also import and use other realm utility non-crossing functions; whereas p packages cannot import realm packages at all. And convenience/utility functions that are being staged before publishing as permanent p code should also reside in upgradeable realms.
Generally you want your methods to be non-crossing. Because they should work for everyone. They are functions that are pre-bound to an object, and that object is like a quasi-realm in itself, that could possibly reside and migrate to other realms. This is consistent with any p code copied over to r realms; none of those methods would be crossing, and behavior would be the same; stored in any realm, mostly non-crossing methods that anyone can call. Why is a quasi-realm self-encapsulated Object in need to modify the realm in which it is declared, by crossing? That's intrusive, but sometimes desired.
You can always cross-call a method from a non-crossing method if you need it.
Implementation for std.CurrentRealm()
and std.PreviousRealm()
are defined
in stdlibs/std/native.gno/go
and related files in the directory, while
overrides for testing are defined in testing/stdlibs/std/std.gno/go
. All
stdlibs functions are available unless overridden by the latter.
std.CurrentRealm()
shifts to std.PreviousRealm()
if and only if a function
is called like cross(fn)(...)
.
MsgCall
MsgCall may only call crossing functions. This is to prevent potential confusion for non-sophisticated users. Non-crossing calls of non-crossing functions of other realms is still possible with MsgRun.
// PKGPATH: gno.land/r/test/test
func Public() {
crossing()
// Returns (
// addr:<origin_caller>,
// pkgpath:""
// ) == std.NewUserRealm(origin_caller)
std.PreviousRealm()
// Returns (
// addr:<derived_from "gno.land/r/test/test">,
// pkgpath:"gno.land/r/test/test"
// ) == std.NewCodeRealm("gno.land/r/test/test")
std.CurrentRealm()
// Already in gno.land/r/test/test realm,
// no need to cross unless the intent
// is to call AnotherPublic() as a consumer
// in which case cross(AnotherPublic)() needed.
AnotherPublic()
}
func AnotherPublic() {
crossing()
...
}
MsgRun
// PKGPATH: gno.land/r/g1user/run
import "gno.land/r/realmA"
func main() {
// There is assumed to be in "frame -1"
// a crossing from UserRealm(g1user) to
// CodeRealm(gno.land/r/g1user/run) before
// main() is called, so crossing() here
// is redundant.
// crossing()
// Returns (
// addr:g1user,
// pkgpath:""
// ) == std.NewUserRealm(g1user)
std.PreviousRealm()
// Returns (
// addr:g1user,
// pkgpath:"gno.land/r/g1user/run"
// ) == std.NewCodeRealm("gno.land/r/g1user/run")
std.CurrentRealm()
realmA.PublicNoncrossing()
cross(realmA.PublicCrossing)()
}
Notice in gnovm/pkg/gnolang/misc.go
, the following:
// For keeping record of package & realm coins.
// If you need the bech32 address it is faster to call DerivePkgBech32Addr().
func DerivePkgCryptoAddr(pkgPath string) crypto.Address {
b32addr, ok := IsGnoRunPath(pkgPath)
if ok {
addr, err := crypto.AddressFromBech32(b32addr)
if err != nil {
panic("invalid bech32 address in run path: " + pkgPath)
}
return addr
}
// NOTE: must not collide with pubkey addrs.
return crypto.AddressFromPreimage([]byte("pkgPath:" + pkgPath))
}
func DerivePkgBech32Addr(pkgPath string) crypto.Bech32Address {
b32addr, ok := IsGnoRunPath(pkgPath)
if ok {
return crypto.Bech32Address(b32addr)
}
// NOTE: must not collide with pubkey addrs.
return crypto.AddressFromPreimage([]byte("pkgPath:" + pkgPath)).Bech32()
}
These function names are distinct from what is available in Gno
from stdlibs/std/crypto.gno
:
// Returns a crypto hash derived pkgPath, unless pkgPath is a MsgRun run path,
// in which case the address is extracted from the path.
func DerivePkgAddr(pkgPath string) Address {
addr := derivePkgAddr(pkgPath) <-- calls gno.DerivePkgBech32Addr()
return Address(addr)
}
std.DerivePkgAddr("gno.land/r/name123/realm")
- bech32 from hash(path)std.DerivePkgAddr("gno.land/r/g1user/run")
- bech32 substring "g1user"
Therefore in the MsgRun file's init()
function the previous realm and current
realm have different pkgpaths (the origin caller always has empty pkgpath) but
the address is the same.
MsgAddPackage
During MsgAddPackage std.PreviousRealm()
refers to the package deployer both
in global var decls as well as inside init()
functions. After that the
package deployer is no longer provided, so packages need to remember the
deployer in the initialization phase if needed.
// PKGPATH: gno.land/r/test/test
func init() {
// Returns (
// addr:<origin_deployer>,
// pkgpath:""
// ) == std.NewUserRealm(origin_deployer)
// Inside init() and global var decls
// are the only time std.PreviousRealm()
// returns the deployer of the package.
// Save it here or lose it forever.
std.PreviousRealm()
// Returns (
// addr:<origin_deployer>,
// pkgpath:"gno.land/r/test/test"
// ) == std.NewCodeRealm("gno.land/r/test/test")
std.CurrentRealm()
}
// Same as in init().
var _ = std.PreviousRealm()
// PKGPATH: gno.land/r/g1user/run
func init() {
// Returns (
// addr:g1user,
// pkgpath:""
// ) == std.NewUserRealm(g1user)
std.PreviousRealm()
// Returns (
// addr:g1user,
// pkgpath:"gno.land/r/g1user/run"
// ) == std.NewCodeRealm("gno.land/r/g1user/run")
std.CurrentRealm()
}
The same applies for p package initialization. Initialization and tests are the
only times that std.CurrentRealm()
will return a p package path that starts
with "/p/" instead of "/r/". The package is technically still mutable during
initialization.
Testing overrides with stdlibs/testing
The gnovm/tests/stdlibs/testing/context_testing.gno
file provides functions
for overriding frame details from Gno test code.
testing.SetRealm(std.NewUserRealm("g1user"))
is identical to
testing.SetOriginCaller("g1user")
. Both will override the Gno frame to make it
appear as if the current frame is the end user signing with a hardware signer.
Both will also set ExecContext.OriginCaller
to that user. One of these will
become deprecated.
Gno test cases with _test.gno
like TestFoo(t *testing.T)
// PKGPATH: gno.land/r/user/myrealm
package myrealm
import (
"std"
"stdlibs/testing"
)
func TestFoo(t *testing.T) {
// At first OriginCaller is not set.
// Override the OriginCaller.
testing.SetRealm(std.NewUserRealm("g1user"))
// Identical behavior:
testing.SetOriginCaller("g1user")
// This panics now: seeking beyond the overridden origin frame:
// std.PreviousRealm()
// Simulate g1user cross-calling Public().
// Produce a new frame to override
func() {
testing.SetRealm(std.SetCodeRealm("gno.land/r/user/myrealm"))
std.PreviousRealm() // "g1user", ""
std.CurrentRealm() // bech32(hash("gno.land/r/user/myrealm")), "gno.land/r/user/myrealm"
Public(...) // already in "gno.land/r/user/myrealm"
}()
// The following is identical to the above,
// but not possible in p packages which
// cannot import realms.
cross(Public)(...)
}
Gno filetest cases with _filetest.gno
// PKGPATH: gno.land/r/test/test
package test
import (
"std"
"stdlibs/testing"
"gno.land/r/user/myrealm"
)
func init() {
// XXX Frame not found, there is no deployer for filetests.
std.PreviousRealm()
// Returns (
// addr:std.DerivePkgAddr("gno.land/r/test/test")
// pkgpath:"gno.land/r/test/test"
// ) == std.NewCodeRealm("gno.land/r/test/test")
std.CurrentRealm()
}
func main() {
// There is assumed to be in "frame -1"
// a crossing from UserRealm(g1user) to
// CodeRealm(gno.land/r/test/test) before
// main() is called, so crossing() here
// is redundant.
// crossing()
// Returns (
// addr:g1user,
// pkgpath:""
// ) == std.NewUserRealm(g1user)
std.PreviousRealm()
// Returns (
// addr:g1user,
// pkgpath:"gno.land/r/test/test"
// ) == std.NewCodeRealm("gno.land/r/test/test")
std.CurrentRealm()
// gno.land/r/test/test cross-calling
// gno.land/r/user/myrealm:
cross(myrealm.Public)(...)
}
// Output:
// XXX
Future Work
std.SetOriginCaller()
should maybe be deprecated in favor of
std.SetRealm(std.NewUserRealm(user))
renamed to
std.SetRealm(std.NewOriginRealm(user))
.
std.SetRealm(std.NewCodeRealm(path))
renamed to
std.SetRealm(std.NewPackageRealm(path))
.