Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

backup.gno

4.94 Kb · 158 lines
  1package memba_notes_v1
  2
  3import (
  4	"bytes"
  5	"chain/runtime"
  6	"crypto/bech32"
  7	"crypto/sha256"
  8	"encoding/binary"
  9	"encoding/hex"
 10	"gno.land/p/samcrew/avl"
 11	keys "gno.land/r/samcrew/enckeys_v1"
 12	"strconv"
 13	"strings"
 14)
 15
 16const MaxBackupBytes = 365
 17
 18type backupRecord struct {
 19	Record               []byte
 20	Mode                 uint8
 21	Generation, Revision uint64
 22	PublicKeyHash        []byte
 23	Last                 receipt
 24}
 25
 26var backupLatest = avl.NewTree()
 27var backups = avl.NewTree()
 28
 29// A wallet-protected historical backup cannot become Vault-safe by relabelling.
 30var standardBackupKeys = avl.NewTree()
 31
 32func backupKey(who address, generation uint64) string {
 33	s := strconv.FormatUint(generation, 10)
 34	return string(who) + ":" + strings.Repeat("0", 20-len(s)) + s
 35}
 36func backupText(record []byte, pos, cap int) (string, int) {
 37	if pos+4 > len(record) {
 38		panic("notes: backup truncated")
 39	}
 40	size := int(binary.BigEndian.Uint32(record[pos : pos+4]))
 41	pos += 4
 42	if size < 1 || size > cap || pos+size > len(record) {
 43		panic("notes: backup text length")
 44	}
 45	for _, b := range record[pos : pos+size] {
 46		if b < 33 || b > 126 {
 47			panic("notes: backup ascii")
 48		}
 49	}
 50	return string(record[pos : pos+size]), pos + size
 51}
 52func validateBackup(who address, record []byte) (uint8, uint64, []byte) {
 53	if len(record) < 185 || len(record) > MaxBackupBytes || string(record[:4]) != "MNKB" || record[4] != 1 || record[5] != 1 {
 54		panic("notes: backup format")
 55	}
 56	mode := record[6]
 57	kdf := record[7]
 58	memory := binary.BigEndian.Uint32(record[8:12])
 59	passes := binary.BigEndian.Uint32(record[12:16])
 60	lanes := record[16]
 61	if !(mode == 1 && kdf == 1 && memory == 0 && passes == 0 && lanes == 0 || mode == 2 && kdf == 2 && memory == 65536 && passes == 3 && lanes == 1) {
 62		panic("notes: backup kdf profile")
 63	}
 64	chain, pos := backupText(record, 33, 64)
 65	realmPath, pos := backupText(record, pos, 128)
 66	if chain != runtime.ChainID() || realmPath != RealmPath || len(record) != pos+132 {
 67		panic("notes: backup network/length")
 68	}
 69	encoded, err := bech32.EncodeFromBase256("g", record[pos:pos+20])
 70	if err != nil || address(encoded) != who {
 71		panic("notes: backup address")
 72	}
 73	pos += 20
 74	generation := binary.BigEndian.Uint64(record[pos : pos+8])
 75	pos += 8
 76	hash := record[pos : pos+32]
 77	pk, gen, active := keys.GetKey(who, keys.SuiteXWingV1)
 78	h := sha256.Sum256(pk)
 79	if generation == 0 || generation != gen || !active || !bytes.Equal(hash, h[:]) {
 80		panic("notes: backup registry identity")
 81	}
 82	return mode, generation, copied(hash)
 83}
 84func SetSeedBackup(cur realm, baseRevision uint64, record []byte, opID [16]byte) {
 85	who := enter(cur, false)
 86	defer leave()
 87	if !cur.Previous().IsUserCall() {
 88		panic("notes: backup directly from account")
 89	}
 90	if opID == ([16]byte{}) || baseRevision == ^uint64(0) {
 91		panic("notes: invalid backup operation")
 92	}
 93	var revision uint64
 94	if v, ok := backupLatest.Get(string(who)); ok {
 95		r := v.(*backupRecord)
 96		revision = r.Revision
 97		if r.Last.Operation == opID {
 98			panic("notes: operation id reused")
 99		}
100	}
101	if revision != baseRevision {
102		panic("notes: stale backup revision")
103	}
104	mode, generation, hash := validateBackup(who, record)
105	exposureKey := string(who) + ":" + hex.EncodeToString(hash)
106	if mode == 2 {
107		if _, seen := standardBackupKeys.Get(exposureKey); seen {
108			panic("notes: vault requires independent identity")
109		}
110	} else {
111		standardBackupKeys.Set(exposureKey, true)
112	}
113	r := &backupRecord{copied(record), mode, generation, revision + 1, hash, receipt{opID, who, runtime.ChainHeight()}}
114	backups.Set(backupKey(who, generation), r)
115	backupLatest.Set(string(who), r)
116}
117func backupJSON(r *backupRecord, withRecord bool) string {
118	s := `{"mode":` + strconv.Itoa(int(r.Mode)) + `,"suite":"xwing-v1","generation":` + u(r.Generation) + `,"revision":` + u(r.Revision) + `,"public_key_hash":` + blob(r.PublicKeyHash) + `,"op_id":` + q(idText(r.Last.Operation)) + `,"actor":` + q(string(r.Last.Actor)) + `,"height":` + q(strconv.FormatInt(r.Last.Height, 10))
119	if withRecord {
120		s += `,"record":` + blob(r.Record)
121	}
122	return s + `}`
123}
124func SeedBackupJSON(who address) string {
125	v, ok := backupLatest.Get(string(keys.CanonicalAddress(who)))
126	if !ok {
127		return "null"
128	}
129	return backupJSON(v.(*backupRecord), true)
130}
131func SeedBackupAtJSON(who address, generation uint64) string {
132	v, ok := backups.Get(backupKey(keys.CanonicalAddress(who), generation))
133	if !ok {
134		return "null"
135	}
136	return backupJSON(v.(*backupRecord), true)
137}
138func SeedBackupsJSON(who address, cursor uint64, limit int) string {
139	who = keys.CanonicalAddress(who)
140	if limit < 1 || limit > MaxPage {
141		panic("notes: page limit")
142	}
143	items := []string{}
144	var next uint64
145	backups.Iterate(backupKey(who, cursor), string(who)+":\xff", func(k string, v interface{}) bool {
146		r := v.(*backupRecord)
147		if r.Generation <= cursor {
148			return false
149		}
150		items = append(items, backupJSON(r, false))
151		next = r.Generation
152		return len(items) == limit
153	})
154	if len(items) < limit {
155		next = 0
156	}
157	return `{"items":[` + strings.Join(items, ",") + `],"next_cursor":` + u(next) + `}`
158}