forked from lurk-lab/lurk-beta
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathbank.lurk
242 lines (156 loc) · 8.68 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 inital 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 modifes 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 0x014a94be6a32b6c74be26071f627aff0a06ef3caade04d335958b8a3bb818925 '(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 "Nova_Pallas_10_3ec93e16c42eb6822e1597d915c0a1f284fea322297786eb506bd814e29e33d3")
;; 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 0x099f5f1cef164ac0a5ef8d6b0cefd930d2d0bf40666df3ae1b12abc33a5d297a '(1 0 2))
!(prove)
!(verify "Nova_Pallas_10_118481b08f97e5ea84499dd305d5c2b86089d5d5e5253e6c345119d55020bde3")
;; Then we can transfer 5 more, proceeding from the new head of the chain.
!(chain 0x11abc1857d88646e3a6a2d0be605a3639feca2b79191c65efb7c6c139465820e '(5 0 2))
!(prove)
!(verify "Nova_Pallas_10_3b5c0928297a8380b1003b343b2b35522b3c16a03fbf69b3deea91c50cbdfc66")
;; And once more, this time we'll transfer 20 from Turing to Church.
!(chain 0x007f5ed09b15a2af11a0504a46884aff364eb7bb1e7e9543431443b962166cb5 '(20 1 0))
!(prove)
!(verify "Nova_Pallas_10_3dc75e7fc1fd2a629b04e6e7469ed20711a4012b03c3f1205c18a351b9cdb7ca")