Skip to content

Commit 3b437af

Browse files
VillaquiranmNormann0izn0iz
authored
feat: use teritori/ghverify instead of gnoland (#1510)
Signed-off-by: Norman <norman@samourai.coop> Co-authored-by: Norman <norman@samourai.coop> Co-authored-by: n0izn0iz <n0izn0iz@users.noreply.github.com>
1 parent 131e649 commit 3b437af

File tree

8 files changed

+309
-10
lines changed

8 files changed

+309
-10
lines changed

gno/p/basedao/basedao.gno

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Config struct {
2323
ImageURI string
2424
Members *MembersStore
2525
NoDefaultHandlers bool
26+
NoEvents bool
2627
InitialCondition daocond.Condition
2728
SetProfileString ProfileStringSetter
2829
GetProfileString ProfileStringGetter
@@ -32,6 +33,8 @@ type ProfileStringSetter func(field string, value string) bool
3233
type ProfileStringGetter func(addr std.Address, field string, def string) string
3334

3435
func New(conf *Config) *DAO {
36+
// XXX: emit events from memberstore
37+
3538
members := conf.Members
3639
if members == nil {
3740
members = NewMembersStore(nil, nil)
@@ -41,8 +44,11 @@ func New(conf *Config) *DAO {
4144
panic(errors.New("GetProfileString is required"))
4245
}
4346

47+
core := daokit.NewCore()
48+
core.NoEvents = conf.NoEvents
49+
4450
dao := &DAO{
45-
Core: daokit.NewCore(),
51+
Core: core,
4652
Members: members,
4753
GetProfileString: conf.GetProfileString,
4854
Realm: std.CurrentRealm(),
@@ -56,11 +62,8 @@ func New(conf *Config) *DAO {
5662
}
5763

5864
if !conf.NoDefaultHandlers {
59-
if conf.SetProfileString == nil {
60-
panic(errors.New("SetProfileString is required"))
61-
}
62-
6365
if conf.InitialCondition == nil {
66+
// XXX: this won't work because members threshold uses events
6467
conf.InitialCondition = daocond.MembersThreshold(0.6, members.IsMember, members.MembersCount)
6568
}
6669

gno/p/daokit/daokit.gno

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
type Core struct {
1010
Resources *resourcesStore
1111
ProposalModule *ProposalModule
12+
NoEvents bool
1213
}
1314

1415
func NewCore() *Core {
@@ -32,12 +33,14 @@ func (d *Core) Vote(voterID string, proposalID uint64, vote daocond.Vote) {
3233
panic("proposal is not open")
3334
}
3435

35-
e := &daocond.EventVote{
36-
VoterID: voterID,
37-
Vote: daocond.Vote(vote),
36+
if !d.NoEvents {
37+
e := &daocond.EventVote{
38+
VoterID: voterID,
39+
Vote: daocond.Vote(vote),
40+
}
41+
proposal.ConditionState.HandleEvent(e, proposal.Votes)
3842
}
3943

40-
proposal.ConditionState.HandleEvent(e, proposal.Votes)
4144
proposal.Votes[voterID] = daocond.Vote(vote)
4245

4346
}

gno/r/ghverify/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# ghverify
2+
3+
Fork of [ghverify](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland/ghverify)
4+
5+
This realm is intended to enable off chain gno address to github handle verification.
6+
The steps are as follows:
7+
- A user calls `RequestVerification` and provides a github handle. This creates a new static oracle feed.
8+
- An off-chain agent controlled by the owner of this realm requests current feeds using the `GnorkleEntrypoint` function and provides a message of `"request"`
9+
- The agent receives the task information that includes the github handle and the gno address. It performs the verification step by checking whether this github user has the address in a github repository it controls.
10+
- The agent publishes the result of the verification by calling `GnorkleEntrypoint` with a message structured like: `"ingest,<task id>,<verification status>"`. The verification status is `OK` if verification succeeded and any other value if it failed.
11+
- The oracle feed's ingester processes the verification and the handle to address mapping is written to the avl trees that exist as ghverify realm variables.

gno/r/ghverify/contract.gno

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package ghverify
2+
3+
import (
4+
"errors"
5+
"std"
6+
7+
"gno.land/p/demo/avl"
8+
"gno.land/p/demo/gnorkle/feeds/static"
9+
"gno.land/p/demo/gnorkle/gnorkle"
10+
"gno.land/p/demo/gnorkle/message"
11+
)
12+
13+
const (
14+
// The agent should send this value if it has verified the github handle.
15+
verifiedResult = "OK"
16+
)
17+
18+
var (
19+
ownerAddress = std.GetOrigCaller()
20+
oracle *gnorkle.Instance
21+
postHandler postGnorkleMessageHandler
22+
23+
handleToAddressMap = avl.NewTree()
24+
addressToHandleMap = avl.NewTree()
25+
)
26+
27+
func init() {
28+
oracle = gnorkle.NewInstance()
29+
oracle.AddToWhitelist("", []string{string(ownerAddress)})
30+
}
31+
32+
type postGnorkleMessageHandler struct{}
33+
34+
// Handle does post processing after a message is ingested by the oracle feed. It extracts the value to realm
35+
// storage and removes the feed from the oracle.
36+
func (h postGnorkleMessageHandler) Handle(i *gnorkle.Instance, funcType message.FuncType, feed gnorkle.Feed) error {
37+
if funcType != message.FuncTypeIngest {
38+
return nil
39+
}
40+
41+
result, _, consumable := feed.Value()
42+
if !consumable {
43+
return nil
44+
}
45+
46+
// The value is consumable, meaning the ingestion occurred, so we can remove the feed from the oracle
47+
// after saving it to realm storage.
48+
defer oracle.RemoveFeed(feed.ID())
49+
50+
// Couldn't verify; nothing to do.
51+
if result.String != verifiedResult {
52+
return nil
53+
}
54+
55+
feedTasks := feed.Tasks()
56+
if len(feedTasks) != 1 {
57+
return errors.New("expected feed to have exactly one task")
58+
}
59+
60+
task, ok := feedTasks[0].(*verificationTask)
61+
if !ok {
62+
return errors.New("expected ghverify task")
63+
}
64+
65+
handleToAddressMap.Set(task.githubHandle, task.gnoAddress)
66+
addressToHandleMap.Set(task.gnoAddress, task.githubHandle)
67+
return nil
68+
}
69+
70+
// RequestVerification creates a new static feed with a single task that will
71+
// instruct an agent to verify the github handle / gno address pair.
72+
func RequestVerification(githubHandle string) {
73+
gnoAddress := string(std.GetOrigCaller())
74+
if err := oracle.AddFeeds(
75+
static.NewSingleValueFeed(
76+
gnoAddress,
77+
"string",
78+
&verificationTask{
79+
gnoAddress: gnoAddress,
80+
githubHandle: githubHandle,
81+
},
82+
),
83+
); err != nil {
84+
panic(err)
85+
}
86+
std.Emit(
87+
"verification_requested",
88+
"from", gnoAddress,
89+
"handle", githubHandle,
90+
)
91+
}
92+
93+
// GnorkleEntrypoint is the entrypoint to the gnorkle oracle handler.
94+
func GnorkleEntrypoint(message string) string {
95+
result, err := oracle.HandleMessage(message, postHandler)
96+
if err != nil {
97+
panic(err)
98+
}
99+
100+
return result
101+
}
102+
103+
// SetOwner transfers ownership of the contract to the given address.
104+
func SetOwner(owner std.Address) {
105+
if ownerAddress != std.GetOrigCaller() {
106+
panic("only the owner can set a new owner")
107+
}
108+
109+
ownerAddress = owner
110+
111+
// In the context of this contract, the owner is the only one that can
112+
// add new feeds to the oracle.
113+
oracle.ClearWhitelist("")
114+
oracle.AddToWhitelist("", []string{string(ownerAddress)})
115+
}
116+
117+
// GetHandleByAddress returns the github handle associated with the given gno address.
118+
func GetHandleByAddress(address string) string {
119+
if value, ok := addressToHandleMap.Get(address); ok {
120+
return value.(string)
121+
}
122+
123+
return ""
124+
}
125+
126+
// GetAddressByHandle returns the gno address associated with the given github handle.
127+
func GetAddressByHandle(handle string) string {
128+
if value, ok := handleToAddressMap.Get(handle); ok {
129+
return value.(string)
130+
}
131+
132+
return ""
133+
}
134+
135+
// Render returns a json object string will all verified handle -> address mappings.
136+
func Render(_ string) string {
137+
result := "{"
138+
var appendComma bool
139+
handleToAddressMap.Iterate("", "", func(handle string, address interface{}) bool {
140+
if appendComma {
141+
result += ","
142+
}
143+
144+
result += `"` + handle + `": "` + address.(string) + `"`
145+
appendComma = true
146+
147+
return false
148+
})
149+
150+
return result + "}"
151+
}

gno/r/ghverify/contract_test.gno

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package ghverify
2+
3+
import (
4+
"std"
5+
"testing"
6+
7+
"gno.land/p/demo/testutils"
8+
)
9+
10+
func TestVerificationLifecycle(t *testing.T) {
11+
defaultAddress := std.GetOrigCaller()
12+
user1Address := std.Address(testutils.TestAddress("user 1"))
13+
user2Address := std.Address(testutils.TestAddress("user 2"))
14+
15+
// Verify request returns no feeds.
16+
result := GnorkleEntrypoint("request")
17+
if result != "[]" {
18+
t.Fatalf("expected empty request result, got %s", result)
19+
}
20+
21+
// Make a verification request with the created user.
22+
std.TestSetOrigCaller(user1Address)
23+
RequestVerification("deelawn")
24+
25+
// A subsequent request from the same address should panic because there is
26+
// already a feed with an ID of this user's address.
27+
var errMsg string
28+
func() {
29+
defer func() {
30+
if r := recover(); r != nil {
31+
errMsg = r.(error).Error()
32+
}
33+
}()
34+
RequestVerification("deelawn")
35+
}()
36+
if errMsg != "feed already exists" {
37+
t.Fatalf("expected feed already exists, got %s", errMsg)
38+
}
39+
40+
// Verify the request returns no feeds for this non-whitelisted user.
41+
result = GnorkleEntrypoint("request")
42+
if result != "[]" {
43+
t.Fatalf("expected empty request result, got %s", result)
44+
}
45+
46+
// Make a verification request with the created user.
47+
std.TestSetOrigCaller(user2Address)
48+
RequestVerification("omarsy")
49+
50+
// Set the caller back to the whitelisted user and verify that the feed data
51+
// returned matches what should have been created by the `RequestVerification`
52+
// invocation.
53+
std.TestSetOrigCaller(defaultAddress)
54+
result = GnorkleEntrypoint("request")
55+
expResult := `[{"id":"` + string(user1Address) + `","type":"0","value_type":"string","tasks":[{"gno_address":"` +
56+
string(user1Address) + `","github_handle":"deelawn"}]},` +
57+
`{"id":"` + string(user2Address) + `","type":"0","value_type":"string","tasks":[{"gno_address":"` +
58+
string(user2Address) + `","github_handle":"omarsy"}]}]`
59+
if result != expResult {
60+
t.Fatalf("expected request result %s, got %s", expResult, result)
61+
}
62+
63+
// Try to trigger feed ingestion from the non-authorized user.
64+
std.TestSetOrigCaller(user1Address)
65+
func() {
66+
defer func() {
67+
if r := recover(); r != nil {
68+
errMsg = r.(error).Error()
69+
}
70+
}()
71+
GnorkleEntrypoint("ingest," + string(user1Address) + ",OK")
72+
}()
73+
if errMsg != "caller not whitelisted" {
74+
t.Fatalf("expected caller not whitelisted, got %s", errMsg)
75+
}
76+
77+
// Set the caller back to the whitelisted user and transfer contract ownership.
78+
std.TestSetOrigCaller(defaultAddress)
79+
SetOwner(defaultAddress)
80+
81+
// Now trigger the feed ingestion from the user and new owner and only whitelisted address.
82+
GnorkleEntrypoint("ingest," + string(user1Address) + ",OK")
83+
GnorkleEntrypoint("ingest," + string(user2Address) + ",OK")
84+
85+
// Verify the ingestion autocommitted the value and triggered the post handler.
86+
data := Render("")
87+
expResult = `{"deelawn": "` + string(user1Address) + `","omarsy": "` + string(user2Address) + `"}`
88+
if data != expResult {
89+
t.Fatalf("expected render data %s, got %s", expResult, data)
90+
}
91+
92+
// Finally make sure the feed was cleaned up after the data was committed.
93+
result = GnorkleEntrypoint("request")
94+
if result != "[]" {
95+
t.Fatalf("expected empty request result, got %s", result)
96+
}
97+
98+
// Check that the accessor functions are working as expected.
99+
if handle := GetHandleByAddress(string(user1Address)); handle != "deelawn" {
100+
t.Fatalf("expected deelawn, got %s", handle)
101+
}
102+
if address := GetAddressByHandle("deelawn"); address != string(user1Address) {
103+
t.Fatalf("expected %s, got %s", string(user1Address), address)
104+
}
105+
}

gno/r/ghverify/gno.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module gno.land/r/teritori/ghverify

gno/r/ghverify/task.gno

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package ghverify
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
)
7+
8+
type verificationTask struct {
9+
gnoAddress string
10+
githubHandle string
11+
}
12+
13+
// MarshalJSON marshals the task contents to JSON.
14+
func (t *verificationTask) MarshalJSON() ([]byte, error) {
15+
buf := new(bytes.Buffer)
16+
w := bufio.NewWriter(buf)
17+
18+
w.Write(
19+
[]byte(`{"gno_address":"` + t.gnoAddress + `","github_handle":"` + t.githubHandle + `"}`),
20+
)
21+
22+
w.Flush()
23+
return buf.Bytes(), nil
24+
}

gno/r/govdao/govdao.gno

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"gno.land/p/teritori/daokit"
77
"gno.land/p/teritori/role_manager"
88
"gno.land/r/demo/profile"
9-
"gno.land/r/gnoland/ghverify"
9+
"gno.land/r/teritori/ghverify"
1010
)
1111

1212
const (
@@ -50,6 +50,7 @@ func init() {
5050
GetProfileString: profile.GetStringField,
5151
})
5252

53+
// XXX: t1Supermajority won't work because daocond.RoleThreshold uses events
5354
t1Supermajority := daocond.RoleThreshold(0.66, Tier1, dao.Members.HasRole, dao.Members.CountMembersWithRole)
5455
supermajority := daocond.GovDaoCondThreshold(0.66, []string{Tier1, Tier2, Tier3}, dao.Members.HasRole, dao.Members.CountMembersWithRole)
5556
majorityWithoutT3 := daocond.GovDaoCondThreshold(0.5, []string{Tier1, Tier2}, dao.Members.HasRole, dao.Members.CountMembersWithRole)

0 commit comments

Comments
 (0)