Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP book.gno #1224

Draft
wants to merge 28 commits into
base: master
Choose a base branch
from
Draft
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
379 changes: 379 additions & 0 deletions examples/gno.land/p/jaekwon/book/book.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,379 @@
package book

/*

GOALS:

1. Create a Book (aka List) of text,
that is owned by a Book of Persons.

2. Allow a Book of Persons to split/fork.

*/

// XXX open questions
// * Do we want a directory?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we need an equivalent of std.Address specifically for objects. The directory will function as the object catalog, similar to how the tm2 account module handles key-based accounts.

// * Should action memoize object/subject/args?
// * Should args be interface{}?
// - NOTE: Amino *can* encode anything registered.
// - XXX: could one pass in a pointer ref as arg?
// - not without a directory, conflicts with GC system.
// - with directory, any ObjectID() can be looked up.
// - PRO: less duplicate string conversion work
// - PRO: more flexible
// - CON: args are mutable, potentially confusing.
// - object/subject *should* be mutable.
// - CON: malleability becomes user problem.
// - so how do you sign an object? SignBytes()?
// - CON: requires some reflection call system.
//

import "gno.land/p/demo/avl"

//----------------------------------------
// Usage example

func main() {
// directory of subjects/objects.
directory := NewDirectory("test")
// action to perform.
action := Action{}

//----------------------------------------
// CASES
// Here are some usage examples.
// For simplicity all methods panic on error.

// CASE 1A: dummy "priv" account.
// Access to privAccount is like access to priv keys.
privAccount.AppendAction(action)
directory.GetObject(action.object).Receive(action)

// CASE 1B: ActionRunner with no signing
// This is just a wrapper around CASE 1A.
ar := ActionRunner(directory)
sar.RunWith(action, privAccount)
// sar.RunWith() impl:
// privAccount.AppendAction(action)
// ar.dir.GetObject(action.object).Receive(action)

// CASE 2A: public crypto key account.
// NOTE: This is NOT secure because
// anyone can call account.Authenticate(),
// but not anyone would complete the tx.
privKey := _ // imagine we have the key
signed := SignAction(action, privKey)
account.Authenticate(signed)
directory.GetObject(action.object).Receive(action)

// CASE 2B: SignedActionRunner
// SignedActionRunner embeds an ActionRunner.
sar := SignedActionRunner()
sar.Run(signed)
// sar.Run() impl:
// signed.subject.Authenticate(signed)
// sar.dir.GetObject(action.object).Receive(action)
}

//----------------------------------------
// Object

type ObjectKey string

func (key ObjectKey) Validate() {
if len(key) == "" {
panic("invalid key")
}
// XXX make sure key is alphanumeric.
}

type Object interface {
// The primary key of the object.
GetKey() ObjectKey
// Set the prmiary key.
// Panics if key exists.
SetKey(ObjectKey)
// Receive performs action on object.
// Panics if action is not permitted.
Receive(Action)
}

//----------------------------------------
// Directory

type Directory struct {
name string // name of directory
objects avl.Tree // key -> Object
auto bool // use automatic numeric keys
lastKey int64 // last auto numeric key used
prefix string // key prefix, if any
}

func NewDirectory(name string, auto bool, prefix string) *Directory {
return &Directory{
name: name,
objects: nil,
auto: auto,
lastKey: 0,
prefix: prefix,
}
}

func (dir *Directory) GetName() string {
return dir.name
}

func (dir *Directory) GetObject(key ObjectKey) Object {
obj, exists := dir.objects.Get(key)
if !exists {
panic("object not found: invalid key")
}
return obj
}

func (dir *Directory) AddObject(obj Object) {
key := ObjectKey("")
if dir.auto {
key = dir.nextKey()
obj.SetKey(key)
} else {
key = obj.GetKey()
key.ValidateBasic()
if !hasPrefix(string(key), dir.prefix) {
panic("invalid key: prefix mismatch")
}
}
updated := dir.objects.Set(string(key), obj)
if updated == true {
panic("duplicate object key")
}
}

func (dir *Directory) RemoveObject(obj Object) {
key := obj.GetKey()
old, removed := dir.objects.Remove(string(key))
if !removed {
panic("cannot remove object: not found")
}
if old != obj {
panic("cannot remove conflicting object")
}
}

func (dir *Directory) RemoveObjectByKey(key ObjectKey) {
if !hasPrefix(key, dir.prefix) {
panic("invalid key: prefix mismatch")
}
_, removed := dir.objects.Remove(string(key))
if !removed {
panic("cannot remove object: not found")
}
}

func (dir *Directory) nextKey() ObjectKey {
if !dir.auto {
panic("cannot generate key: not auto keyed")
}
nextKey := dir.lastKey + 1
if nextKey < 0 {
panic("key overflow")
}
dir.lastKey = nextKey
return ObjectKey(dir.prefix + itoa(nextKey))
}

