From 17a2b4537b7e26b723cefb544886565c96576008 Mon Sep 17 00:00:00 2001 From: Omar Sy Date: Sat, 20 Apr 2024 17:41:33 +0200 Subject: [PATCH] feat: github oracle --- examples/gno.land/r/demo/teritori/gh/CMD.md | 44 +++ .../gno.land/r/demo/teritori/gh/account.gno | 66 +++++ .../gno.land/r/demo/teritori/gh/admin.gno | 25 ++ .../r/demo/teritori/gh/admin_test.gno | 22 ++ examples/gno.land/r/demo/teritori/gh/gno.mod | 6 + .../gno.land/r/demo/teritori/gh/oracle.gno | 54 ++++ .../r/demo/teritori/gh/oracle_test.gno | 60 ++++ .../gno.land/r/demo/teritori/gh/public.gno | 76 +++++ .../r/demo/teritori/gh/public_test.gno | 33 +++ gnovm/stdlibs/crypto/elliptic/elliptic.gno | 280 ++++++++++++++++++ 10 files changed, 666 insertions(+) create mode 100644 examples/gno.land/r/demo/teritori/gh/CMD.md create mode 100644 examples/gno.land/r/demo/teritori/gh/account.gno create mode 100644 examples/gno.land/r/demo/teritori/gh/admin.gno create mode 100644 examples/gno.land/r/demo/teritori/gh/admin_test.gno create mode 100644 examples/gno.land/r/demo/teritori/gh/gno.mod create mode 100644 examples/gno.land/r/demo/teritori/gh/oracle.gno create mode 100644 examples/gno.land/r/demo/teritori/gh/oracle_test.gno create mode 100644 examples/gno.land/r/demo/teritori/gh/public.gno create mode 100644 examples/gno.land/r/demo/teritori/gh/public_test.gno create mode 100644 gnovm/stdlibs/crypto/elliptic/elliptic.gno diff --git a/examples/gno.land/r/demo/teritori/gh/CMD.md b/examples/gno.land/r/demo/teritori/gh/CMD.md new file mode 100644 index 00000000000..80036b24de8 --- /dev/null +++ b/examples/gno.land/r/demo/teritori/gh/CMD.md @@ -0,0 +1,44 @@ +gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="https://rpc.gno.land:443" \ + -chainid="portal-loop" \ + -pkgdir="." \ + -pkgpath="gno.land/r/mikecito/gh_test_4" \ + mykey2 + +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="https://rpc.gno.land:443" \ + -chainid="portal-loop" \ + -pkgpath="gno.land/r/mikecito/gh_test_4" \ + -func="AdminSetOracleAddr" \ + -args="g1d46t4el0dduffs5j56t2razaeyvnmkxlduduuw" \ + mykey2 + +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="https://rpc.gno.land:443" \ + -chainid="portal-loop" \ + -pkgpath="gno.land/r/mikecito/gh_test_4" \ + -func="OracleUpsertAccount" \ + -args="15034695" \ + -args="omarsy" \ + -args="6h057" \ + -args="user" \ + mykey2 + +gnokey query "vm/qeval" -data='gno.land/r/mikecito/gh_test_4 +AccountByID("15034695")' -remote="https://rpc.gno.land:443" + +gnokey query "vm/qeval" -data='gno.land/r/mikecito/gh_test_4 +RenderAccount("g14mfv59k38r8k5vkevpu0lpqlqra0e9trwp3d32")' -remote="https://rpc.gno.land:443" + +gnokey query "vm/qeval" -data='gno.land/r/mikecito/gh_test_4 +Render()' -remote="https://rpc.gno.land:443" \ No newline at end of file diff --git a/examples/gno.land/r/demo/teritori/gh/account.gno b/examples/gno.land/r/demo/teritori/gh/account.gno new file mode 100644 index 00000000000..067a53ab454 --- /dev/null +++ b/examples/gno.land/r/demo/teritori/gh/account.gno @@ -0,0 +1,66 @@ +package gh + +import ( + "errors" + "std" + + "gno.land/p/demo/avl" +) + +// Account represents a GitHub user account or organization. +type Account struct { + id string + login string + name string + kind string +} + +func (a Account) ID() string { return a.id } +func (a Account) Name() string { return a.name } +func (a Account) Kind() string { return a.kind } +func (a Account) URL() string { return "https://github.com/" + a.login } +func (a Account) IsUser() bool { return a.kind == UserAccount } +func (a Account) IsOrg() bool { return a.kind == OrgAccount } + +// TODO: func (a Account) RepoByID() Repo ... + +func (a Account) Validate() error { + if a.id == "" { + return errors.New("empty id") + } + + if a.login == "" { + return errors.New("empty login") + } + if a.kind == "" || (a.kind != UserAccount && a.kind != OrgAccount) { + return errors.New("empty kind") + } + if a.name == "" { + return errors.New("empty name") + } + // TODO: validate + return nil +} + +func (a Account) Render() string { + return `{ "id": "` + a.id + `", "login": "` + a.login + `", "name": "` + a.name + `", "kind": "` + a.kind + `" }` +} + +func (a Account) String() string { + // XXX: better idea? + return a.URL() +} + +const ( + UserAccount string = "user" + OrgAccount string = "org" +) + +func AccountByID(id string) *Account { + res, ok := accounts.Get(id) + if !ok { + return nil + } + + return res.(*Account) +} diff --git a/examples/gno.land/r/demo/teritori/gh/admin.gno b/examples/gno.land/r/demo/teritori/gh/admin.gno new file mode 100644 index 00000000000..81649768ac7 --- /dev/null +++ b/examples/gno.land/r/demo/teritori/gh/admin.gno @@ -0,0 +1,25 @@ +package gh + +import ( + "std" +) + +var( + adminAddr std.Address = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" +) + + +func assertIsAdmin() { + if std.GetOrigCaller() != adminAddr { + panic("restricted area") + } +} + +func setAdminAddress(address std.Address) { + adminAddr = address +} + +func SetAdminAddress(address std.Address) { + assertIsAdmin() + setAdminAddress(address) +} \ No newline at end of file diff --git a/examples/gno.land/r/demo/teritori/gh/admin_test.gno b/examples/gno.land/r/demo/teritori/gh/admin_test.gno new file mode 100644 index 00000000000..07a6c6a979a --- /dev/null +++ b/examples/gno.land/r/demo/teritori/gh/admin_test.gno @@ -0,0 +1,22 @@ +package gh + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/testutils" +) + +func TestAssertIsAdmin(t *testing.T) { + adminAddr = "g1test1234" + randomuser := testutils.TestAddress("g1unknown_player") + defer func() { + if r := recover(); r != nil { + } + }() + std.TestSetOrigCaller(randomuser) + assertIsAdmin() + t.Fatalf("should fail because not admin") +} + diff --git a/examples/gno.land/r/demo/teritori/gh/gno.mod b/examples/gno.land/r/demo/teritori/gh/gno.mod new file mode 100644 index 00000000000..15921940fe9 --- /dev/null +++ b/examples/gno.land/r/demo/teritori/gh/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/demo/teritori/gh + +require ( + gno.land/p/demo/avl v0.0.0-latest +) + diff --git a/examples/gno.land/r/demo/teritori/gh/oracle.gno b/examples/gno.land/r/demo/teritori/gh/oracle.gno new file mode 100644 index 00000000000..840ad3b29c4 --- /dev/null +++ b/examples/gno.land/r/demo/teritori/gh/oracle.gno @@ -0,0 +1,54 @@ +package gh + +import ( + "std" + "strings" + "time" + + "gno.land/p/demo/avl" +) + +var ( + accounts avl.Tree // id -> Account + lastUpdateTime time.Time // used by the bot to only upload the diff + oracleAddr std.Address +) + +func OracleLastUpdated() time.Time { return lastUpdateTime } + +func OracleUpsertAccount(id, login, name, kind string) { + assertIsOracle() + lastUpdateTime = time.Now() + + // get or create + account := &Account{} + res, ok := accounts.Get(id) + if ok { + account = res.(*Account) + } else { + account.id = id + } + + // update fields + account.name = name + account.kind = kind + account.login = login + + if err := account.Validate(); err != nil { + panic(err) + } + + // save + accounts.Set(id, account) +} + +func AdminSetOracleAddr(new std.Address) { + assertIsAdmin() + oracleAddr = new +} + +func assertIsOracle() { + if std.GetOrigCaller() != oracleAddr { + panic("restricted area") + } +} diff --git a/examples/gno.land/r/demo/teritori/gh/oracle_test.gno b/examples/gno.land/r/demo/teritori/gh/oracle_test.gno new file mode 100644 index 00000000000..63c1ab38633 --- /dev/null +++ b/examples/gno.land/r/demo/teritori/gh/oracle_test.gno @@ -0,0 +1,60 @@ +package gh + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/testutils" +) + +func TestOracleUpsertUserIsNotOracle(t *testing.T) { + oracleAddr = "g1test1234" + var randomuser = "g1unknown_player" + user := testutils.TestAddress("user") + defer func() { + if r := recover(); r != nil { + } + }() + OracleUpsertAccount("acountID", "villaquiranm","john doe","user") + t.Fatalf("should fail because not admin") +} + +func TestOracleUpsertUserOk(t *testing.T) { + oracleAddr = "g1random" + if accounts.Size() != 0 { + t.Fatalf("Accounts is not empty") + } + now := time.Now() + + std.TestSetOrigCaller(oracleAddr) + + var randomuser = "g1unknown_player" + OracleUpsertAccount("acountID", "villaquiranm","john doe","user") + + if accounts.Size() != 1 { + t.Fatalf("User was not created") + } + + OracleUpsertAccount("acountID", "villaquiranm","john doe","user") + + if accounts.Size() != 1 { + t.Fatalf("User was created more than once") + } + + if OracleLastUpdated().Unix() < now.Unix() { + t.Fatalf("OracleLastUpdated was not changed") + } +} + +func TestAssertIsOracle(t *testing.T) { + std.TestSetOrigCaller(adminAddr) + AdminSetOracleAddr("g1random123") + defer func() { + if r := recover(); r != nil { + } + }() + assertIsOracle() + t.Fatalf("should fail because user is not oracle") +} \ No newline at end of file diff --git a/examples/gno.land/r/demo/teritori/gh/public.gno b/examples/gno.land/r/demo/teritori/gh/public.gno new file mode 100644 index 00000000000..d147395eceb --- /dev/null +++ b/examples/gno.land/r/demo/teritori/gh/public.gno @@ -0,0 +1,76 @@ +package gh + +import ( + "crypto/ed25519" + "encoding/hex" + "std" + + "gno.land/p/demo/avl" +) + +var ( + addressToAccount avl.Tree // address -> Account + guardianPublicKey string // guardian public (maybe should an array) +) + +func init() { + addressToAccount = avl.Tree{} + setAdminAddress(std.GetOrigCaller()) +} + +// Todo maybe we should gave multi guardian +func SetGuardianPublicKey(publicKey string) { + assertIsAdmin() + guardianPublicKey = publicKey +} + +func LinkAccount(accountID string, address std.Address, signature string) { + if !verifySignature(accountID, address, signature) { + panic("signature verification failed") + } + + account := AccountByID(accountID) + if account == nil { + panic("account not found") + } + + addressToAccount.Set(address.String(), account) +} + +func RenderAccount(address std.Address) string { + account, ok := addressToAccount.Get(address.String()) + if !ok { + panic("account not found") + } + + return account.(*Account).Render() +} + +func Render(address string) string { + if address != "" { + return RenderAccount(std.Address(address)) + } + str := "[" + addressToAccount.Iterate("", "", func(key string, value interface{}) bool { + account := value.(*Account) + str += account.Render() + str += "," + return false + }) + str += "]" + + return str +} + +func verifySignature(accountID string, address std.Address, signature string) bool { + publicKey, err := hex.DecodeString(guardianPublicKey) + if err != nil { + panic("invalid guardian public key") + } + + sign, err := hex.DecodeString(signature) + if err != nil { + panic("invalid signature") + } + return ed25519.Verify(publicKey, []byte(accountID+" "+address.String()), sign) +} diff --git a/examples/gno.land/r/demo/teritori/gh/public_test.gno b/examples/gno.land/r/demo/teritori/gh/public_test.gno new file mode 100644 index 00000000000..d0e53741b8f --- /dev/null +++ b/examples/gno.land/r/demo/teritori/gh/public_test.gno @@ -0,0 +1,33 @@ +package gh + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/testutils" +) + +// private key 023742D9CFA2824BCA670868E11B00125625F9AF735CA42D9305ADD030F84515 +func TestLinkAccount(t *testing.T) { + adminAddr = "g1test1234" + + user := testutils.TestAddress("user") + admin := testutils.TestAddress("admin") + + std.TestSetOrigCaller(admin) + setAdminAddress(admin) + AdminSetOracleAddr(admin) + SetGuardianPublicKey("E921E29BC2F358F60B5F4C31999355309C6CCEB38204D10117557A39B5F59762") + + OracleUpsertAccount("123", "user", "user", UserAccount) + LinkAccount("123", user, "C78A534203008326ACA3C9D3D4338F81B9547002789B79CCAC3514784B780B078126B7AB705954C5CB36A5B171B97B0501EDE74BFDACE3D3096AA0D65B816007") + accountInPublic, ok := addressToAccount.Get(user.String()) + if !ok { + t.Fatalf("account not found") + } + account := AccountByID("123") + if accountInPublic != account { + t.Fatalf("account is not same") + } +} diff --git a/gnovm/stdlibs/crypto/elliptic/elliptic.gno b/gnovm/stdlibs/crypto/elliptic/elliptic.gno new file mode 100644 index 00000000000..290a8e53908 --- /dev/null +++ b/gnovm/stdlibs/crypto/elliptic/elliptic.gno @@ -0,0 +1,280 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package elliptic implements the standard NIST P-224, P-256, P-384, and P-521 +// elliptic curves over prime fields. +// +// Direct use of this package is deprecated, beyond the [P224], [P256], [P384], +// and [P521] values necessary to use [crypto/ecdsa]. Most other uses +// should migrate to the more efficient and safer [crypto/ecdh], or to +// third-party modules for lower-level functionality. +package elliptic + +import ( + "io" + "math/big" + "sync" +) + +// A Curve represents a short-form Weierstrass curve with a=-3. +// +// The behavior of Add, Double, and ScalarMult when the input is not a point on +// the curve is undefined. +// +// Note that the conventional point at infinity (0, 0) is not considered on the +// curve, although it can be returned by Add, Double, ScalarMult, or +// ScalarBaseMult (but not the [Unmarshal] or [UnmarshalCompressed] functions). +// +// Using Curve implementations besides those returned by [P224], [P256], [P384], +// and [P521] is deprecated. +type Curve interface { + // Params returns the parameters for the curve. + Params() *CurveParams + + // IsOnCurve reports whether the given (x,y) lies on the curve. + // + // Deprecated: this is a low-level unsafe API. For ECDH, use the crypto/ecdh + // package. The NewPublicKey methods of NIST curves in crypto/ecdh accept + // the same encoding as the Unmarshal function, and perform on-curve checks. + IsOnCurve(x, y *big.Int) bool + + // Add returns the sum of (x1,y1) and (x2,y2). + // + // Deprecated: this is a low-level unsafe API. + Add(x1, y1, x2, y2 *big.Int) (x, y *big.Int) + + // Double returns 2*(x,y). + // + // Deprecated: this is a low-level unsafe API. + Double(x1, y1 *big.Int) (x, y *big.Int) + + // ScalarMult returns k*(x,y) where k is an integer in big-endian form. + // + // Deprecated: this is a low-level unsafe API. For ECDH, use the crypto/ecdh + // package. Most uses of ScalarMult can be replaced by a call to the ECDH + // methods of NIST curves in crypto/ecdh. + ScalarMult(x1, y1 *big.Int, k []byte) (x, y *big.Int) + + // ScalarBaseMult returns k*G, where G is the base point of the group + // and k is an integer in big-endian form. + // + // Deprecated: this is a low-level unsafe API. For ECDH, use the crypto/ecdh + // package. Most uses of ScalarBaseMult can be replaced by a call to the + // PrivateKey.PublicKey method in crypto/ecdh. + ScalarBaseMult(k []byte) (x, y *big.Int) +} + +var mask = []byte{0xff, 0x1, 0x3, 0x7, 0xf, 0x1f, 0x3f, 0x7f} + +// GenerateKey returns a public/private key pair. The private key is +// generated using the given reader, which must return random data. +// +// Deprecated: for ECDH, use the GenerateKey methods of the [crypto/ecdh] package; +// for ECDSA, use the GenerateKey function of the crypto/ecdsa package. +func GenerateKey(curve Curve, rand io.Reader) (priv []byte, x, y *big.Int, err error) { + N := curve.Params().N + bitSize := N.BitLen() + byteLen := (bitSize + 7) / 8 + priv = make([]byte, byteLen) + + for x == nil { + _, err = io.ReadFull(rand, priv) + if err != nil { + return + } + // We have to mask off any excess bits in the case that the size of the + // underlying field is not a whole number of bytes. + priv[0] &= mask[bitSize%8] + // This is because, in tests, rand will return all zeros and we don't + // want to get the point at infinity and loop forever. + priv[1] ^= 0x42 + + // If the scalar is out of range, sample another random number. + if new(big.Int).SetBytes(priv).Cmp(N) >= 0 { + continue + } + + x, y = curve.ScalarBaseMult(priv) + } + return +} + +// Marshal converts a point on the curve into the uncompressed form specified in +// SEC 1, Version 2.0, Section 2.3.3. If the point is not on the curve (or is +// the conventional point at infinity), the behavior is undefined. +// +// Deprecated: for ECDH, use the crypto/ecdh package. This function returns an +// encoding equivalent to that of PublicKey.Bytes in crypto/ecdh. +func Marshal(curve Curve, x, y *big.Int) []byte { + panicIfNotOnCurve(curve, x, y) + + byteLen := (curve.Params().BitSize + 7) / 8 + + ret := make([]byte, 1+2*byteLen) + ret[0] = 4 // uncompressed point + + x.FillBytes(ret[1 : 1+byteLen]) + y.FillBytes(ret[1+byteLen : 1+2*byteLen]) + + return ret +} + +// MarshalCompressed converts a point on the curve into the compressed form +// specified in SEC 1, Version 2.0, Section 2.3.3. If the point is not on the +// curve (or is the conventional point at infinity), the behavior is undefined. +func MarshalCompressed(curve Curve, x, y *big.Int) []byte { + panicIfNotOnCurve(curve, x, y) + byteLen := (curve.Params().BitSize + 7) / 8 + compressed := make([]byte, 1+byteLen) + compressed[0] = byte(y.Bit(0)) | 2 + x.FillBytes(compressed[1:]) + return compressed +} + +// unmarshaler is implemented by curves with their own constant-time Unmarshal. +// +// There isn't an equivalent interface for Marshal/MarshalCompressed because +// that doesn't involve any mathematical operations, only FillBytes and Bit. +type unmarshaler interface { + Unmarshal([]byte) (x, y *big.Int) + UnmarshalCompressed([]byte) (x, y *big.Int) +} + +// Assert that the known curves implement unmarshaler. +var _ = []unmarshaler{p224, p256, p384, p521} + +// Unmarshal converts a point, serialized by [Marshal], into an x, y pair. It is +// an error if the point is not in uncompressed form, is not on the curve, or is +// the point at infinity. On error, x = nil. +// +// Deprecated: for ECDH, use the crypto/ecdh package. This function accepts an +// encoding equivalent to that of the NewPublicKey methods in crypto/ecdh. +func Unmarshal(curve Curve, data []byte) (x, y *big.Int) { + if c, ok := curve.(unmarshaler); ok { + return c.Unmarshal(data) + } + + byteLen := (curve.Params().BitSize + 7) / 8 + if len(data) != 1+2*byteLen { + return nil, nil + } + if data[0] != 4 { // uncompressed form + return nil, nil + } + p := curve.Params().P + x = new(big.Int).SetBytes(data[1 : 1+byteLen]) + y = new(big.Int).SetBytes(data[1+byteLen:]) + if x.Cmp(p) >= 0 || y.Cmp(p) >= 0 { + return nil, nil + } + if !curve.IsOnCurve(x, y) { + return nil, nil + } + return +} + +// UnmarshalCompressed converts a point, serialized by [MarshalCompressed], into +// an x, y pair. It is an error if the point is not in compressed form, is not +// on the curve, or is the point at infinity. On error, x = nil. +func UnmarshalCompressed(curve Curve, data []byte) (x, y *big.Int) { + if c, ok := curve.(unmarshaler); ok { + return c.UnmarshalCompressed(data) + } + + byteLen := (curve.Params().BitSize + 7) / 8 + if len(data) != 1+byteLen { + return nil, nil + } + if data[0] != 2 && data[0] != 3 { // compressed form + return nil, nil + } + p := curve.Params().P + x = new(big.Int).SetBytes(data[1:]) + if x.Cmp(p) >= 0 { + return nil, nil + } + // y² = x³ - 3x + b + y = curve.Params().polynomial(x) + y = y.ModSqrt(y, p) + if y == nil { + return nil, nil + } + if byte(y.Bit(0)) != data[0]&1 { + y.Neg(y).Mod(y, p) + } + if !curve.IsOnCurve(x, y) { + return nil, nil + } + return +} + +func panicIfNotOnCurve(curve Curve, x, y *big.Int) { + // (0, 0) is the point at infinity by convention. It's ok to operate on it, + // although IsOnCurve is documented to return false for it. See Issue 37294. + if x.Sign() == 0 && y.Sign() == 0 { + return + } + + if !curve.IsOnCurve(x, y) { + panic("crypto/elliptic: attempted operation on invalid point") + } +} + +var initonce sync.Once + +func initAll() { + initP224() + initP256() + initP384() + initP521() +} + +// P224 returns a [Curve] which implements NIST P-224 (FIPS 186-3, section D.2.2), +// also known as secp224r1. The CurveParams.Name of this [Curve] is "P-224". +// +// Multiple invocations of this function will return the same value, so it can +// be used for equality checks and switch statements. +// +// The cryptographic operations are implemented using constant-time algorithms. +func P224() Curve { + initonce.Do(initAll) + return p224 +} + +// P256 returns a [Curve] which implements NIST P-256 (FIPS 186-3, section D.2.3), +// also known as secp256r1 or prime256v1. The CurveParams.Name of this [Curve] is +// "P-256". +// +// Multiple invocations of this function will return the same value, so it can +// be used for equality checks and switch statements. +// +// The cryptographic operations are implemented using constant-time algorithms. +func P256() Curve { + initonce.Do(initAll) + return p256 +} + +// P384 returns a [Curve] which implements NIST P-384 (FIPS 186-3, section D.2.4), +// also known as secp384r1. The CurveParams.Name of this [Curve] is "P-384". +// +// Multiple invocations of this function will return the same value, so it can +// be used for equality checks and switch statements. +// +// The cryptographic operations are implemented using constant-time algorithms. +func P384() Curve { + initonce.Do(initAll) + return p384 +} + +// P521 returns a [Curve] which implements NIST P-521 (FIPS 186-3, section D.2.5), +// also known as secp521r1. The CurveParams.Name of this [Curve] is "P-521". +// +// Multiple invocations of this function will return the same value, so it can +// be used for equality checks and switch statements. +// +// The cryptographic operations are implemented using constant-time algorithms. +func P521() Curve { + initonce.Do(initAll) + return p521 +}