-
Notifications
You must be signed in to change notification settings - Fork 57
/
bank.lurk
242 lines (156 loc) · 8.7 KB
/
bank.lurk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
;; Let's build a functional bank.
;; We'll start by defining a tiny database of people and their balances.
!(def people '((:first-name "Alonzo" :last-name "Church" :balance 123 :id 0)
(:first-name "Alan" :last-name "Turing" :balance 456 :id 1)
(:first-name "Satoshi" :last-name "Nakamoto" :balance 9000 :id 2)))
;; We need a way to look up keys in the database records, so we define a getter.
!(defrec get (lambda (key plist)
(if plist
(if (eq key (car plist))
(car (cdr plist))
(get key (cdr (cdr plist))))
nil)))
;; Let's test it by getting the last name of the first person.
(get :last-name (car people))
;; We also need some functional helpers. Map applies a function to each element of a list.
!(defrec map (lambda (f list)
(if list
(cons (f (car list))
(map f (cdr list)))
())))
;; Filter removes elements of a list that don't satisfy a predicate function.
!(defrec filter (lambda (pred list)
(if list
(if (pred (car list))
(cons (car list) (filter pred (cdr list)))
(filter pred (cdr list)))
())))
;; Let's write a predicate that is true when an entry's balance is at least a specified amount.
!(def balance-at-least? (lambda (x)
(lambda (entry)
(>= (get :balance entry) x))))
;; Putting it together, let's query the database for the first name of people with a balance of at least 200.
(map (get :first-name) (filter (balance-at-least? 200) people))
;; And let's get everyone's balance.
(map (get :balance) people)
;; We can define a function to sum a list of values recursively.
!(defrec sum (lambda (vals)
(if vals
(+ (car vals) (sum (cdr vals)))
0)))
;; Apply this to the balances, and we can calculate the total funds in a database.
!(def total-funds (lambda (db) (sum (map (get :balance) db))))
;; Let's snapshot the initial funds.
!(def initial-total-funds (emit (total-funds people)))
;; We can check a database to see if funds were conserved by comparing with the initial total.
!(def funds-are-conserved? (lambda (db) (= initial-total-funds (total-funds db))))
;; Here's a setter.
!(def set (lambda (key value plist)
(letrec ((aux (lambda (acc plist)
(if plist
(if (eq key (car plist))
(aux (cons key (cons value acc))
(cdr (cdr plist)))
(aux (cons (car plist)
(cons (car (cdr plist)) acc))
(cdr (cdr plist))))
acc))))
(aux () plist))))
;; We can use it to change a balance.
(set :balance 666 (car people))
;; More useful is an update function that modifies a field based on its current value.
!(def update (lambda (key update-fn plist)
(letrec ((aux (lambda (acc plist)
(if plist
(if (eq key (car plist))
(aux (cons key (cons (update-fn (car (cdr plist))) acc))
(cdr (cdr plist)))
(aux (cons (car plist)
(cons (car (cdr plist)) acc))
(cdr (cdr plist))))
acc))))
(aux () plist))))
;; For example, we can double Church's balance.
(update :balance (lambda (x) (* x 2)) (car people))
;; And, here's a function that updates only the rows that satisfy a predicate.
!(def update-where (lambda (predicate key update-fn db)
(letrec ((aux (lambda (db)
(if db
(if (predicate (car db))
(cons (update key update-fn (car db))
(aux (cdr db)))
(cons (car db)
(aux (cdr db))))
nil))))
(aux db))))
;; Let's write a predicate for selecting rows by ID.
!(def has-id? (lambda (id x) (eq id (get :id x))))
;; That lets us change the first letter of the first name of the person with ID 2.
(update-where (has-id? 2) :first-name (lambda (x) (strcons 'Z' (cdr x))) people)
;; Finally, let's put it all together and write a function to send balances from one account to another.
;; We select the from account by filtering on id,
;; Check that its balance is at least the transfer amount,
;; Then update both the sender and receiver's balance by the amount.
;; If the sender doesn't have enough funds, we display an insufficient funds message, and return the database unchanged.
!(def send (lambda (amount from-id to-id db)
(let ((from (car (filter (has-id? from-id) db))))
(if (balance-at-least? amount from)
(let ((debited (update-where (has-id? from-id) :balance (lambda (x) (- x amount)) db))
(credited (update-where (has-id? to-id) :balance (lambda (x) (+ x amount)) debited)))
credited)
(begin (emit "INSUFFICIENT FUNDS") db)))))
;; In token of this new function, we'll call our database of people a ledger.
!(def ledger people)
;; We can send 200 from account 1 to account 0.
!(def ledger1 (send 200 1 0 ledger))
ledger1
;; And assert that funds were conserved. (Nothing was created or destroyed.)
!(assert (funds-are-conserved? ledger1))
;; Or, using the original ledger, we can try sending 200 from account 0 to 1.
!(def ledger2 (send 200 0 1 ledger))
ledger2
;; Notice that this transaction fails due to insufficient funds. However,
!(assert (funds-are-conserved? ledger2))
;; funds are still conserved
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Functional Commitment to a Database Value
;; Let's define a function that takes a database and returns a transfer function.
;; Transfer function takes an amount, a source id, and a destination id, then attempts to send the funds.
!(def fn<-db (lambda (db)
(lambda (transfer)
(let ((amount (car transfer))
(rest (cdr transfer))
(from-id (car rest))
(rest (cdr rest))
(to-id (car rest)))
(send (emit amount) (emit from-id) (emit to-id) (emit db))))))
;; Now let's create a transfer function for our ledger, and commit to it.
!(commit (fn<-db ledger))
;; Now we can open the committed ledger transfer function on a transaction.
!(call 0x207bca2dcc9fb4d1d3e3939d2c1e6a3adaabf41dc0174dd39b9949d697b55aaa '(1 0 2))
;; And the record reflects that Church sent one unit to Satoshi.
;; Let's prove it.
!(prove)
;; We can verify the proof..
!(verify "supernova_bn256_10_11882126c5f0dd9a94f76ff58ea5c499cfe2364162585c320c55f53651e16ffd")
;; Unfortunately, this functional commitment doesn't let us maintain state.
;; Let's turn our single-transaction function into a chained function.
!(def chain<-db (lambda (db secret)
(letrec ((foo (lambda (state msg)
(let ((new-state ((fn<-db state) msg)))
(cons new-state (hide secret (foo new-state)))))))
(foo db))))
;; We'll call this on our ledger, and protect write-access with a secret value (999).
!(commit (chain<-db ledger 999))
;; Now we can transfer one unit from Church to Satoshi like before.
!(chain 0x07ec757321b69b7902c2c5629535c343c1eb335c0ca12275a95ec28918e0af47 '(1 0 2))
!(prove)
!(verify "supernova_bn256_10_0b72908859e73ee3014067a5eaa557a995aea262cfb5f3621922024a176b8281")
;; Then we can transfer 5 more, proceeding from the new head of the chain.
!(chain 0x18b99c6b580d518129921ebf70023b5d757861b935f7f537460c99130eb4447d '(5 0 2))
!(prove)
!(verify "supernova_bn256_10_0d8159faab0d85855d4cf53c7e36a2357a1766a1540afbafb0ef93d7e1537ca8")
;; And once more, this time we'll transfer 20 from Turing to Church.
!(chain 0x0b2d868fad0e6ec88e9ba6818ae9a0345aab06abc2c226200ff3ed45c60a41db '(20 1 0))
!(prove)
!(verify "supernova_bn256_10_0a253296edb4d6c204edd92e63176efed7c30e9f5928b52ba9be2b3f2e6e8b08")