//----------------------------------------
// Book

type Book struct {
attrs Attributes
entries avl.Tree
}

func (bk *Book) Append(XXX) XXX {
XXX
}

func (bk *Book) Size() int {
XXX
}

func (bk *Book) Get(n int) XXX {
XXX
}

//----------------------------------------
// Attributes

type Attributes struct {
meta avl.Tree // catchall
author Author
// TODO
}

//----------------------------------------
// Action

// A subject acts upon an object with a verb and arguments.
// As if subject is calling object.verb(args...).
//
// The sequence is usually an incrementing number,
// but could be something else tracked by an action book.
//
// The subject and object are denoted by their respective
// paths (or URIs?).
type Action struct {
sequence string // typically an incrementing number
subject string // subject path
object string // object path
verb string // verb name
args []string // legible args
}

func (a Action) Sequence() string { return a.sequence }
func (a Action) Subject() string { return a.subject }
func (a Action) Object() string { return a.object }
func (a Action) Verb() string { return a.verb }
func (a Action) NumArgs() int { return len(a.args) }
func (a action) Arg(n int) string { return a.args[n] }

// Authorization struct is only needed for Actions that
// require cryptographic authorization, where the Action's
// subject has a pubkey to verify signatures with.
//
// Presumably once Authorization is validated (signature
// checked) the Action becomes committed, and given a index
// number.
type Authorization struct {
action Action
signatures []Signature
}

type Signature struct {
account number // or address with some extra data unknown
sequence number // or alternative to sequence
signature []byte
}

//----------------------------------------
// PrivActionBook

// This is a thought experiment to make books work for auth.
// Actions are considered authorized if appended in PrivActionBook.
//
// An PrivActionBook is meant to be owned privately by the object.
// This is similar to PrivKey in crypto; it is privileged.
//
// Actions need not necessarily be signed cryptographically
// to be authenticated in an PrivActionBook, because the test of
// authorization is merely inclusion.
//
// XXX consider:
// type CryptoActionBook struct { PubKey, PrivActionBook }
// A CryptoActionBook need not be privileged,
// perhaps anyone can append a signed action,
// XXX but how to guarantee execution?
// XXX without something like `Everything.Execute(Action)`??
//
// XXX consider:
// type ReadActionBook, a readonly ActionBook??
type PrivActionBook struct {

// All actions must have this object.
object string

// Maybe PrivActionBook *is* Book?
// not sure yet.
book *Book

// Number of actions to keep around.
capacity int

// Validates sequences based on sequenceAccum,
// which is accumulated from sequences seen.
sequenceStrategy SequenceStrategy

// Typically the last sequence value.
// The type of value depends on SequenceStrategy.
// This field allows the PrivActionBook to prune
// all previous Actions while preserving sequencing.
// XXX string or TextMarshaller() or Stringer() or?
sequenceAccum string
}

func NewPrivActionBook() *PrivActionBook {
// XXX
}

// If the action is valid, append to PrivActionBook,
// thereby making it officially authorized.
// The execution of action generally should happen
// atomically with authorization by caller.
//
// if err := pab.Append(action); err != nil {
// execute(action)
// }
func (pab *PrivActionBook) Append(action Action) error {
// XXX copy action.

// XXX check action.sequence against ab.last
// XXX if good, append and return nil
// XXX otherwise return error

// XXX match action.object with pab.object.
// XXX set action.object = nil for space.

// XXX check capacity
}

func (pab *PrivActionBook) Len() int {
return pab.book.Len()
}

func (pab *PrivActionBook) Cap() int {
return pab.book.Cap()
}

// XXX Not sure why this would be useful,
// XXX except to show clients previous actions,
// XXX but either way developers should not rely on it
// XXX for transactional logic.
func (pab *PrivActionBook) Get(idx int) Action {
// XXX fetch action from pab.book.Get()
// XXX copy action
// XXX set copy.object = pab.object
// XXX return copy
}

// XXX SequenceStragegy.Name()?
// XXX or just make enums?
// XXX Either way need to make globally unique lookup.
func (pab *PrivActionBook) SequenceStrategy() SequenceStrategy {
// XXX
}

// XXX clients will need this to sign,
// XXX especially after device reset.
func (pab *PrivActionBook) SequenceAccum() string {
// XXX
}

//----------------------------------------
// Account

// XXX incomplete.
type Account struct {
// each account has an associated pab
// XXX or, accounts have devices, each with a pab.
pab *PrivActionBook
}

//----------------------------------------
// misc

func hasPrefix(str, prefix string) bool {
if len(str) <= len(prefix) {
return false
}
return str[:len(prefix)] == prefix
}
Loading